Installation
Posthorn ships in three forms. Docker is the recommended path for most operators — it’s a single container running next to your apps, and it’s the deployment shape that’s exercised most heavily in tests and documentation.
docker pull ghcr.io/craigmccaskill/posthorn:latestMulti-arch images are published for linux/amd64 and linux/arm64. The container expects a TOML config mounted at /etc/posthorn/config.toml and listens on 8080 by default.
A minimal docker-compose.yml:
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} ports: - "8080:8080"Continue to the Quick start for a working contact form.
go install github.com/craigmccaskill/posthorn/cmd/posthorn@latestRequires Go 1.25+. The binary is fully self-contained and runs anywhere.
posthorn validate --config posthorn.toml # check the config without servingposthorn serve --config posthorn.toml # start the HTTP listenerPre-built binaries for common platforms are attached to each GitHub release.
For operators already running Caddy as a front door, the form-ingress mode is also exposed as a Caddy v2 module. Build a Caddy with the adapter:
xcaddy build --with github.com/craigmccaskill/posthorn/caddyThe Caddyfile directive is posthorn:
example.com { posthorn /api/contact { to you@example.com from "Contact Form <noreply@example.com>" transport postmark { api_key {env.POSTMARK_API_KEY} } }}See Caddy adapter for the full directive grammar. Requires Caddy 2.9+.
Prerequisites
Section titled “Prerequisites”Regardless of how you install Posthorn, you need:
| Prerequisite | Why |
|---|---|
| A Postmark account with a verified sender domain | Posthorn relays through Postmark’s HTTP API |
| A Postmark server token (not the account token) | Used as ${env.POSTMARK_API_KEY} |
| SPF, DKIM, and DMARC records on your sending domain | Required for deliverability — see DNS |
| A reverse proxy in front of Posthorn (Caddy, nginx, Traefik, Cloudflare) | TLS termination, HTTP/2, request logging |
Verifying the install
Section titled “Verifying the install”docker run --rm ghcr.io/craigmccaskill/posthorn:latest --version# posthorn v1.0.0If you’re running the standalone binary:
posthorn --versionFor a deeper sanity check, run posthorn validate --config <your-config> — it parses the TOML, resolves ${env.VAR} placeholders, and reports schema errors without starting the listener.