Docker Compose bootstrap for running Dispatch, Netflix's incident and signal management platform. This repository contains no application code. It's the shell, Compose, and configuration needed to build and run Dispatch, nothing more. To change how Dispatch itself behaves, see the main project; to change how it's built, configured, or deployed, you're in the right place.
Netflix/dispatch was archived and made read-only on September 1, 2025. It no longer accepts issues, pull requests, or changes of any kind.
This repository (a fork of Netflix/dispatch-docker) is maintained independently of Netflix. It may diverge from upstream, including with breaking changes, to fix security issues, update outdated components, and adapt the deployment to our own use of the product. Don't expect drop-in compatibility with the archived upstream project going forward.
The application itself is built from Jamyn/dispatch, a fork of Netflix's archived app repo, pinned to a fixed commit in docker-compose.yml. As of the main migration, that pin is our fork's main (release v26.08.10), upstream's final state before archival, 457 commits past the old latest-based pin, plus fixes for the packaging breakage that commit jump introduced and the same base-OS/Postgres-client/mjml rework the old pin carried.
- Docker Engine 25.0.0+
- Docker Compose V2 2.0.0+ (invoked as
docker compose; Compose V1's standalonedocker-composeis not supported) - At least 2400 MB of RAM available to Docker
git clone https://github.com/Jamyn/dispatch-docker.git
cd dispatch-docker
./install.sh
docker compose up -dinstall.sh creates .env and requirements.txt from their .example files if they don't already exist, checks the requirements above, generates secrets, builds the image, and initializes the database. It prompts to load sample data unless CI is set in the environment.
Once the stack is running, register the first user:
http://localhost:8000/default/auth/register
Then grant that user ownership (default is the organization name unless you loaded sample data under a different one):
docker exec -it dispatch-web-1 bash -c \
'dispatch user update --role owner --organization default you@example.com'If you need non-default configuration (e.g. Google OAuth credentials), copy .env.example to .env and edit it before running install.sh. The script won't overwrite an .env that already exists.
All configuration lives in .env (gitignored, created from .env.example). Compose loads it via env_file: on every service, so anything added to the example propagates to postgres, core, web, and scheduler automatically.
| Variable | Purpose |
|---|---|
SECRET_KEY, DISPATCH_JWT_SECRET |
Application secrets. Generated by install.sh on every run where they're still the placeholder. |
DISPATCH_ENCRYPTION_KEY |
Encrypts plugin credentials (Slack tokens, SMTP passwords, etc.) at rest. Generated once, on first install only, and it can't be rotated afterward without re-entering every plugin's config, so install.sh warns instead of touching it once data exists. |
POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB |
Read by the official postgres image to initialize the cluster. Only applied on first init; changing them later doesn't change the running server's password. |
DATABASE_CREDENTIALS |
The same Postgres credentials in user:password form, read by the application. Must stay in sync with POSTGRES_USER/POSTGRES_PASSWORD, and install.sh keeps them in sync when it generates the password itself. |
DATABASE_HOSTNAME, DATABASE_PORT, DATABASE_NAME |
How the app finds Postgres. Defaults (postgres, 5432, dispatch) match the Compose service name and shouldn't need to change. |
VITE_* |
Forwarded as frontend build args if present in .env when install.sh runs (must match ARG declarations in the upstream Dockerfile). |
COMPOSE_PROJECT_NAME |
Prefixes container and volume names. Default dispatch gives containers like dispatch-web-1, which the ownership-grant command below assumes; change the command to match if you change this. |
Extra Dispatch plugins go in requirements.txt (one pip requirement per line) and are installed into the image at build time.
See Administration guide (upstream, archived) for the full set of server configuration options Dispatch itself understands.
./install.sh # full install/upgrade: checks, secrets, build, DB init/migrate, plugins
docker compose up -d # start the stack
docker compose build # rebuild the dispatch-local image from the pinned commit
docker compose logs -f web # tail the API/UI logs
docker compose logs -f scheduler # tail the background job logsinstall.sh is idempotent, and re-running it is the supported way to pick up a rebuilt image. It leaves existing secrets, the database password, and existing data alone, and skips database initialization on an already-initialized schema.
Upgrading across the main repin runs 457 commits of accumulated dispatch database upgrade migrations against your existing data. Back up first: docker compose exec postgres pg_dumpall -U dispatch > backup.sql.
| Service | Role |
|---|---|
postgres |
Database. Publishes no host port by default; reachable only by service name on the Compose network. |
core |
Exists only to hold the build: block that produces the dispatch-local image web and scheduler both reference. |
web |
The API and UI (dispatch.main:app), published on port 8000. |
scheduler |
Background jobs. Runs with STATIC_DIR= blank so it skips frontend assets. |
core is not just a build placeholder. It runs. With no command: override it inherits the image's default CMD, which is the same server web runs. A plain docker compose up -d leaves a second, permanently-running copy of the full application reachable at http://core:8000/ inside the Compose network, with no restart policy. This is a known quirk of the current setup, not an intentional second instance you're expected to use.
For local development against your own Dispatch checkout, point the context: under core at a relative path to it (see the comment in docker-compose.yml) instead of the remote Git URL.
By default Dispatch starts with an empty database. install.sh offers to load the sample data dump (originally published by Netflix, mirrored on our fork) instead: say y at the prompt during install. This drops and recreates the dispatch database first, so only say yes on a fresh install. It loads under the default organization. This prompt is skipped when CI is set.
By default postgres publishes no host port. web, scheduler, and every install.sh database step reach it by service name over the internal Compose network.
If you need direct host access (backups, monitoring), add a port mapping to the postgres service in docker-compose.yml:
postgres:
ports:
- "127.0.0.1:5432:5432"The three options, safest first:
- No published port (the default). Nothing outside the Compose network can reach the database.
"127.0.0.1:5432:5432". Reachable only from the Docker host itself. Good for backups and monitoring that run on that host, and the usual choice when you need access from elsewhere: pair it with an SSH tunnel or VPN rather than publishing the port further."5432:5432". Reachable from every network the host is on. Choose this deliberately, not by accident: Docker's port publishing addsiptablesrules that sit ahead of host firewalls likeufwandfirewalld, so those tools' rules commonly fail to block it. If you go this route, make sure the database has a strong password and that something other than the host firewall is actually restricting access.
Apply the change with:
docker compose up -d --force-recreate postgresDispatch itself serves plain HTTP on port 8000. Put a TLS-terminating proxy such as HAProxy or Nginx in front of it; you'll likely want to add it as another service in docker-compose.yml.
Re-running install.sh only covers Postgres minor version bumps. Crossing a major version (e.g. 14 → 18) needs a manual dump/restore. Expand for the full procedure.
Postgres's on-disk format isn't compatible across major versions; the official image detects a mismatch and refuses to start rather than risk corrupting data, so Dispatch stays down until you complete these steps.
As of the 18 image, the official postgres image also changed where it stores data: a major-version-specific subdirectory (/var/lib/postgresql/<major>/docker) under a single volume mounted at the parent /var/lib/postgresql, instead of /var/lib/postgresql/data directly (see docker-library/postgres#1259). This repo's docker-compose.yml already mounts the volume this way for 18+; if yours still mounts at .../data, update that as part of this procedure rather than just bumping the image tag.
This procedure was validated end-to-end (fresh install → representative data → dump → restore → migrations → application read/write → restart) against a 14 → 18 upgrade. Substitute your actual versions and container/volume names if they differ.
-
Back up first. Stop the application tier but leave Postgres running, then dump the entire cluster using the old container's own client tools:
docker compose stop core web scheduler docker compose exec -T postgres pg_dumpall -U "$POSTGRES_USER" > dispatch-pg-upgrade.sql
Verify the dump is non-trivial before proceeding (e.g.
grep -c '^CREATE TABLE' dispatch-pg-upgrade.sql). Then stop Postgres too:docker compose stop postgres. -
Create a new volume for the new major version rather than reusing the old one, so the old volume stays untouched as a rollback path:
docker volume create dispatch-postgres-new
-
Start the target Postgres version against the new volume, with the same credentials as
.env, mounted at the parent directory:docker run -d --name pg-upgrade-target \ --env-file .env \ -v dispatch-postgres-new:/var/lib/postgresql \ postgres:18.4@sha256:a02db8cac496f15b094798a38254f14d6e00741f709360e5e00bb6668ea31636
-
Restore the dump. Two errors are expected and harmless (
role "dispatch" already exists,database "dispatch" already exists), since the target container already created them from.envon first start:cat dispatch-pg-upgrade.sql | \ docker exec -i --env PGPASSWORD="$POSTGRES_PASSWORD" pg-upgrade-target \ psql -U "$POSTGRES_USER" -d postgres
-
Verify the restored data directly, before touching Dispatch:
docker exec --env PGPASSWORD="$POSTGRES_PASSWORD" pg-upgrade-target \ psql -U "$POSTGRES_USER" -d "$DATABASE_NAME" -c "SHOW server_version;" docker exec --env PGPASSWORD="$POSTGRES_PASSWORD" pg-upgrade-target \ psql -U "$POSTGRES_USER" -d "$DATABASE_NAME" -c "\dt dispatch_organization_default.*"
-
Swap the volume in and remove the temporary container:
docker stop pg-upgrade-target && docker rm pg-upgrade-targetDocker has no "rename a volume" operation, so pick one of two options:
Option A (recommended): point
docker-compose.yml'spostgresservice and top-levelvolumes:block atdispatch-postgres-newinstead ofdispatch-postgres. The original volume is left completely untouched, so rollback stays trivial.Option B: reuse the
dispatch-postgresname. This destroys the old volume, so only do it if you still havedispatch-pg-upgrade.sqlfrom step 1.docker compose stopisn't enough here, because a stopped container still holds a reference to its volumes, so removing one fails withvolume is in useuntil the containers themselves are gone:docker compose down --remove-orphans docker volume rm dispatch-postgres docker volume create dispatch-postgres docker run --rm \ -v dispatch-postgres-new:/from -v dispatch-postgres:/to \ busybox cp -a /from/. /to/
-
Start Dispatch on the new Postgres version and run migrations, then confirm normal operation (log in, view/create an incident, check
docker compose logs schedulerfor errors):docker compose up -d docker compose run --rm web database upgrade
-
Once you've confirmed Dispatch is healthy, the old
dispatch-postgresvolume (untouched since step 2) can be removed. Keep it until you're confident, since it's your rollback, by reattaching it to adocker-compose.ymlstill pinned to the old image and mount path.
Rollback: retrying from step 2 with a new volume name is always safe, since the old volume is never modified. There's no supported way to convert an 18+ data directory back down to an older major version. "Rollback" always means reverting docker-compose.yml and reattaching the untouched old volume, not downgrading the new one.
- Secrets in
.env.exampleship as an obvious placeholder (REPLACEWITHSOMETHIINGSECRET) and a well-known default database password (dispatch).install.shreplaces both with random values on a fresh install; it prints explicit rotation instructions instead if it finds them still in place against a database that already has data, since neither can be rotated automatically without data loss. - Publishing Postgres's port with a bare
5432:5432exposes it on every network the host is on, and Docker's rules sit ahead of the host firewall. See Accessing Postgres from the host. - Dispatch itself serves plain HTTP; put it behind TLS before exposing it beyond your local network.
Reporting a vulnerability: this is a single-maintainer fork with no formal disclosure process or SLA. Open a GitHub issue for anything already public; for something you'd rather not disclose before a fix ships, contact @Jamyn directly.
Pull requests are welcome.
Every PR needs at least one primary label, saying what kind of change it is. CI enforces this:
| label | use it for |
|---|---|
bug |
fixing broken behaviour |
enhancement |
a new or improved capability |
documentation |
documentation only |
maintenance |
chores, refactors, and dependency upkeep |
security |
a security control or vulnerability remediation |
ci |
workflows, actions, and repository automation |
breaking-change |
anything requiring operator action to upgrade |
Prefer security over bug when a change is motivated by a security property, even where the symptom also reads as a defect.
Topic labels are optional and say what a PR touches: postgres, docker, compose, install, upstream, github-actions. They never satisfy the CI check on their own.
Commit subjects follow Conventional Commits: feat, fix, docs, refactor, test, build, ci, chore, perf, or security (a local, non-standard extension used when a change's primary purpose is a security control), followed by a short imperative summary. Commit keywords and PR labels are separate vocabularies — don't assume a commit keyword implies the same-named label.
Apache License 2.0, see LICENSE. This covers the contents of this repository (the Compose file, install script, and configuration); the Dispatch application itself, built from a separate repository, is licensed there.