Skip to main content

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 start 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 stop mercure

To remove the container entirely:

./laradock remove 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:

VariableDefaultWhat it does
MERCURE_NODE_HOST_HTTP_PORT1337Host-side port for HTTP (host:80).
MERCURE_NODE_HOST_HTTPS_PORT1338Host-side port for HTTPS (host:443).
MERCURE_PUBLISHER_JWT_KEYsecretSigning key used to validate JWTs from publishers. Change this for anything beyond local development.
MERCURE_SUBSCRIBER_JWT_KEYanother_secretSigning key used to validate JWTs from subscribers. Change this for anything beyond local development.
MERCURE_DEBUGdebugPassed through as the container's DEBUG environment variable.
MERCURE_SERVER_NAME:80Passed 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 logs 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 Unauthorized when publishing. The JWT must be signed with MERCURE_PUBLISHER_JWT_KEY and include the mercure.publish claim for the topics you're targeting.
  • Browser can't subscribe via CORS. MERCURE_EXTRA_DIRECTIVES in mercure/compose.yml already 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 1337 or 1338. Change MERCURE_NODE_HOST_HTTP_PORT / MERCURE_NODE_HOST_HTTPS_PORT in .env and restart with ./laradock restart mercure.
  • Using the default JWT keys in anything beyond local dev. secret and another_secret are placeholders; change MERCURE_PUBLISHER_JWT_KEY and MERCURE_SUBSCRIBER_JWT_KEY in .env before 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.