---
title: "Run with Compose or systemd"
summary: "A Compose service and a systemd unit with mounted secrets and persistent data."
description: "Run a persistent Pitot instance under a process supervisor."
status: preview
read_when:
  - "You need Pitot to restart after an exit or host reboot."
---
## Run with Compose

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.

```yaml
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](/docs/get-started/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](/docs/guides/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](/docs/get-started/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.

## Run with systemd

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`:

```ini
[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:

```sh
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](/docs/guides/hardening) before exposing the listener.

### Listen on port 443

Some TLS-terminating proxies connect to the origin only on port 443. For
example, [Cloudflare's gRPC proxying](https://developers.cloudflare.com/network/grpc-connections/)
requires the origin to listen on 443 with TLS and HTTP/2 advertised through
ALPN, which Pitot's [direct TLS](/docs/guides/tls#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`:

```ini
[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](https://www.cloudflare.com/ips/).
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:

```sh
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:

```sh
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`.