pgbr
Getting Started

Installation

Install pgbr with one command, or wire it up yourself with Docker Compose or plain docker run.

pgbr ships as two published images:

docker pull ghcr.io/darseen/pgbr-dashboard:latest
docker pull ghcr.io/darseen/pgbr-worker:latest

Both are built from the same monorepo and released together, so always run matching tags. Images are tagged latest (stable releases only) and with the exact version, e.g. 2.3.0.

Option 1: One-line install

curl -fsSL https://raw.githubusercontent.com/darseen/pgbr/main/scripts/install.sh | sh

The installer checks that Docker is running, creates a pgbr/ directory, downloads compose.prod.yaml, generates every secret into a .env file it writes with mode 0600, starts the stack, and waits for the dashboard to report healthy. The only thing it asks you is the URL you will open pgbr on.

Pass that in to skip the prompt entirely — useful from cloud-init or a provisioning script:

curl -fsSL https://raw.githubusercontent.com/darseen/pgbr/main/scripts/install.sh \
  | BASE_URL=https://pgbr.example.com sh

Everything it writes is a normal file you own afterwards: edit .env, run docker compose -f compose.prod.yaml up -d, and you are on the manual path described below. The installer is a starting point, not a runtime dependency.

What you can set

VariableDefaultEffect
BASE_URLprompt, else http://localhost:3000The URL you open in the browser
PGBR_DIR./pgbrWhere the install lives
PGBR_PORT3000Host port the dashboard is published on
PGBR_BIND_ADDR0.0.0.0Set to 127.0.0.1 when a reverse proxy fronts it
PGBR_VERSIONlatestImage tag for both services
PGBR_REFmainGit ref to fetch compose.prod.yaml from

Setting PGBR_REF to a release tag pins the images to match, so PGBR_REF=2.4.1 gets you that version's compose file and that version's images.

Piping a script from the internet into a shell means trusting whatever that URL serves. Read it first if you would rather not:

curl -fsSL https://raw.githubusercontent.com/darseen/pgbr/main/scripts/install.sh -o install.sh
less install.sh
sh install.sh

Re-running it upgrades in place

The installer is safe to run again in an existing install, and that is the supported upgrade path: it refreshes compose.prod.yaml (keeping your previous one as compose.prod.yaml.bak), pulls newer images, and restarts. It never rewrites a value already in your .env — it only appends keys that are missing.

That guarantee matters most for ENCRYPTION_KEY. Regenerating it would leave every saved connection string permanently undecryptable, so the installer will not touch one that already exists — and neither should you.

Configure the environment

The installer writes this file for you. You need it for the two manual paths below, and it is worth understanding either way.

The dashboard and worker share most of their configuration. Put the shared values in a single .env file and pass it to both containers so the two can never drift apart:

.env
DATABASE_URL=postgresql://user:pass@host:5432/pgbr
REDIS_URL=redis://redis:6379
ENCRYPTION_KEY=your-encryption-key
AUTH_SECRET=your-secret-key

# The URL you open in the browser. Required unless that's localhost.
BASE_URL=https://pgbr.example.com

# Object store — these defaults target the SeaweedFS service in the Compose file.
STORAGE_ENDPOINT=http://seaweedfs:8333
STORAGE_REGION=us-east-1
STORAGE_BUCKET=pgbr
STORAGE_ACCESS_KEY_ID=pgbr
STORAGE_SECRET_ACCESS_KEY=pgbrsecret
STORAGE_FORCE_PATH_STYLE=true

Generate the two secrets with:

openssl rand -base64 32   # ENCRYPTION_KEY
openssl rand -base64 32   # AUTH_SECRET

ENCRYPTION_KEY must be identical on the dashboard and the worker, and must never change once you have saved a database connection. It decrypts your stored connection strings and storage credentials — rotating it makes every existing connection permanently unreadable.

AUTH_SECRET is only read by the dashboard, but it is harmless for the worker to receive it from the shared file. See the environment variable reference for every supported value.

Option 2: Docker Compose

The repository ships compose.prod.yaml — the same file the installer downloads. Fetch it alongside an .env, fill in the four required values, and start it:

mkdir pgbr && cd pgbr
curl -fsSL https://raw.githubusercontent.com/darseen/pgbr/main/compose.prod.yaml -o compose.prod.yaml
curl -fsSL https://raw.githubusercontent.com/darseen/pgbr/main/.env.prod.example -o .env
# set ENCRYPTION_KEY, AUTH_SECRET, POSTGRES_PASSWORD, BASE_URL
docker compose -f compose.prod.yaml up -d

Compose loads .env from the same directory automatically. Leave a required secret unset and the stack refuses to start rather than booting with a placeholder:

error while interpolating services.worker.environment.DATABASE_URL:
required variable POSTGRES_PASSWORD is missing a value: set POSTGRES_PASSWORD in .env

What that file does beyond starting the five containers:

  • Postgres, Redis, and SeaweedFS sit on an internal network — no published ports and no internet access. Only the dashboard's port reaches the host.
  • DATABASE_URL is derived from the POSTGRES_* values, so the app and the database cannot disagree about the password.
  • The worker waits for the dashboard to report healthy, which means migrations have finished before it touches the schema.
  • stop_grace_period is long enough for an in-flight pg_dump to finish instead of being killed ten seconds into a shutdown.
  • Redis persists with AOF, so queued jobs survive a restart.
  • Every service gets log rotation, so a long-running install can't fill the disk with JSON logs.

Don't confuse it with compose.yaml, also in the repository. That one is the development setup: it builds from source, ships hardcoded example secrets, and includes two throwaway test_db containers to practise against. Never deploy it.

Option 3: docker run

Create a network

docker network create pgbr

Start Redis

docker run -d --network pgbr --name redis redis:8-alpine

Start the object store

This is the only component that needs a durable volume.

docker run -d --network pgbr \
  -v /path/on/your/machine:/data \
  --name seaweedfs \
  chrislusf/seaweedfs:latest server -dir=/data -s3 -s3.port=8333

Skip this step if you are using external object storage — set the STORAGE_* variables to point at it instead.

Start the dashboard

docker run -d --network pgbr \
  -p 3000:3000 \
  --env-file .env \
  --name pgbr \
  ghcr.io/darseen/pgbr-dashboard:latest

Start the worker

docker run -d --network pgbr \
  --env-file .env \
  --name pgbr-worker \
  ghcr.io/darseen/pgbr-worker:latest

You'll also need a PostgreSQL instance for DATABASE_URL — either an existing one or a postgres:18-alpine container on the same network.

What happens on first boot

The dashboard's entrypoint runs database migrations automatically before the server starts, so you never migrate by hand:

node packages/db/dist/migrate.js

The worker, on boot:

  1. Reconciles backup schedules from Postgres into Redis, behind a short Redis lock so exactly one replica does it when you run several.
  2. Creates the configured storage bucket if it doesn't exist.

If AUTH_SECRET is unset, the dashboard generates a temporary one at startup. That works, but every restart invalidates all sessions — set it explicitly.

Scaling

The dashboard and worker are stateless, so both scale horizontally with no extra configuration:

  • More workers — run more pgbr-worker containers. Schedule reconciliation is lock-guarded and job processors are idempotent, so replicas coordinate safely. With Compose that is docker compose -f compose.prod.yaml up -d --scale worker=3.
  • Per-worker throughput — set WORKER_CONCURRENCY (default 5). This is the concurrency of each queue, so one worker can run up to 5 backups, 5 restores, and 5 migrations at once.

Concurrency multiplies real load. Each running job is a pg_dump/pg_restore process against your database, plus scratch disk in the worker container. Size WORKER_CONCURRENCY against your database's connection limit and the worker's disk, not against how fast you'd like the queue to drain.

Next steps

On this page