Skip to content

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:

Terminal window
docker pull ghcr.io/craigmccaskill/posthorn:latest

Multi-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.

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 proxy

Pair with .env:

Terminal window
POSTMARK_API_KEY=your-postmark-server-token
DetailValue
Default config path/etc/posthorn/config.toml
Default listen address:8080
Entry pointposthorn serve --config /etc/posthorn/config.toml
Non-root usernonroot (UID 65532) from distroless — read-only mounts work without UID matching
FilesystemDistroless-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.

TagWhat you get
:latestMost recent stable release. Multi-arch.
:vX.Y.ZExact tagged version (e.g., :v1.0.0). Multi-arch.
:vX.YLatest patch in the X.Y.* minor line. Floats.
:vXLatest 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.

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: 5s

The 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"
Terminal window
docker compose pull posthorn
docker compose up -d posthorn

Posthorn 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.

Posthorn is light. Typical resource use:

ResourceIdleUnder load
Memory~15-25 MB~50 MB
CPUunder 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'