Skip to content

Self-Host with Docker Compose

BeeBuzz publishes the beebuzz-server and beebuzz-web images to the Codeberg Container Registry. The examples use the latest tag for convenience; replace it with a specific release tag for reproducible deployments.

Both recipes use Docker Compose and include Caddy as the example reverse proxy. The gateway publishes ports 80 and 443 by default. If you already run a reverse proxy, or use different host ports, adapt or replace the gateway service and point it at the server and web services.

BeeBuzz needs a stable HTTPS domain for its browser applications. The dashboard and Hive use secure browser origins for authentication, service workers, and Web Push, while the API and email links must point to the same instance. The example uses three subdomains under one domain:

  • dashboard.example.com for the account application
  • hive.example.com for receiving notifications
  • api.example.com for the API

For a local test without your own domain, use the local testing recipe below.

Create an empty directory and save these three files in it.

name: beebuzz
services:
server:
image: codeberg.org/beebuzz/beebuzz-server:latest
restart: unless-stopped
env_file: .env
environment:
BEEBUZZ_ENV: production
BEEBUZZ_DEPLOYMENT_MODE: self_hosted
BEEBUZZ_DOMAIN: ${BEEBUZZ_DOMAIN:?Set BEEBUZZ_DOMAIN in .env}
BEEBUZZ_PROXY_SUBNET: 172.30.0.0/24
BEEBUZZ_DB_DIR: /var/lib/beebuzz/db
BEEBUZZ_ATTACHMENTS_DIR: /var/lib/beebuzz/attachments
volumes:
- database:/var/lib/beebuzz/db
- attachments:/var/lib/beebuzz/attachments
networks: [beebuzz]
web:
image: codeberg.org/beebuzz/beebuzz-web:latest
restart: unless-stopped
environment:
BEEBUZZ_DOMAIN: ${BEEBUZZ_DOMAIN:?Set BEEBUZZ_DOMAIN in .env}
networks: [beebuzz]
gateway:
image: caddy:2.11-alpine
restart: unless-stopped
depends_on: [server, web]
environment:
BEEBUZZ_DOMAIN: ${BEEBUZZ_DOMAIN:?Set BEEBUZZ_DOMAIN in .env}
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks: [beebuzz]
networks:
beebuzz:
ipam:
config:
- subnet: 172.30.0.0/24
volumes:
database:
attachments:
caddy_data:
caddy_config:
BEEBUZZ_DOMAIN=example.com
BEEBUZZ_BOOTSTRAP_ADMIN_EMAIL=admin@example.com
BEEBUZZ_IP_HASH_SALT=
BEEBUZZ_VAPID_PRIVATE_KEY=
BEEBUZZ_VAPID_PUBLIC_KEY=
BEEBUZZ_MAILER_SENDER=noreply@example.com
BEEBUZZ_MAILER_REPLY_TO=support@example.com
BEEBUZZ_MAILER_SMTP_ADDRESS=smtp.example.com:587
BEEBUZZ_MAILER_SMTP_USER=
BEEBUZZ_MAILER_SMTP_PASSWORD=
# Or use BEEBUZZ_MAILER_RESEND_API_KEY instead of SMTP settings.
dashboard.{$BEEBUZZ_DOMAIN} {
reverse_proxy web:80
}
hive.{$BEEBUZZ_DOMAIN} {
reverse_proxy web:80
}
api.{$BEEBUZZ_DOMAIN} {
reverse_proxy server:8899
}

Create DNS records for dashboard, hive, and api pointing to the Docker host. Generate the keys:

Terminal window
docker compose run --rm server salt generate
docker compose run --rm server vapid generate

Paste the generated values into .env, then start the stack:

Terminal window
docker compose up -d

Caddy obtains public certificates automatically.

Local testing still needs HTTPS certificates installed and trusted on every device that will open BeeBuzz. The steps below use Lancert, which provides a hostname and publicly trusted certificates for the local network. Its wildcard certificate also covers mail.<your-lancert-domain>, which BeeBuzz uses for authenticated SMTP over STARTTLS. Alternatively, create your own local certificates with a tool such as mkcert and install its local CA on every test device and in the BeeBuzz container.

Create a separate empty directory and save these three files in it.

Install the Lancert CLI, then register the private IPv4 address of the machine running Docker:

Terminal window
lancert 192.168.1.50

Lancert prints a hostname such as quiet-otter.lancert.dev and stores its certificate files locally.

name: beebuzz-local
services:
server:
image: codeberg.org/beebuzz/beebuzz-server:latest
restart: unless-stopped
depends_on: [mailpit]
env_file: .env
environment:
BEEBUZZ_ENV: production
BEEBUZZ_DEPLOYMENT_MODE: self_hosted
BEEBUZZ_DOMAIN: ${BEEBUZZ_DOMAIN:?Set BEEBUZZ_DOMAIN in .env}
BEEBUZZ_PROXY_SUBNET: 172.30.0.0/24
BEEBUZZ_DB_DIR: /var/lib/beebuzz/db
BEEBUZZ_ATTACHMENTS_DIR: /var/lib/beebuzz/attachments
BEEBUZZ_MAILER_SMTP_ADDRESS: mail.${BEEBUZZ_DOMAIN}:1025
volumes:
- database:/var/lib/beebuzz/db
- attachments:/var/lib/beebuzz/attachments
networks: [beebuzz]
web:
image: codeberg.org/beebuzz/beebuzz-web:latest
restart: unless-stopped
environment:
BEEBUZZ_DOMAIN: ${BEEBUZZ_DOMAIN:?Set BEEBUZZ_DOMAIN in .env}
networks: [beebuzz]
gateway:
image: caddy:2.11-alpine
restart: unless-stopped
depends_on: [server, web]
environment:
BEEBUZZ_DOMAIN: ${BEEBUZZ_DOMAIN:?Set BEEBUZZ_DOMAIN in .env}
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ${BEEBUZZ_CERTS_DIR:?Set BEEBUZZ_CERTS_DIR in .env}:/certs:ro
- caddy_data:/data
- caddy_config:/config
networks: [beebuzz]
mailpit:
image: axllent/mailpit:latest
restart: unless-stopped
environment:
MP_SMTP_AUTH: dev:dev
MP_SMTP_REQUIRE_STARTTLS: "true"
MP_SMTP_TLS_CERT: /certs/fullchain.pem
MP_SMTP_TLS_KEY: /certs/privkey.pem
ports:
- "127.0.0.1:8025:8025"
volumes:
- ${BEEBUZZ_CERTS_DIR:?Set BEEBUZZ_CERTS_DIR in .env}:/certs:ro
networks:
beebuzz:
aliases:
- mail.${BEEBUZZ_DOMAIN}
networks:
beebuzz:
ipam:
config:
- subnet: 172.30.0.0/24
volumes:
database:
attachments:
caddy_data:
caddy_config:
BEEBUZZ_DOMAIN=quiet-otter.lancert.dev
BEEBUZZ_CERTS_DIR=/absolute/path/to/lancert/certs/quiet-otter.lancert.dev
BEEBUZZ_BOOTSTRAP_ADMIN_EMAIL=admin@example.com
BEEBUZZ_IP_HASH_SALT=
BEEBUZZ_VAPID_PRIVATE_KEY=
BEEBUZZ_VAPID_PUBLIC_KEY=
BEEBUZZ_MAILER_SMTP_USER=dev
BEEBUZZ_MAILER_SMTP_PASSWORD=dev
BEEBUZZ_MAILER_SENDER=noreply@example.com
BEEBUZZ_MAILER_REPLY_TO=support@example.com

The Compose file routes BeeBuzz to mail.<BEEBUZZ_DOMAIN> and configures Mailpit with the Lancert certificate from BEEBUZZ_CERTS_DIR. SMTP authentication requires STARTTLS; port 1025 is available only on the private Compose network. The Mailpit web UI is published only on localhost at http://localhost:8025.

(lancert_tls) {
tls /certs/fullchain.pem /certs/privkey.pem
}
dashboard.{$BEEBUZZ_DOMAIN} {
import lancert_tls
reverse_proxy web:80
}
hive.{$BEEBUZZ_DOMAIN} {
import lancert_tls
reverse_proxy web:80
}
api.{$BEEBUZZ_DOMAIN} {
import lancert_tls
reverse_proxy server:8899
}

Generate the keys from this directory:

Terminal window
docker compose run --rm server salt generate
docker compose run --rm server vapid generate

Paste the generated values into .env, then start the local stack:

Terminal window
docker compose up -d

Open https://quiet-otter.lancert.dev and Mailpit at http://localhost:8025.

From the directory containing the selected recipe, pull the latest images and recreate the containers:

Terminal window
docker compose pull
docker compose up -d

BeeBuzz applies schema migrations automatically on startup.

This minimal configuration omits the optional push and hook hostnames. Use the API paths instead:

Terminal window
curl https://api.example.com/v1/push \
-H "Authorization: Bearer $TOKEN" \
-F title="Hello from BeeBuzz" \
-F body="Self-hosted delivery test"

Set the CLI endpoint before connecting:

Terminal window
export BEEBUZZ_API_URL=https://api.example.com
beebuzz connect

Docker stores BeeBuzz data in the database and attachments volumes. Back up both volumes and the .env file. Stop the server before taking a file-level copy of the database volume:

Terminal window
docker compose stop server
# copy the volume data
docker compose start server