Skip to content

Install with Docker Compose (HTTPS)

The recommended production path: one docker compose up -d brings up Notifycat and a Caddy reverse proxy that obtains and renews a Let's Encrypt certificate automatically. All state is kept in Docker named volumes — no host-directory ownership concerns.

Recommended production path

Use this page for production installs. If you only want to run Notifycat locally or prefer to manage Caddy yourself, see Docker (manual).

Prerequisites

Requirement Notes
Docker Engine + Compose V2 Run docker compose version — must be v2 (the docker compose subcommand, not docker-compose)
A domain name An A (or AAAA) record pointing at the public IP of the host
Ports open 80/tcp (Let's Encrypt HTTP-01 challenge), 443/tcp + 443/udp (HTTPS + HTTP/3) must be reachable from the internet
mappings.yaml Copied from mappings.example.yaml and edited (see step 3)

Quick-start

1. Run the installer

curl -fsSL https://github.com/mptooling/notifycat/releases/latest/download/install.sh | sh
cd notifycat

The installer checks that Docker and Compose V2 are present, creates a ./notifycat directory, downloads all required files into it (compose.yaml, Caddyfile, notifycat wrapper, .env.example, mappings.example.yaml), and verifies each against the release's SHA256SUMS before use. To install a specific release, swap latest for a tag — e.g. releases/download/v0.11.0/install.sh. See Supported tags for what each tag means.

2. Run the setup wizard

./notifycat setup

The wizard prompts for:

  • Domain — the public DNS name pointing at this host (e.g. notifycat.acme.com)
  • ACME email — Let's Encrypt contact address
  • GitHub webhook secret — any strong random string; you'll use it when registering the webhook
  • Slack bot token — starts with xoxb-
  • First mapping — GitHub org, repositories (* for all, or a comma-separated list), and Slack channel ID

It writes .env (permissions 0600) and a starter mappings.yaml. Edit mappings.yaml to add more repos or orgs; see Mappings for the full format reference.

3. Start the stack

docker compose up -d

Caddy contacts Let's Encrypt via the HTTP-01 challenge. First-time certificate provisioning typically completes within 30 seconds.

4. Verify

# HTTPS health check through Caddy
curl -i https://notifycat.example.com/healthz   # expect HTTP/2 200

# Preflight report (config, database, mappings)
./notifycat doctor

All doctor entries should show ok.

5. Smoke-test delivery before wiring the real webhook

The doctor confirms config and connectivity; the smoke test confirms the whole path actually delivers. It forges a correctly-signed pull_request: opened event for a repository in your mappings.yaml, POSTs it to the running server's /webhook/github (exercising the real signature check, dispatcher, and Slack client), and reports the channel and Slack timestamp it produced:

./notifycat smoke <org>/<repo>              # use a repo present in mappings.yaml
./notifycat smoke --reactions <org>/<repo>  # also exercise the review-lifecycle reactions

A real message titled [notifycat smoke] … appears in the mapped channel — delete it once you've confirmed delivery. A secret mismatch is reported as a clear 401, and an unmapped repository is rejected before any request is sent.

Add --reactions to also replay a comment, an approval, and a merge for the same synthetic PR and verify (via reactions.get) that the configured emoji landed on the message — an end-to-end check of reactions:write/read and the reaction handlers. It is skipped with a note when SLACK_REACTIONS_ENABLED=false, and the merge step decorates the message as [Merged], so expect a few extra emoji on the throwaway message.

When bot reviews are not muted (NOTIFYCAT_IGNORE_AI_REVIEWS=false) and a marker is configured (SLACK_REACTION_BOT_REVIEW, default robot_face), --reactions also replays a review from a bot sender and verifies the marker lands. If bot reviews are muted, or the marker is set empty, that step is skipped with a one-line note — so its absence never reads as a silent pass.

6. Register the GitHub webhook

Set your webhook URL to https://notifycat.example.com/webhook/github with the secret from GITHUB_WEBHOOK_SECRET. See GitHub webhook setup.

7. Run the security checklist before go-live

Walk the Security & permissions checklist — confirm .env is 0600, the webhook secret is long and random, and the Slack bot carries only its documented scopes. It also explains why the running server needs no GitHub token at all.

How the stack is wired

Internet ──HTTPS──▶ Caddy :443 ──HTTP──▶ notifycat :8080

Caddy terminates TLS and proxies to the notifycat service on the internal Docker network. Three named volumes hold all persistent state:

Volume Contents
notifycat_data SQLite database (notifycat.db) and mappings.lock
caddy_data Let's Encrypt certificates and ACME state
caddy_config Caddy runtime config

mappings.yaml is bind-mounted read-only at /app/mappings.yaml inside the container. The writable notifycat_data volume covers the rest of /app, so mappings.lock (which Notifycat writes as a sibling file) lives on the named volume without needing write access to the bind mount.

Managing the stack

./notifycat up                                  # start or recreate containers
./notifycat down                                # stop and remove containers (volumes preserved)
./notifycat logs                                # follow server logs
docker compose pull && ./notifycat up           # pull latest image and redeploy
docker compose logs -f caddy                    # follow Caddy logs (ACME, access)
./notifycat doctor                              # run preflight checks

Both containers are set to restart: unless-stopped — they start automatically on reboot.

Upgrading and pinning a version

The shipped compose.yaml tracks ghcr.io/mptooling/notifycat:latest. docker compose up/run reuse an image that is already present locally, so to actually move to a newer release you must pull first:

docker compose pull && ./notifycat up           # fetch the latest image and redeploy

For reproducible deploys, pin a specific release instead of tracking :latest — edit the image: line in compose.yaml to ghcr.io/mptooling/notifycat:vX.Y.Z, then docker compose pull && ./notifycat up. See Supported tags for what each tag means.

Troubleshooting

Caddy fails to obtain a certificate

docker compose logs caddy
Symptom Cause Fix
connection refused / timeout on the ACME challenge Port 80 blocked (firewall or security group) Open inbound TCP 80 to 0.0.0.0/0
no such host / NXDOMAIN DNS A record not yet propagated Wait, then check with dig +short notifycat.example.com
unauthorized DNS points at a different IP than this host Compare curl -s https://api.ipify.org on the host vs dig +short from your laptop
rate limited Repeated failures exceeded Let's Encrypt's 5-failures-per-week limit Wait out the cooldown; or test with the LE staging endpoint (see below)

Testing with the Let's Encrypt staging endpoint (no rate limits, but cert is untrusted by browsers):

Edit Caddyfile and add acme_ca to the global block:

{
    email ops@example.com
    acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}

Then restart Caddy: docker compose restart caddy. Remove the acme_ca line before go-live.

Port 80 or 443 already in use

Caddy fails to bind if another process holds port 80 or 443.

sudo ss -tlnp | grep ':80\|:443'

Common causes: a running nginx/apache2 service, a previous docker run -p 443:443 container, or another Caddy instance. Stop the conflicting process, then run docker compose up -d caddy again.

UID 65532 permission errors on the named volume

Named volumes are initialised from the image's /app directory, which is already owned by 65532:65532 in the published image — so this should not occur on fresh installs.

If you see permission denied after restoring a backup or pre-populating the volume:

# One-shot container to fix ownership (replace the volume name if yours differs)
docker run --rm \
  -v notifycat_notifycat_data:/app \
  alpine chown -R 65532:65532 /app

Run docker volume ls to confirm the exact volume name.

Webhook returns 401

401 means the HMAC-SHA256 signature check failed — the secret on the GitHub webhook settings page does not match GITHUB_WEBHOOK_SECRET in .env.

  1. Copy the exact secret from GitHub → repository → Settings → Webhooks (no trailing whitespace).
  2. Update .env, then restart: docker compose restart notifycat.
  3. Redeliver the failing event from GitHub → Settings → Webhooks → Recent deliveries.

If the secret contains special shell characters, wrap it in single quotes in .env:

GITHUB_WEBHOOK_SECRET='p@$$w0rd!'

Notifycat exits on startup

docker compose logs notifycat

The most common cause is app: startup validation failed for N entries — one or more mappings failed their Slack or GitHub checks at boot. Run the mapping validator to see per-entry detail:

docker compose run --rm notifycat notifycat-mapping validate

Fix the failing entries in mappings.yaml, then docker compose up -d again. See Operations for the full ignored-event reason table.