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.
Before You Start
Section titled “Before You Start”Complete these prerequisites:
- Install Docker Engine with the Docker Compose plugin.
- Point
buzz.example.comand*.buzz.example.comat the host. - Allow inbound TCP traffic on port
443. Port80is 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.
Configure The Deployment
Section titled “Configure The Deployment”-
Create a directory for the deployment:
Terminal window mkdir buzz && cd buzz -
Save this Compose file as
docker-compose.yml:docker-compose.yml services:traefik:image: traefik:v3.6restart: unless-stoppedcommand:- --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:/letsencryptlabels:- 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@internalserver:image: ghcr.io/infomiho/buzzstatic:0.6.1 # x-release-please-versionrestart: unless-stoppedenvironment:- 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:/dataexpose:- "8080"- "8081"healthcheck:test: ["CMD", "uv", "run", "python", "-m", "server.healthcheck"]interval: 30stimeout: 5sstart_period: 10sretries: 3labels:- 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=8080volumes:buzz-data:name: buzz_buzz-datatraefik-certs:name: buzz_traefik-certs -
Create a
.envfile next to it with at least these values:BUZZ_DOMAIN=buzz.example.comGITHUB_CLIENT_ID=your-github-client-idGITHUB_CLIENT_SECRET=your-github-client-secretCF_API_TOKEN=your-cloudflare-api-tokenACME_EMAIL=admin@example.comRestrict access with
chmod 600 .envand keep it out of source control. SetACME_EMAILto an address that can receive Let’s Encrypt notices. The configuration reference lists every variable.
Start Buzz
Section titled “Start Buzz”Pull the pinned images and start the services:
docker compose up -dThe Compose project creates two named volumes:
buzz_buzz-datastores all Buzz data at/data.buzz_traefik-certsstores Traefik’s ACME state.
Do not remove either volume during routine updates.
Verify The Deployment
Section titled “Verify The Deployment”-
Confirm that both containers are running:
Terminal window docker compose ps -
Check the server through Traefik:
Terminal window curl --fail --show-error https://buzz.example.com/healthThe response is:
{"status":"ok"} -
Open
https://buzz.example.comand 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.
Enable The Control Plane
Section titled “Enable The Control Plane”Generate a random token:
python -c 'import secrets; print(secrets.token_urlsafe(48))'Add these values to .env:
BUZZ_CUSTOM_DOMAINS_ENABLED=trueBUZZ_TRAEFIK_CONTROL_TOKEN=replace-with-the-generated-tokenBUZZ_MAX_CUSTOM_DOMAINS_PER_SITE=5BUZZ_MAX_CUSTOM_DOMAINS_PER_USER=20BUZZ_MAX_CUSTOM_DOMAINS_SERVER_WIDE=1000The 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:
docker compose up -dThe 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-customACME resolver using HTTP-01 on entrypointweb. - A protected Traefik runtime API on the private
buzz-adminentrypoint. - Runtime checks for entrypoint
websecureand servicebuzz@docker.
Verify The Control Plane
Section titled “Verify The Control Plane”Check the private readiness response:
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.
Opt Into Cloudflare Support
Section titled “Opt Into Cloudflare Support”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.
Configure Operator Endpoints
Section titled “Configure Operator Endpoints”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-transitionsreports active transitions with current generation-qualified DNS and path evidence.POST /operator/domain-transitions/{claim_id}/cancelvalidates the current effective path before retaining it and returns404for an unknown claim or409when 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.
Exercise Staging Routing
Section titled “Exercise Staging Routing”To exercise staging-only exact-host routing, add this value and recreate the services:
BUZZ_CUSTOM_DOMAIN_ROUTING_ENABLED=trueThe 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:
docker compose logs server traefikSee Troubleshoot Self-Hosting for common causes.
Update Or Roll Back
Section titled “Update Or Roll Back”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:
docker compose pulldocker compose up -dConfirm the new release is serving:
curl --fail --show-error https://buzz.example.com/versionDatabase 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.
Choose An Image Tag
Section titled “Choose An Image Tag”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.