WebP Server Go and the three folders it needs
Go version of WebP Server. A tool that will serve your JPG/PNG/BMP/SVGs as WebP/AVIF format with compression, on-the-fly.
At a glance
- What is it?
- WebP Server Go is a Go process that sits in front of an existing image folder and answers requests for .jpg and .png with WebP or AVIF bytes, caching the result on disk. It trades a build step for a running service, and the documentation shows how thin that surface is.
- Who is it for?
- WebP Server Go fits teams that already serve images from disk or from an application backend and cannot rewrite every template: it converts on the fly, keeps the URL and needs no build step. It does not fit a static site with no origin process to sit in front of, or anyone who wants upload-time conversion instead of request-time work.
- Can I use it commercially?
- Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The URL never changes, only the payload does
Put this process in front of an existing image path and your markup does not have to move. A client requests `https://your.website/pics/tsuki.jpg` and receives `image/webp` or `image/avif` in reply, with the path untouched, which is the whole design: templates, browser caches and any CDN you already run keep working, because every reference still names the original JPEG. The README names eight input formats, from JPEG and PNG through BMP, GIF, SVG, HEIC, NEF and WEBP. Two of them sit outside the normal image pipeline. NEF, a camera raw format, goes through a separate parser, the rawparser module that the dependency file pins. One failure mode is stated without hedging: a GIF is never handed back as AVIF, because the converted result is not animated, so an animated GIF has to be negotiated for by the client instead of silently flattened into a still frame.
pics, exhaust and metadata each hold one thing
The container mounts three host paths and keeps them strictly separate. Source images sit under one, converted output under a second, and the source metadata used for cache validation under a third, so clearing the cache never touches your originals and rebuilding it never rewrites them. Inside, these become `/opt/pics`, `/opt/exhaust` and `/opt/metadata`, while the host side is whatever you type into the compose file. The published port, 3333, is deliberately narrow: `127.0.0.1:3333:3333` binds to the host loopback only, so nothing on the network reaches the service until a reverse proxy stands in front of it. The documented next step is letting Nginx forward with `proxy_pass http://127.0.0.1:3333/;`, which keeps TLS, host filtering and access rules inside the web server you already run. One consequence of splitting cache from source: the image declares `/opt/exhaust` as a volume, so everything converted lands outside the container filesystem layer and outlives a rebuild only when a host directory is mounted over it.
docker-compose up -d and the first request
Installation is a compose file and one command: three mounts, one image, one published port.
version: '3'
services:
webp:
image: webpsh/webp-server-go
# image: ghcr.io/webp-sh/webp_server_go
restart: always
volumes:
- ./path/to/pics:/opt/pics
- ./exhaust:/opt/exhaust
- ./metadata:/opt/metadata
ports:
- 127.0.0.1:3333:3333The image name is the short one on Docker Hub, and the registry alternative sits commented out on the line below it, so switching registries means uncommenting a line rather than rewriting the file. Replace `./path/to/pics` with the directory that already holds your images: for the worked example, a site serving `/var/www/img.webp.sh/path/tsuki.jpg` at `https://img.webp.sh/path/tsuki.jpg` mounts `/var/www/img.webp.sh` instead. Then start it.
docker-compose up -dAfter that, request `http://127.0.0.1:3333/path/tsuki.jpg` and you should receive the converted form of the file sitting on disk, once the reverse proxy is in front of it. What the repository gives you is no throughput data at all: nothing in the compose file sets a worker count, a concurrency limit or a memory ceiling, and no figure for images converted per second is published anywhere in it.
The image build rewrites config.json with sed
Anything you want to change lives in a config file you mount rather than in flags at start-up. Generate one using the configuration guide the README links, then add a single extra mount to the compose file.
version: '3'
services:
webp:
image: webpsh/webp-server-go
# image: ghcr.io/webp-sh/webp_server_go
restart: always
volumes:
- ./path/to/pics:/opt/pics
- ./path/to/exhaust:/opt/exhaust
- ./path/to/metadata:/opt/metadata
- ./config.json:/etc/config.json
ports:
- 127.0.0.1:3333:3333That fourth volume is the entire customisation story, and the repository ships a `config.json` of its own without printing one of its keys, which is the clearest sign that configuration is treated as a documentation project of its own. What the Dockerfile does expose is how the defaults get replaced. Three sed passes rewrite the checked-in file before compiling: the source path becomes the `IMG_PATH` build argument, an empty default becomes `EXHAUST_PATH`, and the address 127.0.0.1 is rewritten to 0.0.0.0. That last substitution explains the split between inside and outside, since the process listens on every interface in its container while the compose port mapping keeps it on loopback. The final stage is a slim Debian base carrying `libvips`, `libjemalloc2`, `libtcmalloc-minimal4`, `curl` and the libheif `aomenc` and `aomdec` plugins, with the compiled binary copied to `/usr/bin/webp-server` and the mounted `/etc/config.json` handed to it at start-up.
The format list and the output list disagree
Read the two format lists against each other and they do not agree. The repository's one-line summary offers to serve JPG, PNG, BMP and SVGs, which is 4 inputs, while the README's own line names 8: JPEG, PNG, BMP, GIF, SVG, HEIC, NEF and WEBP. Three of those eight, GIF, HEIC and NEF, never appear in the shorter summary. Then take the request-flow diagram embedded above that section, whose closing node is labelled as returning an optimized image in webp, avif or jxl, so 3 output formats are advertised rather than 2. The jxl string appears nowhere else in the repository: not among the module dependencies, not in the slim runtime image, not in the compose example. No JPEG XL package is pulled in either, and encoding happens through `libvips` plus the two libheif plugins. So either the diagram was drawn ahead of the implementation, or a third output is planned and unwritten; nothing on record settles it. For a capacity plan, count on WebP and AVIF and treat jxl as unaccounted for.
The binary route warns about glibc first
The README steers almost everyone to Docker and gives one reason: running the binary directly may hit `glibc` and dependency library problems, described as tricky to resolve, and the advanced section repeats it as a spoiler alert. The build files show the mechanism. The compiled program links to `libvips` for image work and to libheif plugins for HEIC input and AVIF encoding, which are shared libraries resolved at run time rather than code frozen into the executable, so a different distribution or a different libvips build breaks resolution in ways the build machine never sees. The Makefile derives its target from `uname` and writes `builds/webp-server-$(OS)-$(ARCH)`, defaulting to amd64 unless the machine reports arm64, so the default output is host-native. Two details are easy to miss there. The docker target tags the image `webpsh/webps`, a different name from the one the compose file pulls, so a local build does not feed that setup. And `malloc_tests` sits at the top of the tree while the runtime image installs both jemalloc and tcmalloc, yet nothing in the repository states which allocator the server selects. The lint target pins golangci-lint at v1.59.1 while staticcheck is installed at latest, an unevenness worth noting. The code is published under GPL-3.0 and the image carries a binary compiled from it, which matters if you fork the image rather than pull it.
A fork replaces rawparser and the pins vary
The dependency file holds the real constraints, and two entries deserve a second reading. First, `github.com/jeremytorres/rawparser v1.0.2` is not consumed as published: a `replace` directive redirects it to `github.com/webp-sh/rawparser`, pinned to one commit hash, so every build compiles a fork maintained by the same people and auditing the upstream module tells you nothing about what ran. That is where a supply chain conversation begins. Second, the pins have moved unevenly. The HTTP path is fiber v2.52.15 over fasthttp v1.73.0, not the standard library's net/http, so handlers and middleware written against net/http do not transfer. Image work goes through govips v2.19.0, which is a binding to a native library rather than a pure Go encoder. Older entries have sat still: the SVG check `github.com/h2non/go-is-svg` is pinned to a pseudo-version whose path carries a 2016 timestamp, and `github.com/h2non/filetype v1.1.4` to a 2023 snapshot. The Go directive is 1.27, matching the golang:1.27-trixie builder, so source builds need a recent toolchain. The last push was on 2026-10-06 and the repository is not archived, so these pins are being touched.
Conversion at request time versus at build time
Two other approaches cover the same job, and the real difference is when the CPU gets spent. A build-time converter writes webp and avif files next to the originals during deployment. Nothing runs per request, the origin serves static bytes, and every source image change has to go through the build again. WebP Server Go inverts that order: it converts on the first request for an uncached source, stores the output in the exhaust directory, and validates it later against the metadata directory. You gain automatic coverage of images that already exist and no build step, at the cost of per-instance CPU on a cold cache plus two extra directories to size and back up. The same maintainers run a hosted alternative, WebP Cloud, aimed at static sites, which needs no local install, converts by URL prefix, takes sizing through a query parameter such as `?width=200`, and caches served images to spare the origin. That removes the container and both cache directories from your stack and moves image traffic to a third party instead. Which one fits depends on whether you already run a reverse proxy: with one, the local server costs a single container; without one, the hosted path is far less plumbing. No pricing, throughput or format parity data is published for the hosted service.
Editorial conclusion
WebP Server Go fits teams that already serve images from disk or from an application backend and cannot rewrite every template: it converts on the fly, keeps the URL and needs no build step. It does not fit a static site with no origin process to sit in front of, or anyone who wants upload-time conversion instead of request-time work. Before adopting it, confirm that your libvips build handles every input format you actually serve, and size the exhaust and metadata volumes for a cache that never shrinks on its own.
Frequently asked questions
Do I need Docker to run WebP Server Go?
The README recommends Docker and warns that running the binary directly may hit `glibc` and dependency library problems, sending binary users to its own documentation instead. The image installs `libvips` and the libheif plugins at run time, which is why that warning exists.
Why does WebP Server Go keep two cache directories instead of one?
The split is deliberate: `exhaust` holds converted output images, while `metadata` holds the source metadata used to validate the cache, and both sit apart from `/opt/pics`, which holds your originals.
Which image formats can WebP Server Go convert?
Its README lists JPEG, PNG, BMP, GIF, SVG, HEIC, NEF and WEBP as inputs. GIF is deliberately excluded from AVIF output because the converted AVIF result is not animated.
Is WebP Server Go safe to expose directly to the internet?
Not as documented. The compose example publishes `127.0.0.1:3333:3333`, so the port answers on the host loopback only, and the documented next step is to reverse proxy to it with Nginx `proxy_pass http://127.0.0.1:3333/;`.
Where do I change WebP Server Go settings?
In a `config.json` you generate and mount to `/etc/config.json`, which the image passes to the binary at start-up. The repository ships its own `config.json`, but the README links out for the keys rather than listing them, and it does not state whether a running server reloads a changed file.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/webp-sh-webp-server-go)Was this useful?