Skip to content

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.

Imageghcr.io/danielmichaels/pf-central, for amd64 and arm64
Tagslatest, and one tag per release such as 0.0.12. Pin a release tag in production.
BaseDistroless, no shell. Runs as UID/GID 65532 (nonroot).
Ports2808 HTTP (web UI, API, /enroll), 24222 NATS
Volume/data. The state folder is /data/state.
Commandpf-central serve. Admin commands run with docker compose exec pf-central pf-central admin ….
DatabaseNot included. Needs PostgreSQL over the network.
  • Directory/opt/pf-central/
    • compose.yaml
    • pf-central.env secrets, mode 0600
    • Caddyfile
  1. Point DNS at the host. Create pf.example.com (an A or AAAA record) for the host. Agents and people both use this name.

  2. Write the secrets file. Generate values with openssl rand -hex 32:

    /opt/pf-central/pf-central.env
    POSTGRES_PASSWORD=generate-me
    PFCENTRAL_DATABASE_URL=postgres://pf_central:generate-me@postgres:5432/pf_central?sslmode=disable
    PFCENTRAL_TOKEN_PEPPER_CURRENT=generate-me
    PFCENTRAL_TOKEN_PEPPER_VERSIONS=generate-me
    Terminal window
    chmod 600 /opt/pf-central/pf-central.env
  3. Write the Compose file.

    /opt/pf-central/compose.yaml
    services:
    postgres:
    image: postgres:17
    restart: unless-stopped
    env_file: pf-central.env
    environment:
    POSTGRES_DB: pf_central
    POSTGRES_USER: pf_central
    volumes:
    - pgdata:/var/lib/postgresql/data
    healthcheck:
    test: ["CMD-SHELL", "pg_isready -U pf_central -d pf_central"]
    interval: 5s
    retries: 10
    pf-central:
    image: ghcr.io/danielmichaels/pf-central:0.0.12 # pin; see Upgrade the server
    restart: unless-stopped
    depends_on:
    postgres:
    condition: service_healthy
    env_file: pf-central.env
    environment:
    PFCENTRAL_SERVER_BASE_URL: https://pf.example.com
    PFCENTRAL_TRUSTED_ORIGINS: https://pf.example.com
    PFCENTRAL_NATS_TLS_ENABLED: "true"
    PFCENTRAL_NATS_TLS_HOSTS: pf.example.com
    PFCENTRAL_LOG_LEVEL: info
    ports:
    - "24222:24222" # agents connect here directly
    expose:
    - "2808" # only Caddy reaches the web port
    volumes:
    - state:/data
    caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
    - "80:80"
    - "443:443"
    volumes:
    - ./Caddyfile:/etc/caddy/Caddyfile:ro
    - caddy:/data
    volumes:
    pgdata:
    state:
    caddy:
  4. 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 -1 passes the UI’s live updates (server-sent events) through without buffering.

  5. Start it and create the owner.

    Terminal window
    cd /opt/pf-central
    docker compose up -d
    docker 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.

  6. 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.

VariableSet it toWhy
PFCENTRAL_SERVER_BASE_URLYour public HTTPS URLSetup, invite and reset links use it
PFCENTRAL_TRUSTED_ORIGINSYour public HTTPS URLForm posts from other origins are refused
PFCENTRAL_COOKIE_SECURELeave unset (true)Session cookies only over HTTPS
PFCENTRAL_TOKEN_PEPPER_*A random value, set onceProtects stored secrets; see Rotate
PFCENTRAL_NATS_TLS_ENABLEDtrueEncrypts the agent link
PFCENTRAL_NATS_TLS_HOSTSEvery name agents dial, ;-separatedGoes into the NATS certificate. Set before the first TLS start.
PFCENTRAL_LOG_LEVELinfoThe default is debug
PFCENTRAL_RATELIMIT_GLOBAL_RPMLeave at 600 unless you see HTTP 429One shared limit for all HTTP requests to this server

The full list is in Environment variables.

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.