Repository files navigation

Image Optimizer

A fast, self-hostable web app that turns one image into every size and format you actually need — web formats, social media presets, and a complete favicon pack — and hands them back as a single ZIP.

Try the live demo at img.hra42.com

Optimize an image into multiple targets, then download a ZIP.

Drop your images in the browser, tick the targets you want, and download the optimized results. No accounts, no uploads kept on disk, no setup beyond running one Docker container.

Why

Preparing images for the web is repetitive: resize for Instagram, crop for an Open Graph card, generate a dozen favicon files, re-encode to WebP/AVIF for performance… usually across several tools. This does all of it in one drag-and-drop step, runs entirely on your own server, and never writes your images to disk.

Features

  • Many targets at once — pick any combination of presets; each image is optimized for every selected target in parallel.
  • Modern + classic formats — outputs WebP, AVIF, JPEG (progressive), and PNG.
  • Wide input support — JPEG, PNG, WebP, AVIF, and iPhone HEIC/HEIF.
  • Drop-in favicon pack — one click produces favicon.ico, all the PNG sizes, apple-touch-icon, site.webmanifest, and a ready-to-paste HTML snippet.
  • Social presets — Instagram, LinkedIn, X, Facebook, Pinterest, Open Graph, plus email/web banners.
  • Live progress — per-target progress streamed over Server-Sent Events.
  • Privacy by design — images are processed in memory only and deleted right after download (or after a short timeout); nothing is stored.
  • Single container — the Svelte UI is embedded into the Go binary, so the whole app is one small Docker image with no external services.

How it works

  1. Add images — drag & drop or browse (JPEG, PNG, WebP, AVIF, HEIC).
  2. Pick targets — choose one or more presets.
  3. Optimize — each image is resized/re-encoded for every target; watch live progress.
  4. Download — get a single ZIP with every optimized variant, named by preset.

Quick start

Run the prebuilt-style image locally (only Docker required):

docker build -t image-optimizer .
docker run -p 3000:3000 image-optimizer

Then open http://localhost:3000 and start dropping images. See Production image for deployment details and Development for the hot-reload setup.

Tech stack

  • Go + Fiber v3 — single compiled binary, low memory footprint
  • govips / libvips — the image processing engine
  • Svelte + Vite — zero-runtime SPA, embedded via //go:embed
  • Single Docker container — Fiber serves both the API and the SPA

Project layout

.
├── main.go # Fiber app, config, graceful shutdown, embeds frontend/dist
├── config/ # env-var configuration (port, limits, workers, job TTL)
├── handlers/ # HTTP handlers: health, upload, progress (SSE), download
├── processor/ # govips pipeline, worker semaphore, presets
├── frontend/ # Svelte + Vite app; dist/ is the go:embed target
├── Dockerfile # 3-stage build: Vite → Go (cgo+libvips) → debian-slim
└── docker-compose.yml # local dev with hot-reload

Note on frontend/dist: a small stub index.html is committed so go build works locally without first running Vite. The Docker frontend stage overwrites it with the real Vite build.

Supported formats & presets

Input: JPEG, PNG, WebP, AVIF, and HEIC/HEIF (iPhone photos). HEIC/HEIF are decoded via libvips' heifload (libheif/libde265, bundled in the Docker images).

The convert_* presets are the "just turn this into a usable file" path: faithful, high-quality format conversion at the original size (no crop). Handy for turning an iPhone HEIC straight into JPEG/PNG/WebP/AVIF without any other tool.

Output presets (registry: processor/preset.go):

PresetFormatDimensionsNotes
convert_jpegJPEGoriginalquality 92, progressive — faithful conversion
convert_pngPNGoriginalcompression 6 (lossless)
convert_webpWebPoriginalquality 90 — faithful conversion
convert_avifAVIForiginalquality 80, effort 4 — faithful conversion
compress_bestsourceoriginalre-encode only — near-lossless, keeps the source format
compress_balancedsourceoriginalre-encode only — strong shrink, great quality
compress_maxsourceoriginalre-encode only — aggressive (PNG uses a lossy palette when it helps)
website_webpWebPoriginalquality 80 (web-optimized)
website_avifAVIForiginalquality 60, effort 4 (web-optimized)
jpeg_originalJPEGoriginalquality 80, progressive
png_originalPNGoriginalcompression 6
instagram_squareJPEG1080×1080progressive
instagram_portraitJPEG1080×1350progressive
instagram_storyJPEG1080×1920progressive
linkedinJPEG1200×627progressive
twitterJPEG1200×675progressive
facebook_postJPEG1200×630progressive
pinterest_pinJPEG1000×1500progressive
og_imagePNG1200×630compression 6
faviconfavicon pack (see below)
thumbnailPNG400×400compression 6
email_headerJPEG600×200progressive
web_bannerJPEG1920×480progressive

Fixed-size presets center-crop to the target dimensions; *_original presets keep the source dimensions and just re-encode + strip metadata.

The compress_* presets are the "just make this smaller for the web" path: they keep the source format (JPEG→JPEG, PNG→PNG, WebP→WebP; HEIC→JPEG, SVG→PNG) and the original dimensions, tuning only the encoding across three honest tiers (best / balanced / max savings). PNG savings are modest — it has no quality knob — so only compress_max tries a lossy palette, and only keeps it when it actually comes out smaller.

Favicon pack

The favicon preset is a multi-file output: instead of one image it generates a complete drop-in icon set, bundled under a favicon/ folder in the ZIP:

  • favicon.ico (multi-size 16/32/48, hand-built ICO container)
  • favicon-16x16.png, favicon-32x32.png, favicon-48x48.png
  • apple-touch-icon.png (180×180)
  • android-chrome-192x192.png, android-chrome-512x512.png
  • site.webmanifest and a README.txt with the exact <head><link> snippet

The pack is generated from a center-cropped square master. The multi-file plumbing lives in Preset.Kind / Result.Files (processor/preset.go, processor/favicon_vips.go, processor/ico.go).

Development

Hot-reload for both backend (Air) and frontend (Vite dev server):

docker compose up

Production image

The single self-contained image (Vite build embedded into the Go binary):

docker build -t image-optimizer .
docker run -p 3000:3000 image-optimizer
curl http://localhost:3000/health # -> 200 {"status":"ok"}

The final image contains only the Go binary plus the libvips runtime libs (libvips42) on debian:bookworm-slim, and runs as a non-root user.

Health check

The image ships a Docker HEALTHCHECK that probes GET /health. The binary self-probes via its -healthcheck flag, so no curl/wget is needed in the minimal runtime image:

docker run -d -p 3000:3000 image-optimizer
docker ps # STATUS shows "healthy" once the start period elapses

Graceful shutdown

On SIGINT/SIGTERM the server stops accepting new connections, drains in-flight jobs (so their SSE clients receive a terminal event and the ZIP stays briefly downloadable), tears down libvips, then exits. Each phase is bounded by a 30s timeout so shutdown never hangs.

Job lifetime

Job state lives in memory only — there is no disk temp storage. A job is freed when its ZIP is downloaded, or by a background reaper after JOB_TTL_MINUTES (default 10), whichever comes first. This bounds memory for jobs that are never downloaded.

Environment variables

All configuration is via environment variables, read once at startup (the resolved values are logged). Invalid or non-positive numeric values fall back to the default rather than failing startup.

VariableDefaultDescription
PORT3000TCP port the HTTP server listens on.
MAX_FILE_SIZE_MB50Per-file upload cap. Larger files are rejected with 400.
WORKER_COUNTnumber of CPUsMax concurrent libvips pipelines (the real concurrency limit).
JOB_TTL_MINUTES10How long a job's in-memory state is retained before the reaper frees it.

The whole-request multipart body limit is derived from MAX_FILE_SIZE_MB plus headroom for multiple files and multipart boundaries.

Requirements

  • Go 1.26+ · Node 22+ · Docker
  • libvips ≥ 8.14 (provided inside the Docker images; install locally only if building the Go binary outside Docker)

License

Released into the public domain under The Unlicense — do whatever you want with it.

About

High-performance web image optimizer with multi-target presets (web, Instagram, LinkedIn, etc.) built on Fiber v3 + govips (libvips).

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Image Optimizer

A fast, self-hostable web app that turns one image into every size and format you actually need — web formats, social media presets, and a complete favicon pack — and hands them back as a single ZIP.

Try the live demo at img.hra42.com

Optimize an image into multiple targets, then download a ZIP.

Drop your images in the browser, tick the targets you want, and download the optimized results. No accounts, no uploads kept on disk, no setup beyond running one Docker container.

Why

Preparing images for the web is repetitive: resize for Instagram, crop for an Open Graph card, generate a dozen favicon files, re-encode to WebP/AVIF for performance… usually across several tools. This does all of it in one drag-and-drop step, runs entirely on your own server, and never writes your images to disk.

Features

  • Many targets at once — pick any combination of presets; each image is optimized for every selected target in parallel.
  • Modern + classic formats — outputs WebP, AVIF, JPEG (progressive), and PNG.
  • Wide input support — JPEG, PNG, WebP, AVIF, and iPhone HEIC/HEIF.
  • Drop-in favicon pack — one click produces favicon.ico, all the PNG sizes, apple-touch-icon, site.webmanifest, and a ready-to-paste HTML snippet.
  • Social presets — Instagram, LinkedIn, X, Facebook, Pinterest, Open Graph, plus email/web banners.
  • Live progress — per-target progress streamed over Server-Sent Events.
  • Privacy by design — images are processed in memory only and deleted right after download (or after a short timeout); nothing is stored.
  • Single container — the Svelte UI is embedded into the Go binary, so the whole app is one small Docker image with no external services.

How it works

  1. Add images — drag & drop or browse (JPEG, PNG, WebP, AVIF, HEIC).
  2. Pick targets — choose one or more presets.
  3. Optimize — each image is resized/re-encoded for every target; watch live progress.
  4. Download — get a single ZIP with every optimized variant, named by preset.

Quick start

Run the prebuilt-style image locally (only Docker required):

docker build -t image-optimizer .
docker run -p 3000:3000 image-optimizer

Then open http://localhost:3000 and start dropping images. See Production image for deployment details and Development for the hot-reload setup.

Tech stack

  • Go + Fiber v3 — single compiled binary, low memory footprint
  • govips / libvips — the image processing engine
  • Svelte + Vite — zero-runtime SPA, embedded via //go:embed
  • Single Docker container — Fiber serves both the API and the SPA

Project layout

.
├── main.go # Fiber app, config, graceful shutdown, embeds frontend/dist
├── config/ # env-var configuration (port, limits, workers, job TTL)
├── handlers/ # HTTP handlers: health, upload, progress (SSE), download
├── processor/ # govips pipeline, worker semaphore, presets
├── frontend/ # Svelte + Vite app; dist/ is the go:embed target
├── Dockerfile # 3-stage build: Vite → Go (cgo+libvips) → debian-slim
└── docker-compose.yml # local dev with hot-reload

Note on frontend/dist: a small stub index.html is committed so go build works locally without first running Vite. The Docker frontend stage overwrites it with the real Vite build.

Supported formats & presets

Input: JPEG, PNG, WebP, AVIF, and HEIC/HEIF (iPhone photos). HEIC/HEIF are decoded via libvips' heifload (libheif/libde265, bundled in the Docker images).

The convert_* presets are the "just turn this into a usable file" path: faithful, high-quality format conversion at the original size (no crop). Handy for turning an iPhone HEIC straight into JPEG/PNG/WebP/AVIF without any other tool.

Output presets (registry: processor/preset.go):

PresetFormatDimensionsNotes
convert_jpegJPEGoriginalquality 92, progressive — faithful conversion
convert_pngPNGoriginalcompression 6 (lossless)
convert_webpWebPoriginalquality 90 — faithful conversion
convert_avifAVIForiginalquality 80, effort 4 — faithful conversion
compress_bestsourceoriginalre-encode only — near-lossless, keeps the source format
compress_balancedsourceoriginalre-encode only — strong shrink, great quality
compress_maxsourceoriginalre-encode only — aggressive (PNG uses a lossy palette when it helps)
website_webpWebPoriginalquality 80 (web-optimized)
website_avifAVIForiginalquality 60, effort 4 (web-optimized)
jpeg_originalJPEGoriginalquality 80, progressive
png_originalPNGoriginalcompression 6
instagram_squareJPEG1080×1080progressive
instagram_portraitJPEG1080×1350progressive
instagram_storyJPEG1080×1920progressive
linkedinJPEG1200×627progressive
twitterJPEG1200×675progressive
facebook_postJPEG1200×630progressive
pinterest_pinJPEG1000×1500progressive
og_imagePNG1200×630compression 6
faviconfavicon pack (see below)
thumbnailPNG400×400compression 6
email_headerJPEG600×200progressive
web_bannerJPEG1920×480progressive

Fixed-size presets center-crop to the target dimensions; *_original presets keep the source dimensions and just re-encode + strip metadata.

The compress_* presets are the "just make this smaller for the web" path: they keep the source format (JPEG→JPEG, PNG→PNG, WebP→WebP; HEIC→JPEG, SVG→PNG) and the original dimensions, tuning only the encoding across three honest tiers (best / balanced / max savings). PNG savings are modest — it has no quality knob — so only compress_max tries a lossy palette, and only keeps it when it actually comes out smaller.

Favicon pack

The favicon preset is a multi-file output: instead of one image it generates a complete drop-in icon set, bundled under a favicon/ folder in the ZIP:

  • favicon.ico (multi-size 16/32/48, hand-built ICO container)
  • favicon-16x16.png, favicon-32x32.png, favicon-48x48.png
  • apple-touch-icon.png (180×180)
  • android-chrome-192x192.png, android-chrome-512x512.png
  • site.webmanifest and a README.txt with the exact <head><link> snippet

The pack is generated from a center-cropped square master. The multi-file plumbing lives in Preset.Kind / Result.Files (processor/preset.go, processor/favicon_vips.go, processor/ico.go).

Development

Hot-reload for both backend (Air) and frontend (Vite dev server):

docker compose up

Production image

The single self-contained image (Vite build embedded into the Go binary):

docker build -t image-optimizer .
docker run -p 3000:3000 image-optimizer
curl http://localhost:3000/health # -> 200 {"status":"ok"}

The final image contains only the Go binary plus the libvips runtime libs (libvips42) on debian:bookworm-slim, and runs as a non-root user.

Health check

The image ships a Docker HEALTHCHECK that probes GET /health. The binary self-probes via its -healthcheck flag, so no curl/wget is needed in the minimal runtime image:

docker run -d -p 3000:3000 image-optimizer
docker ps # STATUS shows "healthy" once the start period elapses

Graceful shutdown

On SIGINT/SIGTERM the server stops accepting new connections, drains in-flight jobs (so their SSE clients receive a terminal event and the ZIP stays briefly downloadable), tears down libvips, then exits. Each phase is bounded by a 30s timeout so shutdown never hangs.

Job lifetime

Job state lives in memory only — there is no disk temp storage. A job is freed when its ZIP is downloaded, or by a background reaper after JOB_TTL_MINUTES (default 10), whichever comes first. This bounds memory for jobs that are never downloaded.

Environment variables

All configuration is via environment variables, read once at startup (the resolved values are logged). Invalid or non-positive numeric values fall back to the default rather than failing startup.

VariableDefaultDescription
PORT3000TCP port the HTTP server listens on.
MAX_FILE_SIZE_MB50Per-file upload cap. Larger files are rejected with 400.
WORKER_COUNTnumber of CPUsMax concurrent libvips pipelines (the real concurrency limit).
JOB_TTL_MINUTES10How long a job's in-memory state is retained before the reaper frees it.

The whole-request multipart body limit is derived from MAX_FILE_SIZE_MB plus headroom for multiple files and multipart boundaries.

Requirements

  • Go 1.26+ · Node 22+ · Docker
  • libvips ≥ 8.14 (provided inside the Docker images; install locally only if building the Go binary outside Docker)

License

Released into the public domain under The Unlicense — do whatever you want with it.

About

High-performance web image optimizer with multi-target presets (web, Instagram, LinkedIn, etc.) built on Fiber v3 + govips (libvips).

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Image Optimizer

A fast, self-hostable web app that turns one image into every size and format you actually need — web formats, social media presets, and a complete favicon pack — and hands them back as a single ZIP.

Try the live demo at img.hra42.com

Optimize an image into multiple targets, then download a ZIP.

Drop your images in the browser, tick the targets you want, and download the optimized results. No accounts, no uploads kept on disk, no setup beyond running one Docker container.

Why

Preparing images for the web is repetitive: resize for Instagram, crop for an Open Graph card, generate a dozen favicon files, re-encode to WebP/AVIF for performance… usually across several tools. This does all of it in one drag-and-drop step, runs entirely on your own server, and never writes your images to disk.

Features

  • Many targets at once — pick any combination of presets; each image is optimized for every selected target in parallel.
  • Modern + classic formats — outputs WebP, AVIF, JPEG (progressive), and PNG.
  • Wide input support — JPEG, PNG, WebP, AVIF, and iPhone HEIC/HEIF.
  • Drop-in favicon pack — one click produces favicon.ico, all the PNG sizes, apple-touch-icon, site.webmanifest, and a ready-to-paste HTML snippet.
  • Social presets — Instagram, LinkedIn, X, Facebook, Pinterest, Open Graph, plus email/web banners.
  • Live progress — per-target progress streamed over Server-Sent Events.
  • Privacy by design — images are processed in memory only and deleted right after download (or after a short timeout); nothing is stored.
  • Single container — the Svelte UI is embedded into the Go binary, so the whole app is one small Docker image with no external services.

How it works

  1. Add images — drag & drop or browse (JPEG, PNG, WebP, AVIF, HEIC).
  2. Pick targets — choose one or more presets.
  3. Optimize — each image is resized/re-encoded for every target; watch live progress.
  4. Download — get a single ZIP with every optimized variant, named by preset.

Quick start

Run the prebuilt-style image locally (only Docker required):

docker build -t image-optimizer .
docker run -p 3000:3000 image-optimizer

Then open http://localhost:3000 and start dropping images. See Production image for deployment details and Development for the hot-reload setup.

Tech stack

  • Go + Fiber v3 — single compiled binary, low memory footprint
  • govips / libvips — the image processing engine
  • Svelte + Vite — zero-runtime SPA, embedded via //go:embed
  • Single Docker container — Fiber serves both the API and the SPA

Project layout

.
├── main.go # Fiber app, config, graceful shutdown, embeds frontend/dist
├── config/ # env-var configuration (port, limits, workers, job TTL)
├── handlers/ # HTTP handlers: health, upload, progress (SSE), download
├── processor/ # govips pipeline, worker semaphore, presets
├── frontend/ # Svelte + Vite app; dist/ is the go:embed target
├── Dockerfile # 3-stage build: Vite → Go (cgo+libvips) → debian-slim
└── docker-compose.yml # local dev with hot-reload

Note on frontend/dist: a small stub index.html is committed so go build works locally without first running Vite. The Docker frontend stage overwrites it with the real Vite build.

Supported formats & presets

Input: JPEG, PNG, WebP, AVIF, and HEIC/HEIF (iPhone photos). HEIC/HEIF are decoded via libvips' heifload (libheif/libde265, bundled in the Docker images).

The convert_* presets are the "just turn this into a usable file" path: faithful, high-quality format conversion at the original size (no crop). Handy for turning an iPhone HEIC straight into JPEG/PNG/WebP/AVIF without any other tool.

Output presets (registry: processor/preset.go):

PresetFormatDimensionsNotes
convert_jpegJPEGoriginalquality 92, progressive — faithful conversion
convert_pngPNGoriginalcompression 6 (lossless)
convert_webpWebPoriginalquality 90 — faithful conversion
convert_avifAVIForiginalquality 80, effort 4 — faithful conversion
compress_bestsourceoriginalre-encode only — near-lossless, keeps the source format
compress_balancedsourceoriginalre-encode only — strong shrink, great quality
compress_maxsourceoriginalre-encode only — aggressive (PNG uses a lossy palette when it helps)
website_webpWebPoriginalquality 80 (web-optimized)
website_avifAVIForiginalquality 60, effort 4 (web-optimized)
jpeg_originalJPEGoriginalquality 80, progressive
png_originalPNGoriginalcompression 6
instagram_squareJPEG1080×1080progressive
instagram_portraitJPEG1080×1350progressive
instagram_storyJPEG1080×1920progressive
linkedinJPEG1200×627progressive
twitterJPEG1200×675progressive
facebook_postJPEG1200×630progressive
pinterest_pinJPEG1000×1500progressive
og_imagePNG1200×630compression 6
faviconfavicon pack (see below)
thumbnailPNG400×400compression 6
email_headerJPEG600×200progressive
web_bannerJPEG1920×480progressive

Fixed-size presets center-crop to the target dimensions; *_original presets keep the source dimensions and just re-encode + strip metadata.

The compress_* presets are the "just make this smaller for the web" path: they keep the source format (JPEG→JPEG, PNG→PNG, WebP→WebP; HEIC→JPEG, SVG→PNG) and the original dimensions, tuning only the encoding across three honest tiers (best / balanced / max savings). PNG savings are modest — it has no quality knob — so only compress_max tries a lossy palette, and only keeps it when it actually comes out smaller.

Favicon pack

The favicon preset is a multi-file output: instead of one image it generates a complete drop-in icon set, bundled under a favicon/ folder in the ZIP:

  • favicon.ico (multi-size 16/32/48, hand-built ICO container)
  • favicon-16x16.png, favicon-32x32.png, favicon-48x48.png
  • apple-touch-icon.png (180×180)
  • android-chrome-192x192.png, android-chrome-512x512.png
  • site.webmanifest and a README.txt with the exact <head><link> snippet

The pack is generated from a center-cropped square master. The multi-file plumbing lives in Preset.Kind / Result.Files (processor/preset.go, processor/favicon_vips.go, processor/ico.go).

Development

Hot-reload for both backend (Air) and frontend (Vite dev server):

docker compose up

Production image

The single self-contained image (Vite build embedded into the Go binary):

docker build -t image-optimizer .
docker run -p 3000:3000 image-optimizer
curl http://localhost:3000/health # -> 200 {"status":"ok"}

The final image contains only the Go binary plus the libvips runtime libs (libvips42) on debian:bookworm-slim, and runs as a non-root user.

Health check

The image ships a Docker HEALTHCHECK that probes GET /health. The binary self-probes via its -healthcheck flag, so no curl/wget is needed in the minimal runtime image:

docker run -d -p 3000:3000 image-optimizer
docker ps # STATUS shows "healthy" once the start period elapses

Graceful shutdown

On SIGINT/SIGTERM the server stops accepting new connections, drains in-flight jobs (so their SSE clients receive a terminal event and the ZIP stays briefly downloadable), tears down libvips, then exits. Each phase is bounded by a 30s timeout so shutdown never hangs.

Job lifetime

Job state lives in memory only — there is no disk temp storage. A job is freed when its ZIP is downloaded, or by a background reaper after JOB_TTL_MINUTES (default 10), whichever comes first. This bounds memory for jobs that are never downloaded.

Environment variables

All configuration is via environment variables, read once at startup (the resolved values are logged). Invalid or non-positive numeric values fall back to the default rather than failing startup.

VariableDefaultDescription
PORT3000TCP port the HTTP server listens on.
MAX_FILE_SIZE_MB50Per-file upload cap. Larger files are rejected with 400.
WORKER_COUNTnumber of CPUsMax concurrent libvips pipelines (the real concurrency limit).
JOB_TTL_MINUTES10How long a job's in-memory state is retained before the reaper frees it.

The whole-request multipart body limit is derived from MAX_FILE_SIZE_MB plus headroom for multiple files and multipart boundaries.

Requirements

  • Go 1.26+ · Node 22+ · Docker
  • libvips ≥ 8.14 (provided inside the Docker images; install locally only if building the Go binary outside Docker)

License

Released into the public domain under The Unlicense — do whatever you want with it.

About

High-performance web image optimizer with multi-target presets (web, Instagram, LinkedIn, etc.) built on Fiber v3 + govips (libvips).

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Image Optimizer

A fast, self-hostable web app that turns one image into every size and format you actually need — web formats, social media presets, and a complete favicon pack — and hands them back as a single ZIP.

Try the live demo at img.hra42.com

Optimize an image into multiple targets, then download a ZIP.

Drop your images in the browser, tick the targets you want, and download the optimized results. No accounts, no uploads kept on disk, no setup beyond running one Docker container.

Why

Preparing images for the web is repetitive: resize for Instagram, crop for an Open Graph card, generate a dozen favicon files, re-encode to WebP/AVIF for performance… usually across several tools. This does all of it in one drag-and-drop step, runs entirely on your own server, and never writes your images to disk.

Features

  • Many targets at once — pick any combination of presets; each image is optimized for every selected target in parallel.
  • Modern + classic formats — outputs WebP, AVIF, JPEG (progressive), and PNG.
  • Wide input support — JPEG, PNG, WebP, AVIF, and iPhone HEIC/HEIF.
  • Drop-in favicon pack — one click produces favicon.ico, all the PNG sizes, apple-touch-icon, site.webmanifest, and a ready-to-paste HTML snippet.
  • Social presets — Instagram, LinkedIn, X, Facebook, Pinterest, Open Graph, plus email/web banners.
  • Live progress — per-target progress streamed over Server-Sent Events.
  • Privacy by design — images are processed in memory only and deleted right after download (or after a short timeout); nothing is stored.
  • Single container — the Svelte UI is embedded into the Go binary, so the whole app is one small Docker image with no external services.

How it works

  1. Add images — drag & drop or browse (JPEG, PNG, WebP, AVIF, HEIC).
  2. Pick targets — choose one or more presets.
  3. Optimize — each image is resized/re-encoded for every target; watch live progress.
  4. Download — get a single ZIP with every optimized variant, named by preset.

Quick start

Run the prebuilt-style image locally (only Docker required):

docker build -t image-optimizer .
docker run -p 3000:3000 image-optimizer

Then open http://localhost:3000 and start dropping images. See Production image for deployment details and Development for the hot-reload setup.

Tech stack

  • Go + Fiber v3 — single compiled binary, low memory footprint
  • govips / libvips — the image processing engine
  • Svelte + Vite — zero-runtime SPA, embedded via //go:embed
  • Single Docker container — Fiber serves both the API and the SPA

Project layout

.
├── main.go # Fiber app, config, graceful shutdown, embeds frontend/dist
├── config/ # env-var configuration (port, limits, workers, job TTL)
├── handlers/ # HTTP handlers: health, upload, progress (SSE), download
├── processor/ # govips pipeline, worker semaphore, presets
├── frontend/ # Svelte + Vite app; dist/ is the go:embed target
├── Dockerfile # 3-stage build: Vite → Go (cgo+libvips) → debian-slim
└── docker-compose.yml # local dev with hot-reload

Note on frontend/dist: a small stub index.html is committed so go build works locally without first running Vite. The Docker frontend stage overwrites it with the real Vite build.

Supported formats & presets

Input: JPEG, PNG, WebP, AVIF, and HEIC/HEIF (iPhone photos). HEIC/HEIF are decoded via libvips' heifload (libheif/libde265, bundled in the Docker images).

The convert_* presets are the "just turn this into a usable file" path: faithful, high-quality format conversion at the original size (no crop). Handy for turning an iPhone HEIC straight into JPEG/PNG/WebP/AVIF without any other tool.

Output presets (registry: processor/preset.go):

PresetFormatDimensionsNotes
convert_jpegJPEGoriginalquality 92, progressive — faithful conversion
convert_pngPNGoriginalcompression 6 (lossless)
convert_webpWebPoriginalquality 90 — faithful conversion
convert_avifAVIForiginalquality 80, effort 4 — faithful conversion
compress_bestsourceoriginalre-encode only — near-lossless, keeps the source format
compress_balancedsourceoriginalre-encode only — strong shrink, great quality
compress_maxsourceoriginalre-encode only — aggressive (PNG uses a lossy palette when it helps)
website_webpWebPoriginalquality 80 (web-optimized)
website_avifAVIForiginalquality 60, effort 4 (web-optimized)
jpeg_originalJPEGoriginalquality 80, progressive
png_originalPNGoriginalcompression 6
instagram_squareJPEG1080×1080progressive
instagram_portraitJPEG1080×1350progressive
instagram_storyJPEG1080×1920progressive
linkedinJPEG1200×627progressive
twitterJPEG1200×675progressive
facebook_postJPEG1200×630progressive
pinterest_pinJPEG1000×1500progressive
og_imagePNG1200×630compression 6
faviconfavicon pack (see below)
thumbnailPNG400×400compression 6
email_headerJPEG600×200progressive
web_bannerJPEG1920×480progressive

Fixed-size presets center-crop to the target dimensions; *_original presets keep the source dimensions and just re-encode + strip metadata.

The compress_* presets are the "just make this smaller for the web" path: they keep the source format (JPEG→JPEG, PNG→PNG, WebP→WebP; HEIC→JPEG, SVG→PNG) and the original dimensions, tuning only the encoding across three honest tiers (best / balanced / max savings). PNG savings are modest — it has no quality knob — so only compress_max tries a lossy palette, and only keeps it when it actually comes out smaller.

Favicon pack

The favicon preset is a multi-file output: instead of one image it generates a complete drop-in icon set, bundled under a favicon/ folder in the ZIP:

  • favicon.ico (multi-size 16/32/48, hand-built ICO container)
  • favicon-16x16.png, favicon-32x32.png, favicon-48x48.png
  • apple-touch-icon.png (180×180)
  • android-chrome-192x192.png, android-chrome-512x512.png
  • site.webmanifest and a README.txt with the exact <head><link> snippet

The pack is generated from a center-cropped square master. The multi-file plumbing lives in Preset.Kind / Result.Files (processor/preset.go, processor/favicon_vips.go, processor/ico.go).

Development

Hot-reload for both backend (Air) and frontend (Vite dev server):

docker compose up

Production image

The single self-contained image (Vite build embedded into the Go binary):

docker build -t image-optimizer .
docker run -p 3000:3000 image-optimizer
curl http://localhost:3000/health # -> 200 {"status":"ok"}

The final image contains only the Go binary plus the libvips runtime libs (libvips42) on debian:bookworm-slim, and runs as a non-root user.

Health check

The image ships a Docker HEALTHCHECK that probes GET /health. The binary self-probes via its -healthcheck flag, so no curl/wget is needed in the minimal runtime image:

docker run -d -p 3000:3000 image-optimizer
docker ps # STATUS shows "healthy" once the start period elapses

Graceful shutdown

On SIGINT/SIGTERM the server stops accepting new connections, drains in-flight jobs (so their SSE clients receive a terminal event and the ZIP stays briefly downloadable), tears down libvips, then exits. Each phase is bounded by a 30s timeout so shutdown never hangs.

Job lifetime

Job state lives in memory only — there is no disk temp storage. A job is freed when its ZIP is downloaded, or by a background reaper after JOB_TTL_MINUTES (default 10), whichever comes first. This bounds memory for jobs that are never downloaded.

Environment variables

All configuration is via environment variables, read once at startup (the resolved values are logged). Invalid or non-positive numeric values fall back to the default rather than failing startup.

VariableDefaultDescription
PORT3000TCP port the HTTP server listens on.
MAX_FILE_SIZE_MB50Per-file upload cap. Larger files are rejected with 400.
WORKER_COUNTnumber of CPUsMax concurrent libvips pipelines (the real concurrency limit).
JOB_TTL_MINUTES10How long a job's in-memory state is retained before the reaper frees it.

The whole-request multipart body limit is derived from MAX_FILE_SIZE_MB plus headroom for multiple files and multipart boundaries.

Requirements

  • Go 1.26+ · Node 22+ · Docker
  • libvips ≥ 8.14 (provided inside the Docker images; install locally only if building the Go binary outside Docker)

License

Released into the public domain under The Unlicense — do whatever you want with it.

About

High-performance web image optimizer with multi-target presets (web, Instagram, LinkedIn, etc.) built on Fiber v3 + govips (libvips).

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Image Optimizer

A fast, self-hostable web app that turns one image into every size and format you actually need — web formats, social media presets, and a complete favicon pack — and hands them back as a single ZIP.

Try the live demo at img.hra42.com

Optimize an image into multiple targets, then download a ZIP.

Drop your images in the browser, tick the targets you want, and download the optimized results. No accounts, no uploads kept on disk, no setup beyond running one Docker container.

Why

Preparing images for the web is repetitive: resize for Instagram, crop for an Open Graph card, generate a dozen favicon files, re-encode to WebP/AVIF for performance… usually across several tools. This does all of it in one drag-and-drop step, runs entirely on your own server, and never writes your images to disk.

Features

  • Many targets at once — pick any combination of presets; each image is optimized for every selected target in parallel.
  • Modern + classic formats — outputs WebP, AVIF, JPEG (progressive), and PNG.
  • Wide input support — JPEG, PNG, WebP, AVIF, and iPhone HEIC/HEIF.
  • Drop-in favicon pack — one click produces favicon.ico, all the PNG sizes, apple-touch-icon, site.webmanifest, and a ready-to-paste HTML snippet.
  • Social presets — Instagram, LinkedIn, X, Facebook, Pinterest, Open Graph, plus email/web banners.
  • Live progress — per-target progress streamed over Server-Sent Events.
  • Privacy by design — images are processed in memory only and deleted right after download (or after a short timeout); nothing is stored.
  • Single container — the Svelte UI is embedded into the Go binary, so the whole app is one small Docker image with no external services.

How it works

  1. Add images — drag & drop or browse (JPEG, PNG, WebP, AVIF, HEIC).
  2. Pick targets — choose one or more presets.
  3. Optimize — each image is resized/re-encoded for every target; watch live progress.
  4. Download — get a single ZIP with every optimized variant, named by preset.

Quick start

Run the prebuilt-style image locally (only Docker required):

docker build -t image-optimizer .
docker run -p 3000:3000 image-optimizer

Then open http://localhost:3000 and start dropping images. See Production image for deployment details and Development for the hot-reload setup.

Tech stack

  • Go + Fiber v3 — single compiled binary, low memory footprint
  • govips / libvips — the image processing engine
  • Svelte + Vite — zero-runtime SPA, embedded via //go:embed
  • Single Docker container — Fiber serves both the API and the SPA

Project layout

.
├── main.go # Fiber app, config, graceful shutdown, embeds frontend/dist
├── config/ # env-var configuration (port, limits, workers, job TTL)
├── handlers/ # HTTP handlers: health, upload, progress (SSE), download
├── processor/ # govips pipeline, worker semaphore, presets
├── frontend/ # Svelte + Vite app; dist/ is the go:embed target
├── Dockerfile # 3-stage build: Vite → Go (cgo+libvips) → debian-slim
└── docker-compose.yml # local dev with hot-reload

Note on frontend/dist: a small stub index.html is committed so go build works locally without first running Vite. The Docker frontend stage overwrites it with the real Vite build.

Supported formats & presets

Input: JPEG, PNG, WebP, AVIF, and HEIC/HEIF (iPhone photos). HEIC/HEIF are decoded via libvips' heifload (libheif/libde265, bundled in the Docker images).

The convert_* presets are the "just turn this into a usable file" path: faithful, high-quality format conversion at the original size (no crop). Handy for turning an iPhone HEIC straight into JPEG/PNG/WebP/AVIF without any other tool.

Output presets (registry: processor/preset.go):

PresetFormatDimensionsNotes
convert_jpegJPEGoriginalquality 92, progressive — faithful conversion
convert_pngPNGoriginalcompression 6 (lossless)
convert_webpWebPoriginalquality 90 — faithful conversion
convert_avifAVIForiginalquality 80, effort 4 — faithful conversion
compress_bestsourceoriginalre-encode only — near-lossless, keeps the source format
compress_balancedsourceoriginalre-encode only — strong shrink, great quality
compress_maxsourceoriginalre-encode only — aggressive (PNG uses a lossy palette when it helps)
website_webpWebPoriginalquality 80 (web-optimized)
website_avifAVIForiginalquality 60, effort 4 (web-optimized)
jpeg_originalJPEGoriginalquality 80, progressive
png_originalPNGoriginalcompression 6
instagram_squareJPEG1080×1080progressive
instagram_portraitJPEG1080×1350progressive
instagram_storyJPEG1080×1920progressive
linkedinJPEG1200×627progressive
twitterJPEG1200×675progressive
facebook_postJPEG1200×630progressive
pinterest_pinJPEG1000×1500progressive
og_imagePNG1200×630compression 6
faviconfavicon pack (see below)
thumbnailPNG400×400compression 6
email_headerJPEG600×200progressive
web_bannerJPEG1920×480progressive

Fixed-size presets center-crop to the target dimensions; *_original presets keep the source dimensions and just re-encode + strip metadata.

The compress_* presets are the "just make this smaller for the web" path: they keep the source format (JPEG→JPEG, PNG→PNG, WebP→WebP; HEIC→JPEG, SVG→PNG) and the original dimensions, tuning only the encoding across three honest tiers (best / balanced / max savings). PNG savings are modest — it has no quality knob — so only compress_max tries a lossy palette, and only keeps it when it actually comes out smaller.

Favicon pack

The favicon preset is a multi-file output: instead of one image it generates a complete drop-in icon set, bundled under a favicon/ folder in the ZIP:

  • favicon.ico (multi-size 16/32/48, hand-built ICO container)
  • favicon-16x16.png, favicon-32x32.png, favicon-48x48.png
  • apple-touch-icon.png (180×180)
  • android-chrome-192x192.png, android-chrome-512x512.png
  • site.webmanifest and a README.txt with the exact <head><link> snippet

The pack is generated from a center-cropped square master. The multi-file plumbing lives in Preset.Kind / Result.Files (processor/preset.go, processor/favicon_vips.go, processor/ico.go).

Development

Hot-reload for both backend (Air) and frontend (Vite dev server):

docker compose up

Production image

The single self-contained image (Vite build embedded into the Go binary):

docker build -t image-optimizer .
docker run -p 3000:3000 image-optimizer
curl http://localhost:3000/health # -> 200 {"status":"ok"}

The final image contains only the Go binary plus the libvips runtime libs (libvips42) on debian:bookworm-slim, and runs as a non-root user.

Health check

The image ships a Docker HEALTHCHECK that probes GET /health. The binary self-probes via its -healthcheck flag, so no curl/wget is needed in the minimal runtime image:

docker run -d -p 3000:3000 image-optimizer
docker ps # STATUS shows "healthy" once the start period elapses

Graceful shutdown

On SIGINT/SIGTERM the server stops accepting new connections, drains in-flight jobs (so their SSE clients receive a terminal event and the ZIP stays briefly downloadable), tears down libvips, then exits. Each phase is bounded by a 30s timeout so shutdown never hangs.

Job lifetime

Job state lives in memory only — there is no disk temp storage. A job is freed when its ZIP is downloaded, or by a background reaper after JOB_TTL_MINUTES (default 10), whichever comes first. This bounds memory for jobs that are never downloaded.

Environment variables

All configuration is via environment variables, read once at startup (the resolved values are logged). Invalid or non-positive numeric values fall back to the default rather than failing startup.

VariableDefaultDescription
PORT3000TCP port the HTTP server listens on.
MAX_FILE_SIZE_MB50Per-file upload cap. Larger files are rejected with 400.
WORKER_COUNTnumber of CPUsMax concurrent libvips pipelines (the real concurrency limit).
JOB_TTL_MINUTES10How long a job's in-memory state is retained before the reaper frees it.

The whole-request multipart body limit is derived from MAX_FILE_SIZE_MB plus headroom for multiple files and multipart boundaries.

Requirements

  • Go 1.26+ · Node 22+ · Docker
  • libvips ≥ 8.14 (provided inside the Docker images; install locally only if building the Go binary outside Docker)

License

Released into the public domain under The Unlicense — do whatever you want with it.

About

High-performance web image optimizer with multi-target presets (web, Instagram, LinkedIn, etc.) built on Fiber v3 + govips (libvips).

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Image Optimizer

A fast, self-hostable web app that turns one image into every size and format you actually need — web formats, social media presets, and a complete favicon pack — and hands them back as a single ZIP.

Try the live demo at img.hra42.com

Optimize an image into multiple targets, then download a ZIP.

Drop your images in the browser, tick the targets you want, and download the optimized results. No accounts, no uploads kept on disk, no setup beyond running one Docker container.

Why

Preparing images for the web is repetitive: resize for Instagram, crop for an Open Graph card, generate a dozen favicon files, re-encode to WebP/AVIF for performance… usually across several tools. This does all of it in one drag-and-drop step, runs entirely on your own server, and never writes your images to disk.

Features

  • Many targets at once — pick any combination of presets; each image is optimized for every selected target in parallel.
  • Modern + classic formats — outputs WebP, AVIF, JPEG (progressive), and PNG.
  • Wide input support — JPEG, PNG, WebP, AVIF, and iPhone HEIC/HEIF.
  • Drop-in favicon pack — one click produces favicon.ico, all the PNG sizes, apple-touch-icon, site.webmanifest, and a ready-to-paste HTML snippet.
  • Social presets — Instagram, LinkedIn, X, Facebook, Pinterest, Open Graph, plus email/web banners.
  • Live progress — per-target progress streamed over Server-Sent Events.
  • Privacy by design — images are processed in memory only and deleted right after download (or after a short timeout); nothing is stored.
  • Single container — the Svelte UI is embedded into the Go binary, so the whole app is one small Docker image with no external services.

How it works

  1. Add images — drag & drop or browse (JPEG, PNG, WebP, AVIF, HEIC).
  2. Pick targets — choose one or more presets.
  3. Optimize — each image is resized/re-encoded for every target; watch live progress.
  4. Download — get a single ZIP with every optimized variant, named by preset.

Quick start

Run the prebuilt-style image locally (only Docker required):

docker build -t image-optimizer .
docker run -p 3000:3000 image-optimizer

Then open http://localhost:3000 and start dropping images. See Production image for deployment details and Development for the hot-reload setup.

Tech stack

  • Go + Fiber v3 — single compiled binary, low memory footprint
  • govips / libvips — the image processing engine
  • Svelte + Vite — zero-runtime SPA, embedded via //go:embed
  • Single Docker container — Fiber serves both the API and the SPA

Project layout

.
├── main.go # Fiber app, config, graceful shutdown, embeds frontend/dist
├── config/ # env-var configuration (port, limits, workers, job TTL)
├── handlers/ # HTTP handlers: health, upload, progress (SSE), download
├── processor/ # govips pipeline, worker semaphore, presets
├── frontend/ # Svelte + Vite app; dist/ is the go:embed target
├── Dockerfile # 3-stage build: Vite → Go (cgo+libvips) → debian-slim
└── docker-compose.yml # local dev with hot-reload

Note on frontend/dist: a small stub index.html is committed so go build works locally without first running Vite. The Docker frontend stage overwrites it with the real Vite build.

Supported formats & presets

Input: JPEG, PNG, WebP, AVIF, and HEIC/HEIF (iPhone photos). HEIC/HEIF are decoded via libvips' heifload (libheif/libde265, bundled in the Docker images).

The convert_* presets are the "just turn this into a usable file" path: faithful, high-quality format conversion at the original size (no crop). Handy for turning an iPhone HEIC straight into JPEG/PNG/WebP/AVIF without any other tool.

Output presets (registry: processor/preset.go):

PresetFormatDimensionsNotes
convert_jpegJPEGoriginalquality 92, progressive — faithful conversion
convert_pngPNGoriginalcompression 6 (lossless)
convert_webpWebPoriginalquality 90 — faithful conversion
convert_avifAVIForiginalquality 80, effort 4 — faithful conversion
compress_bestsourceoriginalre-encode only — near-lossless, keeps the source format
compress_balancedsourceoriginalre-encode only — strong shrink, great quality
compress_maxsourceoriginalre-encode only — aggressive (PNG uses a lossy palette when it helps)
website_webpWebPoriginalquality 80 (web-optimized)
website_avifAVIForiginalquality 60, effort 4 (web-optimized)
jpeg_originalJPEGoriginalquality 80, progressive
png_originalPNGoriginalcompression 6
instagram_squareJPEG1080×1080progressive
instagram_portraitJPEG1080×1350progressive
instagram_storyJPEG1080×1920progressive
linkedinJPEG1200×627progressive
twitterJPEG1200×675progressive
facebook_postJPEG1200×630progressive
pinterest_pinJPEG1000×1500progressive
og_imagePNG1200×630compression 6
faviconfavicon pack (see below)
thumbnailPNG400×400compression 6
email_headerJPEG600×200progressive
web_bannerJPEG1920×480progressive

Fixed-size presets center-crop to the target dimensions; *_original presets keep the source dimensions and just re-encode + strip metadata.

The compress_* presets are the "just make this smaller for the web" path: they keep the source format (JPEG→JPEG, PNG→PNG, WebP→WebP; HEIC→JPEG, SVG→PNG) and the original dimensions, tuning only the encoding across three honest tiers (best / balanced / max savings). PNG savings are modest — it has no quality knob — so only compress_max tries a lossy palette, and only keeps it when it actually comes out smaller.

Favicon pack

The favicon preset is a multi-file output: instead of one image it generates a complete drop-in icon set, bundled under a favicon/ folder in the ZIP:

  • favicon.ico (multi-size 16/32/48, hand-built ICO container)
  • favicon-16x16.png, favicon-32x32.png, favicon-48x48.png
  • apple-touch-icon.png (180×180)
  • android-chrome-192x192.png, android-chrome-512x512.png
  • site.webmanifest and a README.txt with the exact <head><link> snippet

The pack is generated from a center-cropped square master. The multi-file plumbing lives in Preset.Kind / Result.Files (processor/preset.go, processor/favicon_vips.go, processor/ico.go).

Development

Hot-reload for both backend (Air) and frontend (Vite dev server):

docker compose up

Production image

The single self-contained image (Vite build embedded into the Go binary):

docker build -t image-optimizer .
docker run -p 3000:3000 image-optimizer
curl http://localhost:3000/health # -> 200 {"status":"ok"}

The final image contains only the Go binary plus the libvips runtime libs (libvips42) on debian:bookworm-slim, and runs as a non-root user.

Health check

The image ships a Docker HEALTHCHECK that probes GET /health. The binary self-probes via its -healthcheck flag, so no curl/wget is needed in the minimal runtime image:

docker run -d -p 3000:3000 image-optimizer
docker ps # STATUS shows "healthy" once the start period elapses

Graceful shutdown

On SIGINT/SIGTERM the server stops accepting new connections, drains in-flight jobs (so their SSE clients receive a terminal event and the ZIP stays briefly downloadable), tears down libvips, then exits. Each phase is bounded by a 30s timeout so shutdown never hangs.

Job lifetime

Job state lives in memory only — there is no disk temp storage. A job is freed when its ZIP is downloaded, or by a background reaper after JOB_TTL_MINUTES (default 10), whichever comes first. This bounds memory for jobs that are never downloaded.

Environment variables

All configuration is via environment variables, read once at startup (the resolved values are logged). Invalid or non-positive numeric values fall back to the default rather than failing startup.

VariableDefaultDescription
PORT3000TCP port the HTTP server listens on.
MAX_FILE_SIZE_MB50Per-file upload cap. Larger files are rejected with 400.
WORKER_COUNTnumber of CPUsMax concurrent libvips pipelines (the real concurrency limit).
JOB_TTL_MINUTES10How long a job's in-memory state is retained before the reaper frees it.

The whole-request multipart body limit is derived from MAX_FILE_SIZE_MB plus headroom for multiple files and multipart boundaries.

Requirements

  • Go 1.26+ · Node 22+ · Docker
  • libvips ≥ 8.14 (provided inside the Docker images; install locally only if building the Go binary outside Docker)

License

Released into the public domain under The Unlicense — do whatever you want with it.

About

High-performance web image optimizer with multi-target presets (web, Instagram, LinkedIn, etc.) built on Fiber v3 + govips (libvips).

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Image Optimizer

A fast, self-hostable web app that turns one image into every size and format you actually need — web formats, social media presets, and a complete favicon pack — and hands them back as a single ZIP.

Try the live demo at img.hra42.com

Optimize an image into multiple targets, then download a ZIP.

Drop your images in the browser, tick the targets you want, and download the optimized results. No accounts, no uploads kept on disk, no setup beyond running one Docker container.

Why

Preparing images for the web is repetitive: resize for Instagram, crop for an Open Graph card, generate a dozen favicon files, re-encode to WebP/AVIF for performance… usually across several tools. This does all of it in one drag-and-drop step, runs entirely on your own server, and never writes your images to disk.

Features

  • Many targets at once — pick any combination of presets; each image is optimized for every selected target in parallel.
  • Modern + classic formats — outputs WebP, AVIF, JPEG (progressive), and PNG.
  • Wide input support — JPEG, PNG, WebP, AVIF, and iPhone HEIC/HEIF.
  • Drop-in favicon pack — one click produces favicon.ico, all the PNG sizes, apple-touch-icon, site.webmanifest, and a ready-to-paste HTML snippet.
  • Social presets — Instagram, LinkedIn, X, Facebook, Pinterest, Open Graph, plus email/web banners.
  • Live progress — per-target progress streamed over Server-Sent Events.
  • Privacy by design — images are processed in memory only and deleted right after download (or after a short timeout); nothing is stored.
  • Single container — the Svelte UI is embedded into the Go binary, so the whole app is one small Docker image with no external services.

How it works

  1. Add images — drag & drop or browse (JPEG, PNG, WebP, AVIF, HEIC).
  2. Pick targets — choose one or more presets.
  3. Optimize — each image is resized/re-encoded for every target; watch live progress.
  4. Download — get a single ZIP with every optimized variant, named by preset.

Quick start

Run the prebuilt-style image locally (only Docker required):

docker build -t image-optimizer .
docker run -p 3000:3000 image-optimizer

Then open http://localhost:3000 and start dropping images. See Production image for deployment details and Development for the hot-reload setup.

Tech stack

  • Go + Fiber v3 — single compiled binary, low memory footprint
  • govips / libvips — the image processing engine
  • Svelte + Vite — zero-runtime SPA, embedded via //go:embed
  • Single Docker container — Fiber serves both the API and the SPA

Project layout

.
├── main.go # Fiber app, config, graceful shutdown, embeds frontend/dist
├── config/ # env-var configuration (port, limits, workers, job TTL)
├── handlers/ # HTTP handlers: health, upload, progress (SSE), download
├── processor/ # govips pipeline, worker semaphore, presets
├── frontend/ # Svelte + Vite app; dist/ is the go:embed target
├── Dockerfile # 3-stage build: Vite → Go (cgo+libvips) → debian-slim
└── docker-compose.yml # local dev with hot-reload

Note on frontend/dist: a small stub index.html is committed so go build works locally without first running Vite. The Docker frontend stage overwrites it with the real Vite build.

Supported formats & presets

Input: JPEG, PNG, WebP, AVIF, and HEIC/HEIF (iPhone photos). HEIC/HEIF are decoded via libvips' heifload (libheif/libde265, bundled in the Docker images).

The convert_* presets are the "just turn this into a usable file" path: faithful, high-quality format conversion at the original size (no crop). Handy for turning an iPhone HEIC straight into JPEG/PNG/WebP/AVIF without any other tool.

Output presets (registry: processor/preset.go):

PresetFormatDimensionsNotes
convert_jpegJPEGoriginalquality 92, progressive — faithful conversion
convert_pngPNGoriginalcompression 6 (lossless)
convert_webpWebPoriginalquality 90 — faithful conversion
convert_avifAVIForiginalquality 80, effort 4 — faithful conversion
compress_bestsourceoriginalre-encode only — near-lossless, keeps the source format
compress_balancedsourceoriginalre-encode only — strong shrink, great quality
compress_maxsourceoriginalre-encode only — aggressive (PNG uses a lossy palette when it helps)
website_webpWebPoriginalquality 80 (web-optimized)
website_avifAVIForiginalquality 60, effort 4 (web-optimized)
jpeg_originalJPEGoriginalquality 80, progressive
png_originalPNGoriginalcompression 6
instagram_squareJPEG1080×1080progressive
instagram_portraitJPEG1080×1350progressive
instagram_storyJPEG1080×1920progressive
linkedinJPEG1200×627progressive
twitterJPEG1200×675progressive
facebook_postJPEG1200×630progressive
pinterest_pinJPEG1000×1500progressive
og_imagePNG1200×630compression 6
faviconfavicon pack (see below)
thumbnailPNG400×400compression 6
email_headerJPEG600×200progressive
web_bannerJPEG1920×480progressive

Fixed-size presets center-crop to the target dimensions; *_original presets keep the source dimensions and just re-encode + strip metadata.

The compress_* presets are the "just make this smaller for the web" path: they keep the source format (JPEG→JPEG, PNG→PNG, WebP→WebP; HEIC→JPEG, SVG→PNG) and the original dimensions, tuning only the encoding across three honest tiers (best / balanced / max savings). PNG savings are modest — it has no quality knob — so only compress_max tries a lossy palette, and only keeps it when it actually comes out smaller.

Favicon pack

The favicon preset is a multi-file output: instead of one image it generates a complete drop-in icon set, bundled under a favicon/ folder in the ZIP:

  • favicon.ico (multi-size 16/32/48, hand-built ICO container)
  • favicon-16x16.png, favicon-32x32.png, favicon-48x48.png
  • apple-touch-icon.png (180×180)
  • android-chrome-192x192.png, android-chrome-512x512.png
  • site.webmanifest and a README.txt with the exact <head><link> snippet

The pack is generated from a center-cropped square master. The multi-file plumbing lives in Preset.Kind / Result.Files (processor/preset.go, processor/favicon_vips.go, processor/ico.go).

Development

Hot-reload for both backend (Air) and frontend (Vite dev server):

docker compose up

Production image

The single self-contained image (Vite build embedded into the Go binary):

docker build -t image-optimizer .
docker run -p 3000:3000 image-optimizer
curl http://localhost:3000/health # -> 200 {"status":"ok"}

The final image contains only the Go binary plus the libvips runtime libs (libvips42) on debian:bookworm-slim, and runs as a non-root user.

Health check

The image ships a Docker HEALTHCHECK that probes GET /health. The binary self-probes via its -healthcheck flag, so no curl/wget is needed in the minimal runtime image:

docker run -d -p 3000:3000 image-optimizer
docker ps # STATUS shows "healthy" once the start period elapses

Graceful shutdown

On SIGINT/SIGTERM the server stops accepting new connections, drains in-flight jobs (so their SSE clients receive a terminal event and the ZIP stays briefly downloadable), tears down libvips, then exits. Each phase is bounded by a 30s timeout so shutdown never hangs.

Job lifetime

Job state lives in memory only — there is no disk temp storage. A job is freed when its ZIP is downloaded, or by a background reaper after JOB_TTL_MINUTES (default 10), whichever comes first. This bounds memory for jobs that are never downloaded.

Environment variables

All configuration is via environment variables, read once at startup (the resolved values are logged). Invalid or non-positive numeric values fall back to the default rather than failing startup.

VariableDefaultDescription
PORT3000TCP port the HTTP server listens on.
MAX_FILE_SIZE_MB50Per-file upload cap. Larger files are rejected with 400.
WORKER_COUNTnumber of CPUsMax concurrent libvips pipelines (the real concurrency limit).
JOB_TTL_MINUTES10How long a job's in-memory state is retained before the reaper frees it.

The whole-request multipart body limit is derived from MAX_FILE_SIZE_MB plus headroom for multiple files and multipart boundaries.

Requirements

  • Go 1.26+ · Node 22+ · Docker
  • libvips ≥ 8.14 (provided inside the Docker images; install locally only if building the Go binary outside Docker)

License

Released into the public domain under The Unlicense — do whatever you want with it.

About

High-performance web image optimizer with multi-target presets (web, Instagram, LinkedIn, etc.) built on Fiber v3 + govips (libvips).

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Image Optimizer

A fast, self-hostable web app that turns one image into every size and format you actually need — web formats, social media presets, and a complete favicon pack — and hands them back as a single ZIP.

Try the live demo at img.hra42.com

Optimize an image into multiple targets, then download a ZIP.

Drop your images in the browser, tick the targets you want, and download the optimized results. No accounts, no uploads kept on disk, no setup beyond running one Docker container.

Why

Preparing images for the web is repetitive: resize for Instagram, crop for an Open Graph card, generate a dozen favicon files, re-encode to WebP/AVIF for performance… usually across several tools. This does all of it in one drag-and-drop step, runs entirely on your own server, and never writes your images to disk.

Features

  • Many targets at once — pick any combination of presets; each image is optimized for every selected target in parallel.
  • Modern + classic formats — outputs WebP, AVIF, JPEG (progressive), and PNG.
  • Wide input support — JPEG, PNG, WebP, AVIF, and iPhone HEIC/HEIF.
  • Drop-in favicon pack — one click produces favicon.ico, all the PNG sizes, apple-touch-icon, site.webmanifest, and a ready-to-paste HTML snippet.
  • Social presets — Instagram, LinkedIn, X, Facebook, Pinterest, Open Graph, plus email/web banners.
  • Live progress — per-target progress streamed over Server-Sent Events.
  • Privacy by design — images are processed in memory only and deleted right after download (or after a short timeout); nothing is stored.
  • Single container — the Svelte UI is embedded into the Go binary, so the whole app is one small Docker image with no external services.

How it works

  1. Add images — drag & drop or browse (JPEG, PNG, WebP, AVIF, HEIC).
  2. Pick targets — choose one or more presets.
  3. Optimize — each image is resized/re-encoded for every target; watch live progress.
  4. Download — get a single ZIP with every optimized variant, named by preset.

Quick start

Run the prebuilt-style image locally (only Docker required):

docker build -t image-optimizer .
docker run -p 3000:3000 image-optimizer

Then open http://localhost:3000 and start dropping images. See Production image for deployment details and Development for the hot-reload setup.

Tech stack

  • Go + Fiber v3 — single compiled binary, low memory footprint
  • govips / libvips — the image processing engine
  • Svelte + Vite — zero-runtime SPA, embedded via //go:embed
  • Single Docker container — Fiber serves both the API and the SPA

Project layout

.
├── main.go # Fiber app, config, graceful shutdown, embeds frontend/dist
├── config/ # env-var configuration (port, limits, workers, job TTL)
├── handlers/ # HTTP handlers: health, upload, progress (SSE), download
├── processor/ # govips pipeline, worker semaphore, presets
├── frontend/ # Svelte + Vite app; dist/ is the go:embed target
├── Dockerfile # 3-stage build: Vite → Go (cgo+libvips) → debian-slim
└── docker-compose.yml # local dev with hot-reload

Note on frontend/dist: a small stub index.html is committed so go build works locally without first running Vite. The Docker frontend stage overwrites it with the real Vite build.

Supported formats & presets

Input: JPEG, PNG, WebP, AVIF, and HEIC/HEIF (iPhone photos). HEIC/HEIF are decoded via libvips' heifload (libheif/libde265, bundled in the Docker images).

The convert_* presets are the "just turn this into a usable file" path: faithful, high-quality format conversion at the original size (no crop). Handy for turning an iPhone HEIC straight into JPEG/PNG/WebP/AVIF without any other tool.

Output presets (registry: processor/preset.go):

PresetFormatDimensionsNotes
convert_jpegJPEGoriginalquality 92, progressive — faithful conversion
convert_pngPNGoriginalcompression 6 (lossless)
convert_webpWebPoriginalquality 90 — faithful conversion
convert_avifAVIForiginalquality 80, effort 4 — faithful conversion
compress_bestsourceoriginalre-encode only — near-lossless, keeps the source format
compress_balancedsourceoriginalre-encode only — strong shrink, great quality
compress_maxsourceoriginalre-encode only — aggressive (PNG uses a lossy palette when it helps)
website_webpWebPoriginalquality 80 (web-optimized)
website_avifAVIForiginalquality 60, effort 4 (web-optimized)
jpeg_originalJPEGoriginalquality 80, progressive
png_originalPNGoriginalcompression 6
instagram_squareJPEG1080×1080progressive
instagram_portraitJPEG1080×1350progressive
instagram_storyJPEG1080×1920progressive
linkedinJPEG1200×627progressive
twitterJPEG1200×675progressive
facebook_postJPEG1200×630progressive
pinterest_pinJPEG1000×1500progressive
og_imagePNG1200×630compression 6
faviconfavicon pack (see below)
thumbnailPNG400×400compression 6
email_headerJPEG600×200progressive
web_bannerJPEG1920×480progressive

Fixed-size presets center-crop to the target dimensions; *_original presets keep the source dimensions and just re-encode + strip metadata.

The compress_* presets are the "just make this smaller for the web" path: they keep the source format (JPEG→JPEG, PNG→PNG, WebP→WebP; HEIC→JPEG, SVG→PNG) and the original dimensions, tuning only the encoding across three honest tiers (best / balanced / max savings). PNG savings are modest — it has no quality knob — so only compress_max tries a lossy palette, and only keeps it when it actually comes out smaller.

Favicon pack

The favicon preset is a multi-file output: instead of one image it generates a complete drop-in icon set, bundled under a favicon/ folder in the ZIP:

  • favicon.ico (multi-size 16/32/48, hand-built ICO container)
  • favicon-16x16.png, favicon-32x32.png, favicon-48x48.png
  • apple-touch-icon.png (180×180)
  • android-chrome-192x192.png, android-chrome-512x512.png
  • site.webmanifest and a README.txt with the exact <head><link> snippet

The pack is generated from a center-cropped square master. The multi-file plumbing lives in Preset.Kind / Result.Files (processor/preset.go, processor/favicon_vips.go, processor/ico.go).

Development

Hot-reload for both backend (Air) and frontend (Vite dev server):

docker compose up

Production image

The single self-contained image (Vite build embedded into the Go binary):

docker build -t image-optimizer .
docker run -p 3000:3000 image-optimizer
curl http://localhost:3000/health # -> 200 {"status":"ok"}

The final image contains only the Go binary plus the libvips runtime libs (libvips42) on debian:bookworm-slim, and runs as a non-root user.

Health check

The image ships a Docker HEALTHCHECK that probes GET /health. The binary self-probes via its -healthcheck flag, so no curl/wget is needed in the minimal runtime image:

docker run -d -p 3000:3000 image-optimizer
docker ps # STATUS shows "healthy" once the start period elapses

Graceful shutdown

On SIGINT/SIGTERM the server stops accepting new connections, drains in-flight jobs (so their SSE clients receive a terminal event and the ZIP stays briefly downloadable), tears down libvips, then exits. Each phase is bounded by a 30s timeout so shutdown never hangs.

Job lifetime

Job state lives in memory only — there is no disk temp storage. A job is freed when its ZIP is downloaded, or by a background reaper after JOB_TTL_MINUTES (default 10), whichever comes first. This bounds memory for jobs that are never downloaded.

Environment variables

All configuration is via environment variables, read once at startup (the resolved values are logged). Invalid or non-positive numeric values fall back to the default rather than failing startup.

VariableDefaultDescription
PORT3000TCP port the HTTP server listens on.
MAX_FILE_SIZE_MB50Per-file upload cap. Larger files are rejected with 400.
WORKER_COUNTnumber of CPUsMax concurrent libvips pipelines (the real concurrency limit).
JOB_TTL_MINUTES10How long a job's in-memory state is retained before the reaper frees it.

The whole-request multipart body limit is derived from MAX_FILE_SIZE_MB plus headroom for multiple files and multipart boundaries.

Requirements

  • Go 1.26+ · Node 22+ · Docker
  • libvips ≥ 8.14 (provided inside the Docker images; install locally only if building the Go binary outside Docker)

License

Released into the public domain under The Unlicense — do whatever you want with it.

About

High-performance web image optimizer with multi-target presets (web, Instagram, LinkedIn, etc.) built on Fiber v3 + govips (libvips).

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages