Docker and Compose
This page deploys pfCentral on one Linux host with Docker Compose, HTTPS through Caddy, and TLS on the agent link. If you have not run pfCentral before, do Tutorial 1 first; this page assumes you know the moving parts.
The image
Section titled “The image”| Image | ghcr.io/danielmichaels/pf-central, for amd64 and arm64 |
| Tags | latest, and one tag per release such as 0.0.12. Pin a release tag in production. |
| Base | Distroless, no shell. Runs as UID/GID 65532 (nonroot). |
| Ports | 2808 HTTP (web UI, API, /enroll), 24222 NATS |
| Volume | /data. The state folder is /data/state. |
| Command | pf-central serve. Admin commands run with docker compose exec pf-central pf-central admin …. |
| Database | Not included. Needs PostgreSQL over the network. |
Directory/opt/pf-central/
- compose.yaml
- pf-central.env secrets, mode 0600
- Caddyfile
-
Point DNS at the host. Create
pf.example.com(an A or AAAA record) for the host. Agents and people both use this name. -
Write the secrets file. Generate values with
openssl rand -hex 32:/opt/pf-central/pf-central.env POSTGRES_PASSWORD=generate-mePFCENTRAL_DATABASE_URL=postgres://pf_central:generate-me@postgres:5432/pf_central?sslmode=disablePFCENTRAL_TOKEN_PEPPER_CURRENT=generate-mePFCENTRAL_TOKEN_PEPPER_VERSIONS=generate-meTerminal window chmod 600 /opt/pf-central/pf-central.env -
Write the Compose file.
/opt/pf-central/compose.yaml services:postgres:image: postgres:17restart: unless-stoppedenv_file: pf-central.envenvironment:POSTGRES_DB: pf_centralPOSTGRES_USER: pf_centralvolumes:- pgdata:/var/lib/postgresql/datahealthcheck:test: ["CMD-SHELL", "pg_isready -U pf_central -d pf_central"]interval: 5sretries: 10pf-central:image: ghcr.io/danielmichaels/pf-central:0.0.12 # pin; see Upgrade the serverrestart: unless-stoppeddepends_on:postgres:condition: service_healthyenv_file: pf-central.envenvironment:PFCENTRAL_SERVER_BASE_URL: https://pf.example.comPFCENTRAL_TRUSTED_ORIGINS: https://pf.example.comPFCENTRAL_NATS_TLS_ENABLED: "true"PFCENTRAL_NATS_TLS_HOSTS: pf.example.comPFCENTRAL_LOG_LEVEL: infoports:- "24222:24222" # agents connect here directlyexpose:- "2808" # only Caddy reaches the web portvolumes:- state:/datacaddy:image: caddy:2restart: unless-stoppedports:- "80:80"- "443:443"volumes:- ./Caddyfile:/etc/caddy/Caddyfile:ro- caddy:/datavolumes:pgdata:state:caddy: -
Write the Caddyfile. Caddy gets a certificate from Let’s Encrypt for you. For an internal CA, see HTTPS for the web UI.
/opt/pf-central/Caddyfile pf.example.com {reverse_proxy pf-central:2808 {flush_interval -1}}flush_interval -1passes the UI’s live updates (server-sent events) through without buffering. -
Start it and create the owner.
Terminal window cd /opt/pf-centraldocker compose up -ddocker compose logs pf-central | grep -A2 "first-run setup"Open the printed link at
https://pf.example.com/setup?token=…and create the owner account. -
Check the agent endpoints.
Terminal window curl -fsS https://pf.example.com/nats-ca.crt | head -1 # -----BEGIN CERTIFICATE-----openssl s_client -connect pf.example.com:24222 -servername pf.example.com </dev/null 2>/dev/null | grep "Verify return code"The second command reports a verify error. That is expected: the NATS certificate is signed by pfCentral’s own CA, which agents download from
/nats-ca.crt.
Settings that matter in production
Section titled “Settings that matter in production”| Variable | Set it to | Why |
|---|---|---|
PFCENTRAL_SERVER_BASE_URL | Your public HTTPS URL | Setup, invite and reset links use it |
PFCENTRAL_TRUSTED_ORIGINS | Your public HTTPS URL | Form posts from other origins are refused |
PFCENTRAL_COOKIE_SECURE | Leave unset (true) | Session cookies only over HTTPS |
PFCENTRAL_TOKEN_PEPPER_* | A random value, set once | Protects stored secrets; see Rotate |
PFCENTRAL_NATS_TLS_ENABLED | true | Encrypts the agent link |
PFCENTRAL_NATS_TLS_HOSTS | Every name agents dial, ;-separated | Goes into the NATS certificate. Set before the first TLS start. |
PFCENTRAL_LOG_LEVEL | info | The default is debug |
PFCENTRAL_RATELIMIT_GLOBAL_RPM | Leave at 600 unless you see HTTP 429 | One shared limit for all HTTP requests to this server |
The full list is in Environment variables.
Running without Compose
Section titled “Running without Compose”The same settings work with docker run, Podman, or the plain binary from the releases page. With the binary, set PFCENTRAL_STATE_DIR to a persistent folder and run pf-central serve under systemd.
- TLS for the agent link explains the CA and how agents pin it.
- Enroll firewalls with
tls://pf.example.com:24222andhttps://pf.example.com/enroll. - Back up and restore before you enroll more than a few firewalls.