Skip to content
 
 

Repository files navigation

Dispatch Docker

Postgres install License Release Last commit

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.

Status: independently maintained fork

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.

Requirements

  • Docker Engine 25.0.0+
  • Docker Compose V2 2.0.0+ (invoked as docker compose; Compose V1's standalone docker-compose is not supported)
  • At least 2400 MB of RAM available to Docker

Quick start

git clone https://github.com/Jamyn/dispatch-docker.git
cd dispatch-docker
./install.sh
docker compose up -d

install.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.

Configuration

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.

Common commands

./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 logs

install.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.

Architecture

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.

Sample data

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.

Accessing Postgres from the host

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:

  1. No published port (the default). Nothing outside the Compose network can reach the database.
  2. "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.
  3. "5432:5432". Reachable from every network the host is on. Choose this deliberately, not by accident: Docker's port publishing adds iptables rules that sit ahead of host firewalls like ufw and firewalld, 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 postgres

Securing Dispatch with SSL/TLS

Dispatch 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.

Upgrading Postgres to a new major version

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.

  1. 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.

  2. 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
  3. 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
  4. 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 .env on first start:

    cat dispatch-pg-upgrade.sql | \
      docker exec -i --env PGPASSWORD="$POSTGRES_PASSWORD" pg-upgrade-target \
      psql -U "$POSTGRES_USER" -d postgres
  5. 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.*"
  6. Swap the volume in and remove the temporary container:

    docker stop pg-upgrade-target && docker rm pg-upgrade-target

    Docker has no "rename a volume" operation, so pick one of two options:

    Option A (recommended): point docker-compose.yml's postgres service and top-level volumes: block at dispatch-postgres-new instead of dispatch-postgres. The original volume is left completely untouched, so rollback stays trivial.

    Option B: reuse the dispatch-postgres name. This destroys the old volume, so only do it if you still have dispatch-pg-upgrade.sql from step 1. docker compose stop isn't enough here, because a stopped container still holds a reference to its volumes, so removing one fails with volume is in use until 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/
  7. Start Dispatch on the new Postgres version and run migrations, then confirm normal operation (log in, view/create an incident, check docker compose logs scheduler for errors):

    docker compose up -d
    docker compose run --rm web database upgrade
  8. Once you've confirmed Dispatch is healthy, the old dispatch-postgres volume (untouched since step 2) can be removed. Keep it until you're confident, since it's your rollback, by reattaching it to a docker-compose.yml still 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.

Security

  • Secrets in .env.example ship as an obvious placeholder (REPLACEWITHSOMETHIINGSECRET) and a well-known default database password (dispatch). install.sh replaces 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:5432 exposes 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.

Contributing

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.

License

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.

About

No description, website, or topics provided.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages