Mercure 1.0 alpha is available. Check out the new docs
Sponsored by Les-Tilleuls.coop
DocumentationSpecificationCloudDemos
Contribute!

Docker

The official image is dunglas/mercure. Built on top of the Caddy image, so anything Caddy's image supports works here too.

Run the Mercure Docker image

Production mode:

# Run the Mercure Docker Image
docker run \
    -e MERCURE_PUBLISHER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' \
    -e MERCURE_SUBSCRIBER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' \
    -p 80:80 -p 443:443 \
    dunglas/mercure

The hub binds to :80 and :443. Caddy issues a Let's Encrypt cert for SERVER_NAME automatically. Don't set SERVER_NAME=localhost in production: it can't be issued a public certificate.

Behind a reverse proxy that handles TLS, set SERVER_NAME=:80 and skip :443:

# Run the Mercure Docker Image
docker run \
    -e SERVER_NAME=':80' \
    -e MERCURE_PUBLISHER_JWT_KEY='...' \
    -e MERCURE_SUBSCRIBER_JWT_KEY='...' \
    -p 80:80 \
    dunglas/mercure

Mercure Docker development mode

# Mercure Docker Development Mode
docker run \
    -e MERCURE_EXTRA_DIRECTIVES=playground \
    -p 80:80 -p 443:443 \
    dunglas/mercure

The playground directive turns on:

  • the debug UI at /.well-known/mercure/debug/, with a prefilled all-access token,

  • anonymous subscribers,

  • the playground's echo endpoints,

  • a permissive CORS config (cors_origins *).

Don't expose this to the internet.

Compose

# compose.yaml
services:
  mercure:
    image: dunglas/mercure
    restart: unless-stopped
    environment:
      MERCURE_PUBLISHER_JWT_KEY: "!ChangeThisMercureHubJWTSecretKey!"
      MERCURE_SUBSCRIBER_JWT_KEY: "!ChangeThisMercureHubJWTSecretKey!"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - mercure_data:/data
      - mercure_config:/config

volumes:
  mercure_data:
  mercure_config:
VolumeWhat's in it
/dataBoltDB history (mercure.db) and Caddy data (autosave, cert cache).
/configCaddy autosaved configuration.

Persist both. Losing /data means losing replay history; losing /config means re-issuing certificates on next boot.

Mercure Docker healthcheck

The image's built-in healthcheck queries localhost:2019/mercure/health/ready: the transport-aware readiness endpoint, not just "is the process up."

For Compose, override or extend it:

# Mercure Docker Healthcheck
services:
  mercure:
    # ...
    healthcheck:
      test:
        [
          "CMD",
          "wget",
          "-q",
          "--spider",
          "http://localhost:2019/mercure/health/ready",
        ]
      timeout: 5s
      retries: 5
      start_period: 60s

The start_period matters: BoltDB takes a moment to open on first boot, so the first probe may fail; treat that as "not unhealthy" for the first minute.

Rootless Mercure on Docker

The image runs as root by default. Recent Docker (20.10+) sets net.ipv4.ip_unprivileged_port_start=0 inside the container, so an unprivileged process can still bind 80/443 directly.

To run as a non-root user:

# compose.yaml
services:
  mercure:
    image: dunglas/mercure
    user: "1000:1000"
    read_only: true
    tmpfs:
      - /tmp
    environment:
      MERCURE_PUBLISHER_JWT_KEY: "!ChangeThisMercureHubJWTSecretKey!"
      MERCURE_SUBSCRIBER_JWT_KEY: "!ChangeThisMercureHubJWTSecretKey!"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - mercure_data:/data
      - mercure_config:/config

The volumes must be writable by UID 1000. For fresh named volumes, set ownership once:

# Rootless Mercure on Docker
docker run --rm -v mercure_data:/data -v mercure_config:/config alpine chown 1000:1000 /data /config

For bind mounts, chown 1000:1000 the host directory.

Custom Caddyfile

Ship your own Caddyfile:

# compose.yaml
services:
  mercure:
    image: dunglas/mercure
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - mercure_data:/data
      - mercure_config:/config

A custom Caddyfile is the right move once you need:

  • Multiple sites or hostnames on the same hub.

  • Reverse proxying alongside the hub (Caddy handling both).

  • Per-route rate limiting, request transformation, or custom auth.

  • Reading secrets from files ({file./run/secrets/jwt_key}) instead of environment variables.

Mercure hub Docker logs

Caddy logs to stdout in JSON by default. Pipe to whatever your platform expects (Loki, Datadog, CloudWatch). Useful fields:

  • mercure.subscribers_*: connection lifecycle.

  • mercure.update_*: publish events.

  • caddy.error_*: TLS, listener, and transport errors.

Bump verbosity with GLOBAL_OPTIONS=debug (don't leave it on in prod: it logs update payloads).

Mercure Docker image variants

  • dunglas/mercure: Alpine-based, statically linked.

  • dunglas/mercure:<version>: pin to a specific release.

  • Self-Hosted ships its own image with the multi-node transports: see High availability.

Behind a reverse proxy

If you're already running Traefik or NGINX, terminate TLS there and let the hub speak HTTP. See Reverse proxies.

Next steps for Mercure on Docker