Docker Compose Setup
Deploy Bittery on any Linux server with Docker Compose. The stack includes Caddy for automatic HTTPS via Let's Encrypt.
Quick install
The installer script handles everything — downloading configs, generating secrets, and starting services:
curl -fsSL https://raw.githubusercontent.com/bittery-org/bittery/main/deploy/install.sh | bash
The installer will prompt you for:
- Your domain name (e.g.
bittery.example.com) - The exact Bittery release to install (e.g.
0.5.0) - Database choice — built-in PostgreSQL or external
- Install directory (default:
~/bittery)
Note
For non-interactive installs, pass flags directly:
bash install.sh --domain bittery.example.com --release 0.5.0 --install-dir /opt/bittery
Manual setup
If you prefer to set things up manually:
Create a directory
mkdir -p ~/bittery && cd ~/bittery
Download the configuration files
curl -fsSL https://raw.githubusercontent.com/bittery-org/bittery/main/deploy/docker/docker-compose.yml -o docker-compose.yml
curl -fsSL https://raw.githubusercontent.com/bittery-org/bittery/main/deploy/docker/Caddyfile -o Caddyfile
Create an .env file
Generate secrets and configure your domain:
cat > .env << 'EOF'
DOMAIN=bittery.example.com
BITTERY_RELEASE=0.5.0
BITTERY_MODE=self-hosted
JWT_SECRET=$(openssl rand -hex 32)
DB_PASSWORD=$(openssl rand -hex 16)
EOF
Replace bittery.example.com with your actual domain.
Point DNS to your server
Create an A record for your domain pointing to your server's public IP address. Caddy will automatically provision a TLS certificate once DNS propagates.
Start the services
With built-in PostgreSQL:
COMPOSE_PROFILES=builtin-db docker compose up -d
With an external database:
# Add to .env: DATABASE_URL=postgresql://user:pass@host:5432/bittery
docker compose up -d
Verify
docker compose ps
All services should show (healthy). Open https://your-domain.com to create your first account.
Enabling optional services
File storage (MinIO)
Enable S3-compatible storage for file attachments:
# Add to .env
MINIO_ROOT_PASSWORD=your-secure-password
BITTERY_STORAGE_ENDPOINT=http://minio:9000
BITTERY_STORAGE_BUCKET=bittery
BITTERY_STORAGE_ACCESS_KEY_ID=bittery
BITTERY_STORAGE_SECRET_ACCESS_KEY=your-secure-password
COMPOSE_PROFILES=builtin-db,storage docker compose up -d
Cache (Valkey)
Enable Redis-compatible caching for rate limiting and pub/sub:
# Add to .env
REDIS_URL=valkey://valkey:6379
RATE_LIMIT_ADAPTER=auto
RATE_LIMIT_REDIS_URL=valkey://valkey:6379
COMPOSE_PROFILES=builtin-db,cache docker compose up -d
RATE_LIMIT_ADAPTER accepts auto, postgres, or redis. With auto (the default) the server uses Redis when RATE_LIMIT_REDIS_URL is set and falls back to Postgres otherwise, so rate limiting works even without the cache profile. Setting redis requires RATE_LIMIT_REDIS_URL and fails at startup if it is missing. There is no in-memory backend.
The per-endpoint limits can be tuned via optional environment variables (the time windows are fixed in code):
| Variable | Default | Description |
|---|---|---|
RATE_LIMIT_LOGIN_IP | 20 | Login attempts per 15 minutes per IP |
RATE_LIMIT_LOGIN_EMAIL | 10 | Login attempts per 15 minutes per email |
RATE_LIMIT_SIGNUP_IP | 10 | Signups per hour per IP |
RATE_LIMIT_SIGNUP_EMAIL | 5 | Signups per hour per email |
RATE_LIMIT_SIGNUP_VERIFY_REQUEST | 5 | Signup verification code requests per hour, counted independently per email and per IP |
RATE_LIMIT_SIGNUP_VERIFY_MAX | 10 | Failed signup verification code guesses per email before lockout |
RATE_LIMIT_SIGNUP_VERIFY_LOCK_MINUTES | 15 | Signup verification lockout duration in minutes |
RATE_LIMIT_RECOVERY_REQUEST | 5 | Recovery requests per hour |
RATE_LIMIT_RECOVERY_VERIFY_MAX | 5 | Recovery verification attempts before lockout |
RATE_LIMIT_RECOVERY_LOCK_MINUTES | 15 | Recovery lockout duration in minutes |
RATE_LIMIT_SHARE_EMAIL_VERIFY_MAX | 5 | Failed share-link email verification attempts before lockout |
RATE_LIMIT_SHARE_EMAIL_VERIFY_LOCK_MINUTES | 15 | Share-link email verification lockout duration in minutes |
RATE_LIMIT_AUTH_IP | 30 | Requests per 15 minutes per IP for other auth endpoints |
RATE_LIMIT_ACCOUNT_MUTATION | 5 | Attempts per hour per user and operation for sensitive account mutations |
RATE_LIMIT_DEVICE_REVOKE | 10 | Session revocations per 15 minutes per authenticated device |
RATE_LIMIT_DEVICE_RENAME | 30 | Session renames per 15 minutes per authenticated device |
RATE_LIMIT_SIGNUP_VERIFY_MAX is keyed on the email address and requesting a new verification code does not reset it, so a user who exhausts their guesses stays locked out for RATE_LIMIT_SIGNUP_VERIFY_LOCK_MINUTES regardless of how many fresh codes they send themselves. See Configuration Reference for the full explanation.
Reverse proxy
The included Caddy configuration routes traffic as follows:
| Path | Service |
|---|---|
/api/* | Versioned API and protocol metadata |
/api/v1/sync/events | Real-time sync hints (SSE, unbuffered) |
/cdn/* | CDN proxy |
/favicon/* | Favicon service |
/healthz | Health check |
| Everything else | Web SPA |
Warning
If you use your own reverse proxy instead of Caddy, make sure to set TRUST_PROXY_MODE appropriately. Valid values are none, cloudflare, or forwarded.
The public deployment must use HTTPS. Non-loopback HTTP is an explicit insecure mode for exceptional private-network deployments: the operator must enable it for the server, and each client account must separately confirm it. An operator setting alone does not authorize clients to send sessions over plaintext HTTP. Internal container traffic such as Caddy-to-server and server-to-MinIO may remain HTTP when it is confined to the deployment's private network.
The BITTERY_RELEASE value pins the server and web images to the same release. Change that one
value to upgrade or roll back the pair; do not replace it with latest on a durable install.
Backups
Database
If using the built-in PostgreSQL, back up the database regularly:
docker compose exec postgres pg_dump -U bittery bittery > backup-$(date +%Y%m%d).sql
Restoring
docker compose exec -T postgres psql -U bittery bittery < backup-20250101.sql
Tip
For production deployments, consider using an external managed PostgreSQL service with automated backups.