Docker (recommended)
Running Posthorn as a Docker container next to your other services is the recommended deployment shape. It keeps the email-gateway concern decoupled from your reverse proxy, your application stack, and your TLS termination.
The official image is published to GitHub Container Registry:
docker pull ghcr.io/craigmccaskill/posthorn:latestMulti-arch tags are published for linux/amd64 and linux/arm64 from a single distroless base. The manifest list selects the right architecture automatically — docker pull on an ARM host fetches the arm64 image, on an x86 host the amd64 image, with no flag needed.
docker-compose.yml
Section titled “docker-compose.yml”The canonical setup. Mount the config read-only, pass the Postmark token through the environment, expose port 8080 to the internal network only (reverse-proxy from your front door):
services: posthorn: image: ghcr.io/craigmccaskill/posthorn:latest restart: unless-stopped volumes: - ./posthorn.toml:/etc/posthorn/config.toml:ro environment: POSTMARK_API_KEY: ${POSTMARK_API_KEY} networks: - web ports: - "127.0.0.1:8080:8080" # bind to loopback only; reverse-proxy from there
networks: web: external: true # if you share a network with your reverse proxyPair with .env:
POSTMARK_API_KEY=your-postmark-server-tokenImage conventions
Section titled “Image conventions”| Detail | Value |
|---|---|
| Default config path | /etc/posthorn/config.toml |
| Default listen address | :8080 |
| Entry point | posthorn serve --config /etc/posthorn/config.toml |
| Non-root user | nonroot (UID 65532) from distroless — read-only mounts work without UID matching |
| Filesystem | Distroless-static; no shell, no package manager, no debug tools |
The image is intentionally minimal — distroless means no sh, bash, apk, apt, or coreutils. Debugging via docker exec is not possible. This is by design (smaller attack surface, no risk of leaving debug tools in production).
If you need to inspect the container for development, override the image command with a debug image temporarily.
When a vX.Y.Z tag is pushed to the GitHub repo, the release workflow publishes multi-arch (linux/amd64, linux/arm64) images to GHCR.
| Tag | What you get |
|---|---|
:latest | Most recent stable release. Multi-arch. |
:vX.Y.Z | Exact tagged version (e.g., :v1.0.0). Multi-arch. |
:vX.Y | Latest patch in the X.Y.* minor line. Floats. |
:vX | Latest release in the X.* major line. Floats. |
For production, pin to a specific version (:vX.Y.Z) and update intentionally. :latest is convenient for development. Check the GHCR package page for the current tag set.
Health checks
Section titled “Health checks”Posthorn exposes a /healthz endpoint on the same listener as the configured endpoints. It returns 200 OK with body ok when the listener is alive. For Docker:
services: posthorn: # ... healthcheck: test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/healthz || exit 1"] interval: 30s timeout: 3s retries: 3 start_period: 5sThe distroless image has no shell or wget. For the distroless variant, run the health check from the reverse proxy or a sidecar, or use the grpc-health-probe-style technique of building a tiny static binary into the image.
For Prometheus-style scrapes, the same listener exposes /metrics in the Prometheus text exposition format.
Posthorn writes structured JSON logs to stdout. With the default Docker logging driver, those land in docker logs posthorn. For production, ship them to your log pipeline via the appropriate driver:
services: posthorn: # ... logging: driver: "json-file" options: max-size: "10m" max-file: "3"Or to a remote sink:
services: posthorn: # ... logging: driver: "loki" options: loki-url: "https://loki.example.com/loki/api/v1/push"Updating
Section titled “Updating”docker compose pull posthorndocker compose up -d posthornPosthorn handles SIGTERM gracefully — it stops accepting new connections, drains in-flight requests up to the 10-second per-request cap, then exits. The rolling update is safe; in-flight submissions complete before the container is replaced.
Resource limits
Section titled “Resource limits”Posthorn is light. Typical resource use:
| Resource | Idle | Under load |
|---|---|---|
| Memory | ~15-25 MB | ~50 MB |
| CPU | under 1% | under 5% |
| File descriptors | ~30 | ~80 |
For a docker-compose deployment, you generally don’t need explicit limits. If you want belt-and-suspenders:
services: posthorn: # ... deploy: resources: limits: memory: 256M cpus: '0.5'