Write the deployment guide and an example Caddy config (closes #89)
check / check (push) Failing after 2s

README.md gains a "Deployment" section: 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, what
cache_max_bytes counts and why to set it; the health check for a load
balancer; what SIGTERM does and the exit codes; and what running outside
Docker needs. configs/Caddyfile is the example, as Caddy needs no
settings beyond the host name and pixa's address.

Model: opus-5-5
This commit is contained in:
2026-10-04 12:42:27 +00:00
parent 604b51eea6
commit 719bdbec7a
3 changed files with 85 additions and 3 deletions
+59
View File
@@ -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:
+9 -3
View File
@@ -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 the metrics basic auth, CORS preflight, request logging and
metrics recording have tests (closes #79): `MetricsAuth` on its own answers
401 with a challenge without credentials or with a wrong username or password
@@ -485,6 +494,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
+17
View File
@@ -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