Docker Compose
Passbox, PostgreSQL and Caddy with automatic HTTPS.
PostgreSQL
Separate roles, row-level security, encrypted backups.
MIT License
Every feature, free, for any number of members.
Quick start
Clone the repository, fill in the settings and start everything with Docker Compose:
$ git clone https://github.com/passboxnet/passbox$ cd passbox/deploy/compose$ cp .env.example .env # then fill in every value$ docker compose up -dBefore the first start, set PASSBOX_DOMAIN, ACME_EMAIL, the four database passwords (openssl rand -hex 32) and the public backup key BACKUP_AGE_RECIPIENT in .env. Caddy then gets the certificate, the migrate service sets up the database, and the vault is available at your domain. Create your account first; it becomes the instance admin.
services: postgres: image: docker.io/library/postgres:17.10-bookworm volumes: - postgres-data:/var/lib/postgresql/data - ./initdb:/docker-entrypoint-initdb.d:ro networks: [backend] # One-shot: applies database migrations, then exits. migrate: image: ${PASSBOX_IMAGE:-ghcr.io/passboxnet/server:latest} command: ["migrate"] depends_on: postgres: condition: service_healthy networks: [backend] passbox: image: ${PASSBOX_IMAGE:-ghcr.io/passboxnet/server:latest} environment: PASSBOX_DATABASE_URL: postgres://passbox_app:…@postgres:5432/passbox PASSBOX_MIGRATION_DATABASE_URL: postgres://passbox_migrator:…@postgres:5432/passbox PASSBOX_WORKER_DATABASE_URL: postgres://passbox_worker:…@postgres:5432/passbox PASSBOX_PUBLIC_URL: https://${PASSBOX_DOMAIN:?set in .env} depends_on: migrate: condition: service_completed_successfully networks: [backend, frontend] caddy: image: docker.io/library/caddy:2.11.6-alpine ports: - "80:80" - "443:443" - "443:443/udp" networks: [frontend] # Encrypted backups: pg_dump of the database and a tar of the attachment # blobs, encrypted with age to BACKUP_AGE_RECIPIENT. backup: build: ./backup environment: BACKUP_AGE_RECIPIENT: ${BACKUP_AGE_RECIPIENT:?set in .env} networks: [backend]networks: # No route to the internet: only Passbox, migrations and backups reach the # database. backend: internal: true frontend: {}Back up from day one
Requirements
- A Linux host with Docker Engine 24 or later and Compose v2 (amd64 or arm64)
- 1 vCPU and 2 GB of memory for a few hundred users
- A domain name whose DNS records point to the host, with ports 80 and 443 reachable
- Optional: an SMTP server for invitations and security notices
HTTPS
Browsers only allow the cryptography and clipboard features Passbox needs on HTTPS (or localhost). In the Docker Compose setup, Caddy gets and renews the certificate automatically. If you run the image on its own, put your own reverse proxy with TLS in front of it. The server sends strict security headers and rate-limits sign-in attempts on its own.
Administration
The first account on a fresh server becomes its instance admin. Create yours right after starting the server. Accounts listed in PASSBOX_ADMIN_EMAILS are admins too. Admins find Administration in the web vault's sidebar.
- Name the server, set a help address and the language of mails
- Choose who can create an account and require stronger master passwords
- Invite people, see accounts and their last activity, disable accounts
- Read the administration log, protected by a hash chain
| Mode | Who can create an account |
|---|---|
open | Anyone who can reach the server (default) |
invite_only | Only people with an invitation |
domains | Emails of the allowed domains, plus invited people |
Passbox sends invitations and security notices, such as sign-ins from new devices and recovery requests, in English or German. Set PASSBOX_SMTP_URL, PASSBOX_MAIL_FROM and PASSBOX_PUBLIC_URL; without them, mails are written to the log. Mails go through a queue in the database and are retried.
PASSBOX_SMTP_URL='smtps://passbox%40example.com:app-password@smtp.example.com:465'PASSBOX_MAIL_FROM='Passbox <passbox@example.com>'PASSBOX_PUBLIC_URL='https://passbox.example.com'Configuration
Every option can be set as a command-line flag or an environment variable. In the Docker Compose setup, .env fills in the database URLs and the most common options; add others to the environment of the passbox service.
| Variable | Default | Description |
|---|---|---|
PASSBOX_BIND | 0.0.0.0:8080 | Address and port to listen on |
PASSBOX_DATABASE_URL | required | PostgreSQL URL for the API; its role is subject to row-level security |
PASSBOX_MIGRATION_DATABASE_URL | PASSBOX_DATABASE_URL | PostgreSQL URL of the role that owns the schema and runs migrations |
PASSBOX_WORKER_DATABASE_URL | PASSBOX_DATABASE_URL | PostgreSQL URL of the role for background jobs such as the mail queue |
PASSBOX_WEB_DIR | unset | Directory with the built web vault; the image sets it |
PASSBOX_PUBLIC_URL | unset | Address users open Passbox at, for links in mails |
PASSBOX_SMTP_URL | unset | SMTP server for outgoing mail |
PASSBOX_MAIL_FROM | unset | Sender of mails, required with an SMTP server |
PASSBOX_ADMIN_EMAILS | unset | Comma-separated emails that are always instance admins |
PASSBOX_TRUST_PROXY | false | Take the client IP from X-Forwarded-For. Enable only behind a reverse proxy that sets it; IPs are used for rate limiting and shown in the activity log |
PASSBOX_LOG_FORMAT | pretty | pretty or json |
RUST_LOG | info | Log level, for example debug |
Database
Passbox stores everything in PostgreSQL. On its first start, the postgres service creates the database and three roles, each with its own password from .env:
| Role | Used for |
|---|---|
passbox_migrator | Owns the schema; the migrate service runs migrations with it |
passbox_app | The API; row-level security applies to it |
passbox_worker | Background jobs such as the mail queue, with narrow rights |
The database sits on an internal network without internet access and is not published on the host. To use your own PostgreSQL server instead, set PASSBOX_DATABASE_URL and ideally the two other URLs; they default to PASSBOX_DATABASE_URL.
PASSBOX_DATABASE_URL=postgres://passbox_app:…@db.example.com/passbox?sslmode=verify-fullPASSBOX_MIGRATION_DATABASE_URL=postgres://passbox_migrator:…@db.example.com/passbox?sslmode=verify-fullPASSBOX_WORKER_DATABASE_URL=postgres://passbox_worker:…@db.example.com/passbox?sslmode=verify-fullBackups
The backup service makes a backup every 24 hours and keeps it for 14 days (BACKUP_INTERVAL_HOURS, BACKUP_KEEP_DAYS). Everything is encrypted with age to your public key; the private key stays offline.
- Database: a
pg_dumpin custom format, including the server secrets. - Attachments: an archive of the attachment files, which the clients have already encrypted.
- Off the machine: copy the
backupsdirectory elsewhere, for example with rsync or restic. A backup on demand:docker compose run --rm backup once.
To restore, stop Passbox and run the restore service with the private key and the time of the backup. It replaces the database and the attachments; docker compose up -d then starts everything again and migrates an older backup.
$ docker compose stop passbox backup$ BACKUP_AGE_KEY_FILE=/path/to/passbox-backup-key.txt \$ docker compose --profile restore run --rm restore 20261025T030000Z$ docker compose up -dKeep the server secrets
server_secrets table invalidates all logins. Users cannot sign in anymore, although their data stays intact. The backups include it; test a restore from time to time.Updating
Make a backup, then pull the new images and restart. The migrate service applies database migrations before Passbox starts. Migrations are not reversed; to go back, restore the backup. For production, pin a version in PASSBOX_IMAGE.
$ docker compose pull && docker compose up -d