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:latestBoth 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 | shThe 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 shEverything 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
| Variable | Default | Effect |
|---|---|---|
BASE_URL | prompt, else http://localhost:3000 | The URL you open in the browser |
PGBR_DIR | ./pgbr | Where the install lives |
PGBR_PORT | 3000 | Host port the dashboard is published on |
PGBR_BIND_ADDR | 0.0.0.0 | Set to 127.0.0.1 when a reverse proxy fronts it |
PGBR_VERSION | latest | Image tag for both services |
PGBR_REF | main | Git 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.shRe-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:
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=trueGenerate the two secrets with:
openssl rand -base64 32 # ENCRYPTION_KEY
openssl rand -base64 32 # AUTH_SECRETENCRYPTION_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 -dCompose 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 .envWhat 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_URLis derived from thePOSTGRES_*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_periodis long enough for an in-flightpg_dumpto 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 pgbrStart Redis
docker run -d --network pgbr --name redis redis:8-alpineStart 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=8333Skip 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:latestStart the worker
docker run -d --network pgbr \
--env-file .env \
--name pgbr-worker \
ghcr.io/darseen/pgbr-worker:latestYou'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.jsThe worker, on boot:
- Reconciles backup schedules from Postgres into Redis, behind a short Redis lock so exactly one replica does it when you run several.
- 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-workercontainers. Schedule reconciliation is lock-guarded and job processors are idempotent, so replicas coordinate safely. With Compose that isdocker compose -f compose.prod.yaml up -d --scale worker=3. - Per-worker throughput — set
WORKER_CONCURRENCY(default5). 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.
pgbr