Standalone binary
If Docker isn’t an option — bare-metal homelab, traditional VPS without container support, or just personal preference — you can run Posthorn as a plain Go binary under systemd, supervisord, runit, or your init system of choice.
Install
Section titled “Install”From a release artifact
Section titled “From a release artifact”Pre-built binaries for common platforms are attached to each GitHub release:
curl -L -o posthorn.tar.gz \ https://github.com/craigmccaskill/posthorn/releases/download/v1.0.0/posthorn-linux-amd64.tar.gztar -xzf posthorn.tar.gzsudo install -m 0755 posthorn /usr/local/bin/posthornFrom source
Section titled “From source”go install github.com/craigmccaskill/posthorn/cmd/posthorn@latestRequires Go 1.25+. The binary lands at $GOPATH/bin/posthorn (or $HOME/go/bin/posthorn by default).
From the repo
Section titled “From the repo”git clone https://github.com/craigmccaskill/posthorncd posthorngo build -o posthorn ./core/cmd/posthornVerify
Section titled “Verify”posthorn --version# posthorn v1.0.0Four subcommands — serve, validate, version, help. The two you’ll use day-to-day:
posthorn validate --config /etc/posthorn/config.tomlposthorn serve --config /etc/posthorn/config.tomlSee the CLI reference for the full surface.
validate parses the TOML, resolves ${env.VAR} placeholders, checks the schema, and compiles templates — without starting the listener. Exit 0 means good.
serve starts the HTTP listener on the address configured (default :8080).
See CLI reference for all flags.
systemd unit
Section titled “systemd unit”Create /etc/systemd/system/posthorn.service:
[Unit]Description=Posthorn email gatewayAfter=network-online.targetWants=network-online.target
[Service]Type=simpleUser=posthornGroup=posthornExecStart=/usr/local/bin/posthorn serve --config /etc/posthorn/config.tomlEnvironmentFile=/etc/posthorn/posthorn.envRestart=on-failureRestartSec=5
# HardeningNoNewPrivileges=trueProtectSystem=strictProtectHome=truePrivateTmp=truePrivateDevices=trueProtectKernelTunables=trueProtectKernelModules=trueProtectControlGroups=trueReadOnlyPaths=/etc/posthornRestrictAddressFamilies=AF_INET AF_INET6LockPersonality=trueRestrictRealtime=trueSystemCallArchitectures=native
[Install]WantedBy=multi-user.targetCreate /etc/posthorn/posthorn.env:
POSTMARK_API_KEY=your-postmark-server-tokenRestrict its permissions:
sudo chown root:posthorn /etc/posthorn/posthorn.envsudo chmod 0640 /etc/posthorn/posthorn.envCreate the unprivileged user:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin posthornStart and enable:
sudo systemctl daemon-reloadsudo systemctl enable --now posthornsudo systemctl status posthornLogs:
journalctl -u posthorn -fGraceful shutdown
Section titled “Graceful shutdown”Posthorn handles SIGTERM and SIGINT for graceful shutdown:
- Stop accepting new connections.
- Drain in-flight requests up to each request’s 10-second timeout.
- Exit with code 0.
A second signal forces immediate exit. systemd’s default KillSignal=SIGTERM followed by TimeoutStopSec=90 (default 90 seconds) is more than enough for in-flight drain.
Reverse proxy
Section titled “Reverse proxy”The binary listens on 0.0.0.0:8080 by default. Don’t expose this to the public internet. Bind to loopback or an internal network and reverse-proxy through Caddy, nginx, or your front door of choice. See Reverse proxy for example configs.
Resource use
Section titled “Resource use”Same as the Docker variant — idle around 15-25 MB RSS, single-digit CPU under typical contact-form load. The binary itself is ~12 MB (statically linked Go binary, stripped).