Configuration

Every environment variable EzFD reads. On a deploy.sh install these live in /opt/ezfd/.env, written by deploy.sh and read by the systemd unit. On a Docker Compose install they live in .env beside compose.yaml, in a slightly different shape — see The Docker Compose .env.

Required

DATABASE_URL

PostgreSQL connection string.

DATABASE_URL=postgres://ezfd:PASSWORD@localhost:5432/ezfd

Used by the connection pool and, separately, by the real-time endpoint, which opens its own dedicated connection per client to LISTEN on that event's channels.

A connection pooler in transaction mode (PgBouncer's default) silently breaks LISTEN/NOTIFY and therefore all live updates. Use session mode or connect directly.

EZFD_ENCRYPTION_KEY

64 hex characters — 32 bytes. Encrypts stored QRZ passwords with AES-256-GCM.

$ openssl rand -hex 32

deploy.sh generates one automatically and preserves it across updates.

Without it, creating an event that includes QRZ credentials fails with a clear error. Everything else works. Changing it makes existing stored QRZ passwords undecryptable — they must be re-entered.

EZFD_ADMIN_KEY

If set, creating an event requires this key. If unset, anyone who can reach the site can create events.

Worth setting on a public server. Not needed on a private LAN.

Optional

EZFD_DOMAIN, EZFD_CERT_EMAIL

Recorded by deploy.sh for the nginx configuration and certbot. Re-used when you re-run the script so it doesn't re-prompt.

EZFD_REPO_DIR

Where the source was cloned. The admin console's Update application action uses it to find the git checkout to pull. Written by deploy.sh.

EZFD_FD_CALL_HISTORY_URL, EZFD_WFD_CALL_HISTORY_URL

Override where the N1MM call history file is fetched from. Supports a {year} placeholder.

These exist because N1MM's files are contest- and year-specific, published at a URL built from the contest and year. The app derives the URL and falls back to the prior year if the current one isn't published yet. Override if N1MM changes their scheme.

EZFD_MASTER_SCP_URL

Override where MASTER.SCP is fetched from. Unlike the call history file this one is evergreen — not year-specific — and is shared across every event on the server, refreshed at most once a day.

The Docker Compose .env

compose.yaml reads .env from the same directory and builds the app's environment from it. Start from the template in the repository — cp .env.example .env — which lists every setting below with a comment; nothing generates the file for you. The first three are required, and docker compose refuses to start without them, naming the one that is missing.

Variable What it is
POSTGRES_PASSWORD The database superuser's password. Used by the db container and by the init step that applies the schema; the app never sees it
EZFD_DB_PASSWORD The ezfd role's password. The init step sets it on every start and compose builds DATABASE_URL from it, so there is no DATABASE_URL to write
EZFD_ENCRYPTION_KEY As above. 64 hex characters
EZFD_ADMIN_KEY As above. Optional
EZFD_DOMAIN The domain Caddy obtains a certificate for. Blank serves plain HTTP on port 80
EZFD_HTTP_PORT, EZFD_HTTPS_PORT The host ports Caddy listens on, 80 and 443 by default. A certificate needs both at their defaults. Either may carry an address, such as 127.0.0.1:8080, to listen on this machine only behind your own proxy
EZFD_REF The branch, tag or commit of https://github.com/nreed97/EzFD the images are built from. Blank builds master
EZFD_SOURCE Replaces the repository and EZFD_REF entirely. . builds from the checkout compose.yaml sits in, including uncommitted changes; a fork's URL with its own #branch builds the fork. Blank uses GitHub at EZFD_REF

The call history and MASTER.SCP overrides work here too, under the same names. EZFD_CERT_EMAIL and EZFD_REPO_DIR are deploy.sh's and are not used.

Generate the secrets with openssl rand -hex, as .env.example and Deployment → First install with Docker show. Changing EZFD_DB_PASSWORD later is fine, since init re-applies it on the next start. Changing POSTGRES_PASSWORD after the first start is not: the db container only reads it when it creates the database, so the new value stops matching and init fails to connect.

The WSJT-X relay

These are read by wsjtx-bridge.cjs, which runs on the operator's machine, not the server. Each has a matching command-line flag that takes precedence.

Variable Flag Default
EZFD_EVENT_ID --event-id required
EZFD_API_URL --api-url http://localhost:3000
EZFD_OPERATOR --operator (empty)
EZFD_STATION --station 1
EZFD_UDP_PORT --port 2237
EZFD_SPOOL --spool ~/.ezfd/wsjtx-queue-<event-id>.jsonl

See Digital modes.

Applying changes

# nano /opt/ezfd/.env
# systemctl restart ezfd

The service reads the file at start, so a restart is required.

On a Docker install, edit .env beside compose.yaml and recreate the containers, which a plain restart does not do:

$ docker compose up -d

Security notes

/opt/ezfd/.env holds the database password, the encryption key and the admin key. It should be readable only by the service user. The Docker .env holds the same and more, so chmod 600 it; .dockerignore keeps it out of the image.

QRZ passwords are encrypted at rest with EZFD_ENCRYPTION_KEY and never returned by the API — the event endpoint omits the column entirely.

There are no user accounts. Access to an event is the six-character join code, and operator identity is self-asserted at join time. For a special event you can additionally require roster approval before an operator may log, but that is an authorisation gate, not authentication. Treat the join code as the secret it is, and don't expose an instance publicly if that model doesn't suit you.