Skip to content

Run with Compose or systemd

Save this as docker-compose.yml. Provision an auth code secret of at least 32 characters through your secret manager. Set AUTH_SECRET_PATH to its mounted source file, readable by UID/GID 65532.

services:
pitot:
image: ghcr.io/labstack/pitot:2026.11.0-alpha
container_name: pitot
restart: unless-stopped
ports:
- "127.0.0.1:7520:7520"
volumes:
- pitot-data:/var/lib/pitot/data
environment:
PITOT_AUTH_CODE_SECRET_FILE: /run/secrets/auth_code
secrets:
- auth_code
healthcheck:
test: ["CMD", "pitot", "healthcheck"]
interval: 30s
timeout: 3s
start_period: 40s
retries: 3
secrets:
auth_code:
file: ${AUTH_SECRET_PATH}
volumes:
pitot-data:

Start with docker compose up -d. Read the setup URL with docker compose logs pitot, then complete first boot. The image listens on :7520 and keeps data in /var/lib/pitot/data. The port mapping above exposes it only on the host’s loopback interface. Use TLS and a restricted network route for remote clients.

The ingest token is issued during administrator setup and stored in control state as a hash. An automated install can set the first token instead with PITOT_INGEST_TOKEN or PITOT_INGEST_TOKEN_FILE; see send telemetry. Give the token to your exporters through their secret delivery mechanism.

A named volume preserves data across container replacement. Do not remove it when upgrading. The healthcheck marks an unhealthy container; Docker does not restart it solely because that check fails.

Install the binary as /usr/local/bin/pitot. Create a dedicated system user and group named pitot, with no interactive login. Store the auth secret at /etc/pitot/auth_code, readable only by root. systemd passes it to the service as a credential.

Save this as /etc/systemd/system/pitot.service:

[Unit]
Description=Pitot
After=network.target
[Service]
Type=simple
User=pitot
Group=pitot
StateDirectory=pitot
WorkingDirectory=/var/lib/pitot
Environment=PITOT_ADDR=127.0.0.1:7520
Environment=PITOT_DATA_DIR=/var/lib/pitot/data
LoadCredential=pitot_auth_code:/etc/pitot/auth_code
Environment=PITOT_AUTH_CODE_SECRET_FILE=%d/pitot_auth_code
ExecStart=/usr/local/bin/pitot
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target

StateDirectory creates /var/lib/pitot for the service user. Pitot creates its data subdirectory. Load and start the unit:

Terminal window
sudo systemctl daemon-reload
sudo systemctl enable --now pitot
sudo journalctl -u pitot

Use the setup URL from the journal. Restrict journal access because that URL is an administrator credential. Check http://127.0.0.1:7520/readyz after startup. See hardening before exposing the listener.

Some TLS-terminating proxies connect to the origin only on port 443. For example, Cloudflare’s gRPC proxying requires the origin to listen on 443 with TLS and HTTP/2 advertised through ALPN, which Pitot’s direct TLS provides. On Cloudflare, also enable gRPC in the zone’s network settings (otherwise Cloudflare answers gRPC requests with 403 Forbidden), set the DNS record to proxied, and set the SSL/TLS encryption mode to Full (strict). Cloudflare accepts Full, but only Full (strict) verifies the origin certificate.

Linux reserves ports below 1024 for privileged processes, so grant the unprivileged pitot user only the capability to bind them rather than running as root.

Add a drop-in with sudo systemctl edit pitot:

[Service]
Environment=PITOT_ADDR=:443
LoadCredential=pitot_tls_cert:/etc/pitot/tls/cert.pem
LoadCredential=pitot_tls_key:/etc/pitot/tls/key.pem
Environment=PITOT_TLS_CERT_FILE=%d/pitot_tls_cert
Environment=PITOT_TLS_KEY_FILE=%d/pitot_tls_key
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true

AmbientCapabilities gives the process CAP_NET_BIND_SERVICE although it runs as pitot. CapabilityBoundingSet drops every other capability, and NoNewPrivileges stops the process and its children from gaining more. The later PITOT_ADDR replaces the unit’s loopback address; :443 listens on every interface, so restrict the port to the proxy’s networks with a firewall. For Cloudflare, allow only Cloudflare’s published IP ranges. The certificate and key are passed as credentials, like the auth secret, so they can stay readable only by root. systemd copies credentials when the service starts, and Pitot loads the certificate once, so restart the service after renewing the certificate.

Restart and confirm the listener runs as pitot with only that capability:

Terminal window
sudo systemctl daemon-reload
sudo systemctl restart pitot
systemctl show pitot -p User -p AmbientCapabilities -p CapabilityBoundingSet
sudo ss -ltnp 'sport = :443'
grep Cap /proc/"$(systemctl show pitot -p MainPID --value)"/status

ss should show the pitot process on port 443, and CapAmb and CapEff should both be 0000000000000400 (CAP_NET_BIND_SERVICE alone). Check readiness on the host with a name in the certificate:

Terminal window
curl --resolve pitot.example.com:443:127.0.0.1 https://pitot.example.com/readyz

If the certificate comes from a private CA or is a Cloudflare Origin CA certificate, which public trust stores reject, pass that CA’s certificate with --cacert, for example --cacert /etc/pitot/tls/origin-ca.pem.