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):

VariableDefaultDescription
RATE_LIMIT_LOGIN_IP20Login attempts per 15 minutes per IP
RATE_LIMIT_LOGIN_EMAIL10Login attempts per 15 minutes per email
RATE_LIMIT_SIGNUP_IP10Signups per hour per IP
RATE_LIMIT_SIGNUP_EMAIL5Signups per hour per email
RATE_LIMIT_SIGNUP_VERIFY_REQUEST5Signup verification code requests per hour, counted independently per email and per IP
RATE_LIMIT_SIGNUP_VERIFY_MAX10Failed signup verification code guesses per email before lockout
RATE_LIMIT_SIGNUP_VERIFY_LOCK_MINUTES15Signup verification lockout duration in minutes
RATE_LIMIT_RECOVERY_REQUEST5Recovery requests per hour
RATE_LIMIT_RECOVERY_VERIFY_MAX5Recovery verification attempts before lockout
RATE_LIMIT_RECOVERY_LOCK_MINUTES15Recovery lockout duration in minutes
RATE_LIMIT_SHARE_EMAIL_VERIFY_MAX5Failed share-link email verification attempts before lockout
RATE_LIMIT_SHARE_EMAIL_VERIFY_LOCK_MINUTES15Share-link email verification lockout duration in minutes
RATE_LIMIT_AUTH_IP30Requests per 15 minutes per IP for other auth endpoints
RATE_LIMIT_ACCOUNT_MUTATION5Attempts per hour per user and operation for sensitive account mutations
RATE_LIMIT_DEVICE_REVOKE10Session revocations per 15 minutes per authenticated device
RATE_LIMIT_DEVICE_RENAME30Session 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:

PathService
/api/*Versioned API and protocol metadata
/api/v1/sync/eventsReal-time sync hints (SSE, unbuffered)
/cdn/*CDN proxy
/favicon/*Favicon service
/healthzHealth check
Everything elseWeb 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.