diff --git a/README.md b/README.md index 2ab577b..54de083 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,65 @@ else has a built-in default. A config file mounted at `/etc/pixa/config.yml` is optional: it is read when present, and an environment variable wins over the same setting in it. +## Deployment + +pixa listens on plain HTTP and runs behind a reverse proxy that terminates TLS. +[`configs/Caddyfile`](configs/Caddyfile) is an example for Caddy, chosen because +it is the smallest correct one: Caddy gets the TLS certificate itself and does +everything in this list without further settings. The reverse proxy must: + +- terminate TLS, as the login and generator pages work only over HTTPS (see + Routes); +- pass the `Host`, `Origin` and `Referer` headers on unchanged, as pixa refuses + a form from those pages unless `Origin` or `Referer` names the host in `Host`, + and builds encrypted URLs from `Host`; +- set `X-Forwarded-For` to the client's address, with `trusted_proxies` set to + the address pixa sees the proxy's requests come from, so the login limit + counts each client by its own address (see `trusted_proxies` under + Configuration); +- wait for pixa's answer for at least `downstream_timeout` (default `60s`), the + longest pixa takes to fetch, convert and send an image. + +It may also refuse `/metrics`, as the example does, so that only a scraper that +reaches pixa directly can read it; pixa itself asks for the metrics username and +password there. + +pixa does the rest itself: it checks signatures and encrypted URLs, applies the +allowlist, refuses upstream hosts with private or local addresses, limits login +attempts, upstream response size and image dimensions, and sends the security +headers, `Strict-Transport-Security` included, with every response. + +The state directory (`state_dir`, `/var/lib/pixa` in the container) holds the +database and the disk cache: + +- It needs a persistent volume: without one, every restart starts with an empty + cache. In the container, the startup script gives the directory to the user + pixa runs as (uid 65532) and sets its mode to `750`; outside it, that user + must be able to write the directory. +- `cache_max_bytes` limits the source and transformed images together. The + database, the metadata files, the `.meta` file beside each transformed image + and files still being written come on top, and eviction runs in the + background, so the cache can pass the limit for a while: leave room on the + volume beyond it. +- Set `cache_max_bytes` for a lasting deployment. Its default is 75% of the + space free when pixa starts, which the cache's own files reduce, so a fuller + cache gives a smaller limit after a restart. + +A load balancer's health check can request `/.well-known/healthcheck.json`, +which answers 200 whenever pixa is running, in maintenance mode too (see +`maintenance_mode`). + +On SIGTERM or SIGINT pixa stops accepting connections, gives the requests in +progress and the images being processed 5 seconds to finish, and exits: with 0, +or with 1 when images were still being processed after those 5 seconds or +another part of pixa failed to stop. A request not finished by then is cut off. +`docker stop` waits 10 seconds before it kills the container. + +Outside Docker, pixa needs libvips (the image has 8.15) and libheif to run, as +it uses libvips through CGO; building it also needs their development files, +`pkg-config` and a C compiler. `script/bootstrap` installs all of these with +nix, apt, brew or apk. + ## Running under upaas What the [upaas](https://git.eeqj.de/sneak/upaas) app for pixa needs: diff --git a/TODO.md b/TODO.md index 2138144..fc475a5 100644 --- a/TODO.md +++ b/TODO.md @@ -29,6 +29,15 @@ P2: security: referer blacklist # Completed Steps +- 2026-10-04 deployment guide and example Caddy config (closes #89): + "Deployment" in `README.md` says what the reverse proxy in front of pixa must + do (terminate TLS; pass `Host`, `Origin` and `Referer` on unchanged; set + `X-Forwarded-For`, with `trusted_proxies` to match; wait at least + `downstream_timeout`; optionally refuse `/metrics`) and what pixa does itself, + that the state directory needs a persistent volume and what `cache_max_bytes` + counts, the health check for a load balancer, what a stop does and its exit + codes, and what running outside Docker needs; `configs/Caddyfile` is the + example, checked with `caddy validate`. - 2026-10-04 request IDs returned and passed on, and `/v1/e/` revalidates (closes #84): pixa's own `RequestID` middleware, in place of chi's, gives each request an ID, its own `X-Request-ID` when that is at most 64 letters, digits, @@ -461,6 +470,3 @@ P2: security: referer blacklist - Prometheus performance metrics - integration tests for the image proxy flow - load tests to verify the 1k to 5k req/s target -- P2: documentation - - deployment guide - - example nginx or caddy reverse proxy config diff --git a/configs/Caddyfile b/configs/Caddyfile new file mode 100644 index 0000000..df453b5 --- /dev/null +++ b/configs/Caddyfile @@ -0,0 +1,17 @@ +# Example Caddy config for running pixa behind Caddy; see "Deployment" in +# README.md. Replace images.example.com with pixa's public host name, and +# 127.0.0.1:8080 with the address Caddy reaches pixa on. +# +# Caddy gets and renews the TLS certificate for the host name, passes the +# Host, Origin and Referer headers on unchanged, sets X-Forwarded-For to the +# client's address, and waits for pixa's answer with no time limit of its +# own, so pixa's downstream_timeout is what ends a slow request. + +images.example.com + +# pixa asks for metrics.username and metrics.password on /metrics. This +# line also keeps it off the public address, for a scraper that reaches +# pixa directly; remove it to read /metrics through Caddy. +respond /metrics 404 + +reverse_proxy 127.0.0.1:8080