Mercure
What is Mercure?
Mercure is an open protocol and hub for pushing real-time updates to web and mobile clients using Server-Sent Events. It's the default real-time solution for Symfony and API Platform, and works as a lightweight alternative to WebSocket servers when you only need server-to-client push. Laradock builds it from the official dunglas/mercure image.
Start Mercure
- Laradock CLI
- Docker Compose
./laradock start mercure
docker compose up -d mercure
The hub keeps no persistent data on disk, there's no volume in mercure/compose.yml. All subscriptions and in-flight updates live in the running container's memory only, so a restart clears them, there's nothing to back up. Name any other services alongside it to start them together, for example ./laradock start mercure workspace.
Stop Mercure
Stopping just pauses the container:
- Laradock CLI
- Docker Compose
./laradock stop mercure
docker compose stop mercure
To remove the container entirely:
- Laradock CLI
- Docker Compose
./laradock remove mercure
docker compose rm -sf mercure
Since there's no data volume, stopping and removing are functionally the same in terms of what's lost: any currently-open subscriptions and undelivered updates. Starting again gives you a clean hub.
Configuration
All settings live in mercure/defaults.env and can be overridden by adding the same line to your own .env:
| Variable | Default | What it does |
|---|---|---|
MERCURE_NODE_HOST_HTTP_PORT | 1337 | Host-side port for HTTP (host:80). |
MERCURE_NODE_HOST_HTTPS_PORT | 1338 | Host-side port for HTTPS (host:443). |
MERCURE_PUBLISHER_JWT_KEY | secret | Signing key used to validate JWTs from publishers. Change this for anything beyond local development. |
MERCURE_SUBSCRIBER_JWT_KEY | another_secret | Signing key used to validate JWTs from subscribers. Change this for anything beyond local development. |
MERCURE_DEBUG | debug | Passed through as the container's DEBUG environment variable. |
MERCURE_SERVER_NAME | :80 | Passed through as the Caddy SERVER_NAME the Mercure hub binds to. |
mercure/compose.yml also sets MERCURE_EXTRA_DIRECTIVES to allow CORS from any origin and to allow publishing from http://localhost:<MERCURE_NODE_HOST_HTTP_PORT> and its HTTPS equivalent. Edit that block in mercure/compose.yml directly if you need different origins.
Publish and subscribe
Inside Laradock, other containers reach the hub by container name: mercure:80. From your host machine (or the browser), use http://localhost:1337 (or your custom MERCURE_NODE_HOST_HTTP_PORT).
Publishing requires a JWT signed with MERCURE_PUBLISHER_JWT_KEY containing the topics you're allowed to publish to; subscribing (from the browser via EventSource) requires a JWT signed with MERCURE_SUBSCRIBER_JWT_KEY for private topics, or no JWT at all for public ones. See the Mercure documentation for the JWT claim format.
Test it from the command line
Every Mercure hub exposes the protocol's fixed endpoint, /.well-known/mercure, for both publishing and subscribing, useful for a quick sanity check without wiring up your app first.
Subscribe to a topic (this blocks and streams events as they arrive, -N disables curl's output buffering):
curl -N "http://localhost:1337/.well-known/mercure?topic=https://example.com/my-topic"
In a second terminal, publish an update to that same topic (data is the payload your subscribers receive):
curl -X POST "http://localhost:1337/.well-known/mercure" \
-H "Authorization: Bearer <your publisher JWT>" \
-d "topic=https://example.com/my-topic" \
-d 'data={"hello":"world"}'
You should see the update appear on the first terminal's stream. The publisher JWT must be signed (HS256) with your MERCURE_PUBLISHER_JWT_KEY and carry a mercure.publish claim covering the topic (["*"] to allow all topics during local testing). If the topic you're subscribing to is private, the subscribe request also needs an Authorization header with a JWT signed by MERCURE_SUBSCRIBER_JWT_KEY.
Debug mode
MERCURE_DEBUG (default debug) is passed straight through as the container's DEBUG variable, turning on more verbose logging from the underlying Caddy server. Tail it while reproducing a failing publish or subscribe:
- Laradock CLI
- Docker Compose
./laradock logs mercure
docker compose logs --tail=100 mercure
Talk to this hub from another Laradock project
Each Laradock project is its own isolated Docker network by default, so a second project's containers can't reach this hub by container name (mercure) out of the box. The ports are already published to your host (MERCURE_NODE_HOST_HTTP_PORT/MERCURE_NODE_HOST_HTTPS_PORT), so point the other project at your host machine's address instead, for example http://host.docker.internal:1337 (Docker Desktop) from inside another project's containers. Make sure the two projects use different MERCURE_NODE_HOST_HTTP_PORT values if they're both running at once.
Common issues
401 Unauthorizedwhen publishing. The JWT must be signed withMERCURE_PUBLISHER_JWT_KEYand include themercure.publishclaim for the topics you're targeting.- Browser can't subscribe via CORS.
MERCURE_EXTRA_DIRECTIVESinmercure/compose.ymlalready allows all origins (cors_allowed_origins *) by default; if you've narrowed it, make sure your frontend's origin is included. - Port already in use on your host. Another local Mercure hub (or another Laradock project) is already bound to
1337or1338. ChangeMERCURE_NODE_HOST_HTTP_PORT/MERCURE_NODE_HOST_HTTPS_PORTin.envand restart with./laradock restart mercure. - Using the default JWT keys in anything beyond local dev.
secretandanother_secretare placeholders; changeMERCURE_PUBLISHER_JWT_KEYandMERCURE_SUBSCRIBER_JWT_KEYin.envbefore this leaves your machine. - Updates aren't reaching subscribers after a restart. Expected: Mercure keeps no persistent state, subscriptions and undelivered updates don't survive a restart. Your app needs to resubscribe.
Need Pusher-protocol WebSockets for Laravel Echo instead? See Soketi or Laravel Reverb. New to Laradock? Start at Getting Started.