Self-host Backu with Docker
This guide sets up a small production-style Backu installation with Docker Compose. It includes:
- Backu's gateway and web app
- PostgreSQL for accounts, sessions, and server identity
- MinIO for S3-compatible object storage
- Caddy for HTTPS certificates and reverse proxying
- persistent storage, health checks, backups, and upgrades
The examples are written for an IT administrator running Backu on one Linux server. You can swap Caddy or MinIO for services you already operate.
Before you begin
Backu currently suits a trusted team or household. Anyone who can reach the sign-up page and verify an email address can create an account, and verified accounts can access the machines connected to this Backu server. There is not yet an invite-only mode or per-user machine isolation.
Put the service on a private network or behind your existing access-control layer unless open registration is acceptable. Only connect machines whose files may be accessed by every verified user on this server.
What you need
- A Linux server with a supported version of Docker Engine and the Docker Compose plugin
- Git, so you can build the current Backu image
- At least 2 CPU cores, 4 GB of memory, and enough disk space for PostgreSQL, uploads, and backups
- Two stable DNS names pointing to the server, for example:
backu.example.comfor Backustorage.example.comfor the MinIO S3 API
- Inbound TCP ports 80 and 443 open to the networks that will use Backu
- Outbound internet access for container builds, email delivery, and Iroh peer discovery and relay connections
- A Resend API key and a verified sender address
The storage name must be reachable from every machine that uploads files. Backu gives clients short-lived MinIO URLs, so a Docker-only name such as minio:9000 will not work as BACKU_S3_ENDPOINT.
Use a stable Backu hostname from the start. Passkeys are tied to the hostname, and changing it later can prevent existing passkeys from signing in.
1. Prepare the server
Create a working directory and clone Backu into it:
The repository is currently private, so Git will ask for credentials from an account with access.
Create four strong, unrelated secrets. A password manager is the best place to keep the long-term copy.
Create /opt/backu/.env and replace every example value:
Lock down the file because it contains credentials:
Do not commit this file to Git or copy its values into tickets and chat messages.
2. Configure MinIO permissions
MinIO should keep the bucket private. Backu only needs access to its own bucket and multipart uploads.
Create /opt/backu/minio-policy.json:
This policy assumes the bucket is named backu. If you change BACKU_S3_BUCKET, update both resource names in the policy too.
3. Create the Docker Compose stack
Create /opt/backu/compose.yaml:
For predictable upgrades, replace the latest MinIO tags with a release you have tested and update them deliberately.
4. Add HTTPS
Create /opt/backu/Caddyfile:
Caddy will request public TLS certificates automatically. Both DNS names must resolve to the server, and ports 80 and 443 must reach Caddy.
If you use an existing load balancer or reverse proxy, send the Backu hostname to port 3000 and the storage hostname to MinIO port 9000. Keep both backend ports private. HTTPS is required for passkeys outside localhost.
5. Start Backu
Build and start the stack:
The first build can take several minutes. Follow startup logs with:
Check the public health endpoint:
A healthy response looks like this:
If emailConfigured is false, check both Resend variables before inviting users. Do not enable BACKU_DEV_EMAIL_LINKS in production: it exposes account links in API responses and logs.
Open https://backu.example.com/dash/, create the first account, register a passkey, and follow the email verification link.
6. Connect a machine
Install or build the Backu daemon on the machine that owns the files. Set the same enrollment secret used by the server, then name each folder you want to expose:
Run the daemon as a dedicated operating-system user and grant it read access only to the folders Backu should expose. Keep its data directory persistent because it contains the machine identity. The web app should show the machine as soon as the daemon connects.
The daemon and server use Iroh/QUIC. They try a direct encrypted path and can fall back to relay infrastructure, so you normally do not need to forward a separate inbound peer-to-peer port.
Day-two operations
Back up the right data
Back up the state in all three storage services:
- PostgreSQL contains accounts, sessions, one-time tokens, and the signing and Iroh server identities.
backu-datacontains locally synced photos.minio-datacontains uploaded objects.
Also back up /opt/backu/.env, compose.yaml, Caddyfile, and minio-policy.json in your secrets-management or configuration-backup system.
Use pg_dump for a transactionally consistent PostgreSQL backup. For example:
Also snapshot backu-data and minio-data, or stop their writers briefly while taking file-level copies. Test the PostgreSQL dump and volume restores together on another host. A backup is only useful once you know it restores.
The PostgreSQL backup is especially important. Recreating the database changes signing keys and the Iroh server identity, invalidates sessions, and can require machines to be paired again.
Upgrade
Read the release notes and take a backup first. Then rebuild from the version you intend to run:
Pinning a tested tag or commit makes rollbacks and audits much easier than following the repository's default branch.
Monitor
At minimum, monitor:
https://backu.example.com/api/healthfor a successful response- free space for Docker volumes and the host filesystem
- PostgreSQL availability, storage growth, and backup freshness
- container restart counts and Backu, Caddy, and MinIO logs
- certificate-renewal errors
- whether
emailConfiguredremainstrue
The machines value in the health response is the number of machines currently known to the running server. Alerting on it can be useful, but remember that planned maintenance or sleeping devices may reduce it.
Open the MinIO console safely
The Compose file binds the MinIO console to localhost only. Reach it through an SSH tunnel instead of publishing it to the internet:
Then open http://localhost:9001 and sign in with the MinIO administrator credentials from .env.
Troubleshooting
Passkey registration fails
Confirm you are using the exact URL in BACKU_PUBLIC_URL, that it is HTTPS, and that its certificate is trusted. Check that a proxy is not changing the browser-visible hostname. If you set BACKU_PASSKEY_RP_ID, it must be the same hostname or a registrable parent domain you control.
Account creation says email is not configured
Both BACKU_RESEND_API_KEY and BACKU_EMAIL_FROM must be present. The sender domain must be verified in Resend. Check docker compose logs backu for a rejected email request.
Uploads cannot reach object storage
Test https://storage.example.com from the uploading machine, not just from the Docker host. Confirm that BACKU_S3_ENDPOINT uses that public or private-network DNS name and that path-style access is enabled for this MinIO setup.
Do not make the bucket public. Backu signs short-lived upload requests with the restricted MinIO account.
The web page returns a gateway error
Make sure BACKU_BIND=0.0.0.0:3000 is present. Without it, the server listens only inside its own container. Then inspect:
Backu cannot connect to PostgreSQL
Check that DATABASE_URL is present and that its password matches POSTGRES_PASSWORD. Then inspect docker compose ps postgres and docker compose logs postgres backu. Backu runs embedded migrations at startup, so its database user must own the backu database and be allowed to create and alter tables and indexes.
A machine cannot connect
Check that its --server-endpoint is the public Backu URL and that its --token exactly matches BACKU_TOKEN on the server. Confirm outbound HTTPS and UDP are allowed. Preserve the daemon's data directory across restarts so it keeps the same identity.
Replace MinIO with an existing S3 service
You can remove the minio and minio-init services and point Backu at an existing private bucket. Set:
Path-style access is common for self-hosted S3 services. AWS S3 and some hosted providers use virtual-host style instead; omit BACKU_S3_PATH_STYLE in that case. Give the credentials only multipart-upload and object permissions for the selected bucket.