Skip to content

Deploy With Docker Compose

Deploy Buzz with the Docker Compose file on this page. It runs the published Buzz server image and Traefik on the same host and obtains a wildcard certificate through Cloudflare DNS.

The Compose file pins the exact current server release, ghcr.io/infomiho/buzzstatic:<version>. Every release updates the pin, so this page always shows the newest version.

Complete these prerequisites:

  • Install Docker Engine with the Docker Compose plugin.
  • Point buzz.example.com and *.buzz.example.com at the host.
  • Allow inbound TCP traffic on port 443. Port 80 is also required when the optional custom-domain HTTP-01 resolver is enabled; otherwise it is used only for the bundled HTTP-to-HTTPS redirect.
  • Create a Cloudflare API token and a GitHub OAuth app.
  • Complete the access decision in the Self-Hosting Overview. Configure upstream controls before deployment if Buzz is for a closed group.

Follow Configure DNS And TLS and Configure GitHub Authentication for the required credentials.

  1. Create a directory for the deployment:

    Terminal window
    mkdir buzz && cd buzz
  2. Save this Compose file as docker-compose.yml:

    docker-compose.yml
    services:
    traefik:
    image: traefik:v3.6
    restart: unless-stopped
    command:
    - --providers.docker=true
    - --providers.docker.exposedbydefault=false
    - --providers.http=${BUZZ_CUSTOM_DOMAINS_ENABLED:-false}
    - --providers.http.endpoint=http://server:8081/traefik
    - "--providers.http.headers.Authorization=Bearer ${BUZZ_TRAEFIK_CONTROL_TOKEN:-}"
    - --providers.http.pollInterval=5s
    - --providers.http.pollTimeout=2s
    - --providers.http.maxResponseBodySize=1048576
    - --api=true
    - --entrypoints.web.address=:80
    - --entrypoints.web.http.redirections.entrypoint.to=websecure
    - --entrypoints.web.http.redirections.entrypoint.scheme=https
    - --entrypoints.websecure.address=:443
    - --entrypoints.buzz-admin.address=:8082
    - --certificatesresolvers.cloudflare.acme.email=${ACME_EMAIL}
    - --certificatesresolvers.cloudflare.acme.storage=/letsencrypt/acme.json
    - --certificatesresolvers.cloudflare.acme.dnschallenge.provider=cloudflare
    - --certificatesresolvers.cloudflare.acme.dnschallenge.resolvers=1.1.1.1:53,8.8.8.8:53
    - --certificatesresolvers.buzz-custom.acme.email=${ACME_EMAIL}
    - --certificatesresolvers.buzz-custom.acme.storage=/letsencrypt/acme.json
    - --certificatesresolvers.buzz-custom.acme.httpchallenge.entrypoint=web
    - --certificatesresolvers.buzz-custom.acme.caserver=${BUZZ_CUSTOM_DOMAIN_ACME_CA_SERVER:-https://acme-staging-v02.api.letsencrypt.org/directory}
    environment:
    - CF_DNS_API_TOKEN=${CF_API_TOKEN}
    ports:
    - "80:80"
    - "443:443"
    volumes:
    - /var/run/docker.sock:/var/run/docker.sock:ro
    - traefik-certs:/letsencrypt
    labels:
    - traefik.enable=true
    - traefik.http.routers.wildcard-certs.tls.certresolver=cloudflare
    - traefik.http.routers.wildcard-certs.tls.domains[0].main=${BUZZ_DOMAIN}
    - traefik.http.routers.wildcard-certs.tls.domains[0].sans=*.${BUZZ_DOMAIN}
    - "traefik.http.routers.buzz-runtime-api.rule=PathPrefix(`/api`) && Header(`Authorization`, `Bearer ${BUZZ_TRAEFIK_CONTROL_TOKEN:-}`)"
    - traefik.http.routers.buzz-runtime-api.entrypoints=buzz-admin
    - traefik.http.routers.buzz-runtime-api.service=api@internal
    server:
    image: ghcr.io/infomiho/buzzstatic:0.6.1 # x-release-please-version
    restart: unless-stopped
    environment:
    - BUZZ_DATA_DIR=/data
    - BUZZ_DOMAIN=${BUZZ_DOMAIN}
    - GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
    - GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET}
    - BUZZ_ANALYTICS_SECRET=${BUZZ_ANALYTICS_SECRET:-}
    - BUZZ_MAX_ARCHIVE_BYTES=${BUZZ_MAX_ARCHIVE_BYTES:-524288000}
    - BUZZ_MAX_SITE_BYTES=${BUZZ_MAX_SITE_BYTES:-524288000}
    - BUZZ_MAX_SITE_FILES=${BUZZ_MAX_SITE_FILES:-10000}
    - BUZZ_MAX_ARCHIVE_PATH_BYTES=${BUZZ_MAX_ARCHIVE_PATH_BYTES:-1024}
    - BUZZ_ALLOW_REGISTRATION=${BUZZ_ALLOW_REGISTRATION:-true}
    - BUZZ_ALLOWED_GITHUB_USERS=${BUZZ_ALLOWED_GITHUB_USERS:-}
    - BUZZ_CUSTOM_DOMAINS_ENABLED=${BUZZ_CUSTOM_DOMAINS_ENABLED:-false}
    - BUZZ_TRAEFIK_CONTROL_TOKEN=${BUZZ_TRAEFIK_CONTROL_TOKEN:-}
    - BUZZ_TRAEFIK_API_URL=http://traefik:8082/api
    - "BUZZ_TRAEFIK_API_AUTHORIZATION=Bearer ${BUZZ_TRAEFIK_CONTROL_TOKEN:-}"
    - BUZZ_TRAEFIK_HTTPS_ENTRYPOINT=websecure
    - BUZZ_CUSTOM_DOMAIN_ROUTING_ENABLED=${BUZZ_CUSTOM_DOMAIN_ROUTING_ENABLED:-false}
    - BUZZ_CUSTOM_DOMAIN_ADMISSION_ENABLED=${BUZZ_CUSTOM_DOMAIN_ADMISSION_ENABLED:-false}
    - BUZZ_CLOUDFLARE_DIAGNOSTICS_ENABLED=${BUZZ_CLOUDFLARE_DIAGNOSTICS_ENABLED:-false}
    - BUZZ_CLOUDFLARE_ACTIVATION_ENABLED=${BUZZ_CLOUDFLARE_ACTIVATION_ENABLED:-false}
    - BUZZ_AUTOMATIC_DOMAIN_TRANSITION_ADMISSION_ENABLED=${BUZZ_AUTOMATIC_DOMAIN_TRANSITION_ADMISSION_ENABLED:-false}
    - BUZZ_CUSTOM_DOMAIN_OPERATOR_TOKEN=${BUZZ_CUSTOM_DOMAIN_OPERATOR_TOKEN:-}
    - BUZZ_MAX_CUSTOM_DOMAINS_PER_SITE=${BUZZ_MAX_CUSTOM_DOMAINS_PER_SITE:-5}
    - BUZZ_MAX_CUSTOM_DOMAINS_PER_USER=${BUZZ_MAX_CUSTOM_DOMAINS_PER_USER:-20}
    - BUZZ_MAX_CUSTOM_DOMAINS_SERVER_WIDE=${BUZZ_MAX_CUSTOM_DOMAINS_SERVER_WIDE:-1000}
    - BUZZ_CUSTOM_DOMAIN_INGRESS_IPS=${BUZZ_CUSTOM_DOMAIN_INGRESS_IPS:-}
    - BUZZ_CUSTOM_DOMAIN_ORIGIN_HOST=${BUZZ_CUSTOM_DOMAIN_ORIGIN_HOST:-traefik}
    - BUZZ_TRAEFIK_CERT_RESOLVER=${BUZZ_TRAEFIK_CERT_RESOLVER:-buzz-custom}
    - BUZZ_CUSTOM_DOMAIN_RECONCILE_SECONDS=${BUZZ_CUSTOM_DOMAIN_RECONCILE_SECONDS:-5}
    volumes:
    - buzz-data:/data
    expose:
    - "8080"
    - "8081"
    healthcheck:
    test: ["CMD", "uv", "run", "python", "-m", "server.healthcheck"]
    interval: 30s
    timeout: 5s
    start_period: 10s
    retries: 3
    labels:
    - traefik.enable=true
    - traefik.http.routers.buzz.rule=Host(`${BUZZ_DOMAIN}`)
    - traefik.http.routers.buzz.entrypoints=websecure
    - traefik.http.routers.buzz.tls=true
    - traefik.http.routers.buzz-wildcard.rule=HostRegexp(`.+\.${BUZZ_DOMAIN}`)
    - traefik.http.routers.buzz-wildcard.entrypoints=websecure
    - traefik.http.routers.buzz-wildcard.tls=true
    - traefik.http.services.buzz.loadbalancer.server.port=8080
    volumes:
    buzz-data:
    name: buzz_buzz-data
    traefik-certs:
    name: buzz_traefik-certs
  3. Create a .env file next to it with at least these values:

    BUZZ_DOMAIN=buzz.example.com
    GITHUB_CLIENT_ID=your-github-client-id
    GITHUB_CLIENT_SECRET=your-github-client-secret
    CF_API_TOKEN=your-cloudflare-api-token
    ACME_EMAIL=admin@example.com

    Restrict access with chmod 600 .env and keep it out of source control. Set ACME_EMAIL to an address that can receive Let’s Encrypt notices. The configuration reference lists every variable.

Pull the pinned images and start the services:

Terminal window
docker compose up -d

The Compose project creates two named volumes:

  • buzz_buzz-data stores all Buzz data at /data.
  • buzz_traefik-certs stores Traefik’s ACME state.

Do not remove either volume during routine updates.

  1. Confirm that both containers are running:

    Terminal window
    docker compose ps
  2. Check the server through Traefik:

    Terminal window
    curl --fail --show-error https://buzz.example.com/health

    The response is:

    {"status":"ok"}
  3. Open https://buzz.example.com and start a GitHub sign-in.

Prepare The Optional Custom Domain Control Plane

Section titled “Prepare The Optional Custom Domain Control Plane”

Custom domains are disabled by default. Skip this section when the operator does not want Buzz to manage custom domains.

Generate a random token:

Terminal window
python -c 'import secrets; print(secrets.token_urlsafe(48))'

Add these values to .env:

BUZZ_CUSTOM_DOMAINS_ENABLED=true
BUZZ_TRAEFIK_CONTROL_TOKEN=replace-with-the-generated-token
BUZZ_MAX_CUSTOM_DOMAINS_PER_SITE=5
BUZZ_MAX_CUSTOM_DOMAINS_PER_USER=20
BUZZ_MAX_CUSTOM_DOMAINS_SERVER_WIDE=1000

The quota settings limit pending and verified aliases per site, per user, and across the server. Each alias is reconciled and removed independently; aliases awaiting acknowledged withdrawal continue to consume quota.

Recreate the services:

Terminal window
docker compose up -d

The bundled Compose configuration then enables Traefik’s HTTP provider, which polls the private Buzz listener on port 8081. The port is exposed only to the Compose network. It also prepares:

  • The buzz-custom ACME resolver using HTTP-01 on entrypoint web.
  • A protected Traefik runtime API on the private buzz-admin entrypoint.
  • Runtime checks for entrypoint websecure and service buzz@docker.

Check the private readiness response:

Terminal window
docker compose exec server uv run python -c 'import json,os,urllib.request; token=os.environ["BUZZ_TRAEFIK_CONTROL_TOKEN"]; request=urllib.request.Request("http://localhost:8081/ready",headers={"Authorization":f"Bearer {token}"}); print(json.dumps(json.load(urllib.request.urlopen(request)),indent=2))'

This stage can confirm provider polling, runtime API access, buzz@docker, and the HTTPS entrypoint. It cannot prove that the unused ACME resolver can issue certificates. Certificate issuance and ACME storage are exercised later with a staging hostname.

Two flags build on the control plane, in order:

Variable Turns on Set it
BUZZ_CLOUDFLARE_DIAGNOSTICS_ENABLED Admission of credential-free Cloudflare proxy claims First
BUZZ_CLOUDFLARE_ACTIVATION_ENABLED Content serving for healthy Cloudflare claims After controlled-zone verification

Diagnostics checks persistent TXT ownership, Cloudflare addresses, edge TLS, challenge forwarding, and Full (strict) origin behavior. Bypass redirects, caching, WAF, Workers, Access, and challenges on Buzz and ACME verification paths.

Opt Into Automatic Connection-Path Detection

Section titled “Opt Into Automatic Connection-Path Detection”

Automatic connection-path detection defaults off. It is available only when direct routing and Cloudflare diagnostics and activation are all ready, including current Cloudflare ranges and the transition runtime. After validating existing direct and Cloudflare claims, set BUZZ_AUTOMATIC_DOMAIN_TRANSITION_ADMISSION_ENABLED=true to let Buzz detect and validate supported DNS path changes without replacing the domain claim. Closing admission prevents new transitions while active transitions continue to completion, cancellation, or their deadline.

Set BUZZ_CUSTOM_DOMAIN_OPERATOR_TOKEN to one or more dedicated bearer tokens separated by commas. On the private control port 8081:

  • GET /operator/domain-transitions reports active transitions with current generation-qualified DNS and path evidence.
  • POST /operator/domain-transitions/{claim_id}/cancel validates the current effective path before retaining it and returns 404 for an unknown claim or 409 when cancellation is unsafe or loses a race.

Operator routes authenticate before route or method handling and return no-store JSON errors. Do not reuse BUZZ_TRAEFIK_CONTROL_TOKEN, expose these endpoints through the public app, or log/send the token outside the authorization header.

To exercise staging-only exact-host routing, add this value and recreate the services:

BUZZ_CUSTOM_DOMAIN_ROUTING_ENABLED=true

The bundled buzz-custom resolver defaults to Let’s Encrypt’s staging directory through BUZZ_CUSTOM_DOMAIN_ACME_CA_SERVER. Add and TXT-verify a hostname from its site detail page, wait for router acknowledgement, then open the displayed verification URL. Only that reserved verification path is available on the custom hostname in this stage.

Set BUZZ_CUSTOM_DOMAIN_ROUTING_ENABLED=false to withdraw staging routers. Wait for acknowledged withdrawal before disabling the custom-domain control plane. Do not change BUZZ_CUSTOM_DOMAIN_ACME_CA_SERVER to production until the production custom-domain stage is implemented and verified.

If a container exits or TLS isn’t issued, inspect the logs:

Terminal window
docker compose logs server traefik

See Troubleshoot Self-Hosting for common causes.

Create a cold backup before an update and read the release notes for the versions you skip past. Update the image: tag in docker-compose.yml to the new release, or copy the Compose file above again when the release notes mention deployment changes. Then recreate the services:

Terminal window
docker compose pull
docker compose up -d

Confirm the new release is serving:

Terminal window
curl --fail --show-error https://buzz.example.com/version

Database migrations run automatically when the new server starts, and the instance is unavailable while they run. Migrations are forward-only: if the newer release added one, an older image refuses to start against the migrated database and exits. To roll back, stop the stack, restore the pre-update backup, set the image: tag back to the previous release, and start the stack again.

The published image carries several tags:

Tag Meaning Use
<major>.<minor>.<patch> Exact release, immutable Recommended. The Compose file above pins this form.
<major>.<minor> Latest patch release in the series Unattended patch updates.
latest Latest release Not recommended. Deployments stop being reproducible.

To automate updates, commit the deployment directory (without .env) to a repository you control. The tag stays inline in docker-compose.yml, so Renovate’s docker-compose manager opens a pull request per release, including the release notes. For notification-only updates, point Diun at the running containers. Avoid tools that auto-pull and restart running containers: an unattended update can run a database migration you did not plan for.