Mercure is an open-source protocol for pushing data updates to web browsers and other HTTP clients in real-time. It’s built on top of Server-Sent Events (SSE) and provides a fast, reliable, and battery-efficient way to implement real-time communications. The Mercure hub is distributed as a custom build of the Caddy web server with the Mercure module included.
In this guide, we’ll walk through setting up Mercure on your own server using Docker Compose, covering all the essential configuration options and environment variables you need to get started.
Basic Docker Compose Setup
Here’s a minimal Docker Compose configuration to get Mercure running:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# compose.yaml services: mercure: image: dunglas/mercure restart: unless-stopped environment: SERVER_NAME: "localhost" 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: |
Development Configuration
For development purposes, you can enable the debug UI and allow anonymous subscribers:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# compose.yaml services: mercure: image: dunglas/mercure restart: unless-stopped environment: SERVER_NAME: "localhost" MERCURE_PUBLISHER_JWT_KEY: "!ChangeThisMercureHubJWTSecretKey!" MERCURE_SUBSCRIBER_JWT_KEY: "!ChangeThisMercureHubJWTSecretKey!" MERCURE_EXTRA_DIRECTIVES: | anonymous ui demo command: /usr/bin/caddy run --config /etc/caddy/dev.Caddyfile ports: - "80:80" - "443:443" volumes: - mercure_data:/data - mercure_config:/config volumes: mercure_data: mercure_config: |
Production Configuration with Health Checks
For production deployments, include health checks and proper security settings:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# compose.yaml services: mercure: image: dunglas/mercure restart: unless-stopped environment: # Server configuration SERVER_NAME: "mercure.yourdomain.com" # JWT Keys - CHANGE THESE IN PRODUCTION MERCURE_PUBLISHER_JWT_KEY: "your-strong-publisher-jwt-secret-key-here" MERCURE_SUBSCRIBER_JWT_KEY: "your-strong-subscriber-jwt-secret-key-here" MERCURE_PUBLISHER_JWT_ALG: "HS256" MERCURE_SUBSCRIBER_JWT_ALG: "HS256" # CORS configuration MERCURE_EXTRA_DIRECTIVES: | cors_origins https://yourdomain.com https://www.yourdomain.com publish_origins https://yourdomain.com https://www.yourdomain.com ports: - "80:80" - "443:443" volumes: - mercure_data:/data - mercure_config:/config healthcheck: test: ["CMD", "wget", "-q", "--spider", "https://localhost/healthz"] timeout: 5s retries: 5 start_period: 60s volumes: mercure_data: mercure_config: |
HTTP-Only Setup (Behind Reverse Proxy)
If you’re running Mercure behind a reverse proxy and want to disable HTTPS:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# compose.yaml services: mercure: image: dunglas/mercure restart: unless-stopped environment: SERVER_NAME: ":80" # Disable HTTPS MERCURE_PUBLISHER_JWT_KEY: "!ChangeThisMercureHubJWTSecretKey!" MERCURE_SUBSCRIBER_JWT_KEY: "!ChangeThisMercureHubJWTSecretKey!" ports: - "80:80" volumes: - mercure_data:/data - mercure_config:/config volumes: mercure_data: mercure_config: |
Environment Variables Reference
Core Configuration
- SERVER_NAME: The server name or address (default:
localhost)- Examples:
localhost,mercure.example.com,:3000,:80
- Examples:
- MERCURE_PUBLISHER_JWT_KEY: JWT key for publishers (required)
- MERCURE_SUBSCRIBER_JWT_KEY: JWT key for subscribers (required)
- MERCURE_PUBLISHER_JWT_ALG: JWT algorithm for publishers (default:
HS256) - MERCURE_SUBSCRIBER_JWT_ALG: JWT algorithm for subscribers (default:
HS256)
Advanced Configuration
- MERCURE_EXTRA_DIRECTIVES: Additional Mercure directives (multiline)
|
1 2 3 4 5 6 7 8 9 |
MERCURE_EXTRA_DIRECTIVES: | anonymous ui demo heartbeat 30s cors_origins https://example.com publish_origins https://example.com |
- CADDY_EXTRA_CONFIG: Additional Caddy configuration
- CADDY_SERVER_EXTRA_DIRECTIVES: Additional Caddy server directives
- GLOBAL_OPTIONS: Global Caddy options block
JWT Configuration Options
You can use different JWT algorithms:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# HMAC (symmetric key) environment: MERCURE_PUBLISHER_JWT_KEY: "your-secret-key" MERCURE_PUBLISHER_JWT_ALG: "HS256" # RSA (asymmetric key) environment: MERCURE_PUBLISHER_JWT_KEY: | -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... -----END PUBLIC KEY----- MERCURE_PUBLISHER_JWT_ALG: "RS256" |
Complete Production Example
Here’s a comprehensive production setup with all common options:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# compose.yaml services: mercure: image: dunglas/mercure:latest restart: unless-stopped environment: # Server configuration SERVER_NAME: "mercure.yourdomain.com" # JWT Configuration MERCURE_PUBLISHER_JWT_KEY: "${MERCURE_PUBLISHER_JWT_KEY}" MERCURE_SUBSCRIBER_JWT_KEY: "${MERCURE_SUBSCRIBER_JWT_KEY}" MERCURE_PUBLISHER_JWT_ALG: "HS256" MERCURE_SUBSCRIBER_JWT_ALG: "HS256" # Mercure-specific directives MERCURE_EXTRA_DIRECTIVES: | cors_origins https://yourdomain.com https://api.yourdomain.com publish_origins https://yourdomain.com https://api.yourdomain.com heartbeat 40s dispatch_timeout 5s write_timeout 600s transport bolt://mercure.db?size=1000&cleanup_frequency=0.3 # Additional Caddy configuration CADDY_EXTRA_CONFIG: | handle /health* { respond "OK" 200 } ports: - "80:80" - "443:443" volumes: - mercure_data:/data - mercure_config:/config - ./mercure.db:/data/mercure.db healthcheck: test: ["CMD", "wget", "-O-", "https://localhost/healthz"] timeout: 5s retries: 5 start_period: 60s networks: - mercure_network networks: mercure_network: driver: bridge volumes: mercure_data: mercure_config: |
Environment File Setup
Create a .env file for your secrets:
|
1 2 3 4 5 |
# .env file MERCURE_PUBLISHER_JWT_KEY=your-super-secret-publisher-jwt-key-change-this-in-production MERCURE_SUBSCRIBER_JWT_KEY=your-super-secret-subscriber-jwt-key-change-this-in-production |
Mercure Directives Explained
Security Directives
anonymous: Allow subscribers without JWT tokenscors_origins <origins>: Allowed CORS originspublish_origins <origins>: Origins allowed to publish (cookie auth only)
Performance Directives
heartbeat <duration>: Interval between heartbeats (default: 40s)dispatch_timeout <duration>: Max dispatch time per update (default: 5s)write_timeout <duration>: Max connection duration (default: 600s)transport <config>: Storage backend configuration
Feature Directives
ui: Enable the debug UIdemo: Enable demo endpointssubscriptions: Enable subscription API
Accessing Your Mercure Hub
Once running, your Mercure hub will be available at:
- Subscribe endpoint:
GET https://your-domain/.well-known/mercure - Publish endpoint:
POST https://your-domain/.well-known/mercure - Health check:
GET https://your-domain/healthz - Debug UI (if enabled):
GET https://your-domain/.well-known/mercure/ui/
Starting Your Setup
- Save your Docker Compose configuration to
compose.yaml - Create your
.envfile with secure JWT keys - Generate strong JWT keys (JWT Encoder / Decoder):
|
1 2 3 4 |
# Generate a random 256-bit key openssl rand -hex 32 |
- Start the service:
|
1 2 3 |
docker compose up -d |
- Check the logs:
|
1 2 3 |
docker compose logs -f mercure |
Generating JWT Tokens
To publish updates, you’ll need to generate JWT tokens. Here’s a simple example in multiple languages:
PHP
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
use Firebase\JWT\JWT; use Firebase\JWT\Key; $key = 'your-publisher-jwt-key'; $payload = [ 'mercure' =--> [ 'publish' => ['*'], // Allow publishing to all topics 'subscribe'=> ["*"] // Topics the token can subscribe to ] ]; $token = JWT::encode($payload, $key, 'HS256'); |
JavaScript/Node.js
|
1 2 3 4 5 6 7 8 9 10 11 12 |
const jwt = require('jsonwebtoken'); const key = 'your-publisher-jwt-key'; const payload = { mercure: { publish: ['*'] // Allow publishing to all topics } }; const token = jwt.sign(payload, key, { algorithm: 'HS256' }); |
Python
|
1 2 3 4 5 6 7 8 9 10 11 12 |
import jwt key = 'your-publisher-jwt-key' payload = { 'mercure': { 'publish': ['*'] # Allow publishing to all topics } } token = jwt.encode(payload, key, algorithm='HS256') |
Publishing Updates
Once you have a JWT token, you can publish updates using a simple HTTP POST request:
|
1 2 3 4 5 6 7 |
curl -X POST https://your-domain/.well-known/mercure \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "topic=https://example.com/books/1" \ -d "data={\"message\": \"Book updated!\"}" |
Subscribing to Updates
Clients can subscribe to updates using Server-Sent Events:
|
1 2 3 4 5 6 7 8 9 10 11 |
const eventSource = new EventSource('https://your-domain/.well-known/mercure?topic=https://example.com/books/1'); eventSource.onmessage = function(event) { console.log('Received update:', event.data); }; eventSource.onerror = function(event) { console.error('Connection error:', event); }; |
Advanced Configuration Examples
Multiple Topics and Private Updates
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
services: mercure: image: dunglas/mercure environment: SERVER_NAME: "mercure.example.com" MERCURE_PUBLISHER_JWT_KEY: "${MERCURE_PUBLISHER_JWT_KEY}" MERCURE_SUBSCRIBER_JWT_KEY: "${MERCURE_SUBSCRIBER_JWT_KEY}" MERCURE_EXTRA_DIRECTIVES: | # Enable subscriptions API for monitoring subscriptions # Custom heartbeat interval heartbeat 30s # Configure transport with history transport bolt://mercure.db?size=5000&cleanup_frequency=0.2 # CORS for multiple domains cors_origins https://app.example.com https://admin.example.com publish_origins https://app.example.com https://admin.example.com |
High Availability Setup
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
services: mercure: image: dunglas/mercure deploy: replicas: 3 environment: SERVER_NAME: "mercure.example.com" MERCURE_PUBLISHER_JWT_KEY: "${MERCURE_PUBLISHER_JWT_KEY}" MERCURE_SUBSCRIBER_JWT_KEY: "${MERCURE_SUBSCRIBER_JWT_KEY}" MERCURE_EXTRA_DIRECTIVES: | # Use Redis for clustering transport redis://redis:6379 # Optimized timeouts for HA dispatch_timeout 3s write_timeout 300s heartbeat 20s depends_on: - redis networks: - mercure_network redis: image: redis:alpine networks: - mercure_network |
Troubleshooting Common Issues
CORS Issues
If you’re experiencing CORS problems, make sure to configure the cors_origins directive properly:
|
1 2 3 4 5 6 |
MERCURE_EXTRA_DIRECTIVES: | cors_origins https://your-frontend-domain.com https://another-domain.com # Or allow all origins (NOT recommended for production) cors_origins * |
JWT Token Issues
Ensure your JWT tokens have the correct structure:
|
1 2 3 4 5 6 7 8 |
{ "mercure": { "publish": ["*"], // Topics the token can publish to "subscribe": ["*"] // Topics the token can subscribe to } } |
Connection Issues
Check if the Mercure hub is accessible:
|
1 2 3 4 5 6 7 |
# Test health endpoint curl -k https://localhost/healthz # Check if Mercure endpoint responds curl -k https://localhost/.well-known/mercure |
Security Best Practices
- Use strong JWT keys: Generate random, long keys for production
- Restrict CORS origins: Only allow trusted domains
- Use HTTPS: Always enable TLS in production
- Limit topic access: Use specific topic patterns in JWT tokens
- Regular key rotation: Rotate JWT keys periodically
- Monitor access logs: Keep track of connections and publications
Performance Optimization
Transport Configuration
Optimize the Bolt database settings for your use case:
|
1 2 3 4 5 6 7 8 9 10 11 |
MERCURE_EXTRA_DIRECTIVES: | # Large history for high-traffic apps transport bolt://mercure.db?size=10000&cleanup_frequency=0.1 # Smaller history for memory-constrained environments transport bolt://mercure.db?size=100&cleanup_frequency=0.5 # No history (fastest, but no recovery for disconnected clients) transport local://local |
Timeout Configuration
|
1 2 3 4 5 6 7 8 9 |
MERCURE_EXTRA_DIRECTIVES: | # Faster dispatch for real-time apps dispatch_timeout 1s # Longer connection timeout for stable connections write_timeout 1200s # More frequent heartbeats for mobile clients heartbeat 15s |
Important Links
- Official Website: https://mercure.rocks
- GitHub Repository: https://github.com/dunglas/mercure
- Docker Hub: https://hub.docker.com/r/dunglas/mercure
- Documentation: https://mercure.rocks/docs/hub/install
- Configuration Reference: https://mercure.rocks/docs/hub/config
- Symfony Integration: https://symfony.com/doc/current/mercure.html
Your Mercure hub is now ready to handle real-time communications for your applications!
EXTRA: Setting Up Mercure with Caddy and NGINX Proxy Manager
1. Mercure Configuration with Caddy
First, let’s configure Mercure with its built-in Caddy server. Create a Caddyfile:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# Caddyfile for Mercure { auto_https off # If NGINX Proxy Manager handles SSL } :3000 { log { output stdout format console level INFO } mercure { # JWT key for publishers publisher_jwt !ChangeThisSecretKey! # JWT key for subscribers (optional) subscriber_jwt !ChangeThisSubscriberKey! # CORS configuration cors_origins "https://yourdomain.com" "https://app.yourdomain.com" publish_origins "*" # Allow anonymous subscribers (optional) anonymous # UI for debugging demo } respond /healthz 200 } |
Or using environment variables in docker-compose.yml:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
version: '3.8' services: mercure: image: dunglas/mercure restart: unless-stopped environment: # Mercure configuration MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisSecretKey!' MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisSubscriberKey!' # CORS settings MERCURE_CORS_ALLOWED_ORIGINS: "https://yourdomain.com https://app.yourdomain.com" MERCURE_PUBLISH_ALLOWED_ORIGINS: '*' # Optional: Allow anonymous subscribers MERCURE_ANONYMOUS: "1" # Optional: Enable debug UI MERCURE_DEMO: "1" # Server configuration SERVER_NAME: ':3000' MERCURE_TRANSPORT_URL: "bolt://mercure.db" # Disable Caddy's auto HTTPS since NGINX will handle it CADDY_AUTO_HTTPS: "off" ports: - "3000:3000" volumes: - mercure_data:/data - mercure_config:/config volumes: mercure_data: mercure_config: |
2. NGINX Proxy Manager Configuration
In NGINX Proxy Manager, create a proxy host with these settings:
Details Tab:
- Domain Names:
mercure.yourdomain.com - Scheme:
http - Forward Hostname/IP:
mercure(or the container name/IP) - Forward Port:
3000 - Enable “Websockets Support” ✓ (Important!)
- Enable “Block Common Exploits” ✓
- Enable “HTTP/2 Support” ✓
SSL Tab:
- Request SSL Certificate with Let’s Encrypt
- Force SSL ✓
Advanced Tab – Custom Nginx Configuration:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
# Proxy headers for Mercure proxy_read_timeout 24h; proxy_http_version 1.1; proxy_set_header Connection ""; # Important for SSE (Server-Sent Events) proxy_buffering off; proxy_cache off; chunked_transfer_encoding off; # Keep connections alive proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Proto $scheme; # CORS headers (if Mercure's CORS isn't sufficient) add_header 'Access-Control-Allow-Origin' $http_origin always; add_header 'Access-Control-Allow-Credentials' 'true' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,Mercure' always; # Handle preflight requests if ($request_method = 'OPTIONS') { add_header 'Access-Control-Allow-Origin' $http_origin always; add_header 'Access-Control-Allow-Credentials' 'true' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,Mercure' always; add_header 'Access-Control-Max-Age' 1728000; add_header 'Content-Type' 'text/plain; charset=utf-8'; add_header 'Content-Length' 0; return 204; } # SSE specific headers add_header 'Cache-Control' 'no-cache'; add_header 'X-Accel-Buffering' 'no'; |
3. Working CORS Configuration for NGINX Proxy Manager
Option 1: Simple CORS Configuration (Recommended)
In the Advanced Tab of your proxy host, add this configuration that works within NPM’s limitations:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# Proxy settings for Mercure/SSE proxy_read_timeout 24h; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_buffering off; proxy_cache off; chunked_transfer_encoding off; # SSE specific proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Proto $scheme; # CORS headers - always append add_header 'Access-Control-Allow-Origin' $http_origin always; add_header 'Access-Control-Allow-Credentials' 'true' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,Mercure' always; add_header 'Access-Control-Max-Age' '86400' always; # SSE headers add_header 'Cache-Control' 'no-cache' always; add_header 'X-Accel-Buffering' 'no' always; |
Option 2: Let Mercure Handle CORS (Cleaner Approach)
Since Mercure has built-in CORS support, you can configure CORS entirely in Mercure and just focus on proxy settings in NPM:
NGINX Proxy Manager Advanced Config:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# Minimal proxy configuration proxy_read_timeout 24h; proxy_http_version 1.1; proxy_set_header Connection ""; # Critical for SSE proxy_buffering off; proxy_cache off; chunked_transfer_encoding off; add_header 'X-Accel-Buffering' 'no' always; # Forward headers proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; |
Mercure Docker Configuration:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
version: '3.8' services: mercure: image: dunglas/mercure restart: unless-stopped environment: # JWT keys MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisSecretKey32Characters!' MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisSubscriberKey32Characters!' # Let Mercure handle ALL CORS MERCURE_CORS_ALLOWED_ORIGINS: "*" # Or specific origins MERCURE_PUBLISH_ALLOWED_ORIGINS: "*" # Allow OPTIONS preflight MERCURE_CORS_ALLOW_CREDENTIALS: "true" # Other settings MERCURE_ANONYMOUS: "1" MERCURE_DEMO: "1" SERVER_NAME: ':3000' CADDY_AUTO_HTTPS: "off" ports: - "3000:3000" |
Option 3: Using Custom Location Block
Some versions of NPM allow custom location blocks. Try this in the Advanced tab:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
location /.well-known/mercure { proxy_pass http://mercure:3000/.well-known/mercure; proxy_read_timeout 24h; proxy_http_version 1.1; # SSE requirements proxy_set_header Connection ""; proxy_buffering off; proxy_cache off; # Headers proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Proto $scheme; # CORS add_header 'Access-Control-Allow-Origin' $http_origin always; add_header 'Access-Control-Allow-Credentials' 'true' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'Authorization,Mercure,Content-Type' always; add_header 'X-Accel-Buffering' 'no' always; } |
4. Complete Docker Compose Setup
Here’s a complete setup with both services:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
version: '3.8' networks: web: external: true internal: external: false services: mercure: image: dunglas/mercure restart: unless-stopped networks: - internal - web environment: MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisSecretKey32Characters!' MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisSubscriberKey32Characters!' MERCURE_CORS_ALLOWED_ORIGINS: "https://yourdomain.com https://app.yourdomain.com http://localhost:3000" MERCURE_PUBLISH_ALLOWED_ORIGINS: '*' MERCURE_ANONYMOUS: "1" MERCURE_DEMO: "1" SERVER_NAME: ':3000' CADDY_AUTO_HTTPS: "off" volumes: - mercure_data:/data - mercure_config:/config labels: - "traefik.enable=false" # If using Traefik alongside nginx-proxy-manager: image: 'jc21/nginx-proxy-manager:latest' restart: unless-stopped ports: - '80:80' - '443:443' - '81:81' # Admin GUI networks: - web - internal volumes: - npm_data:/data - npm_letsencrypt:/etc/letsencrypt volumes: mercure_data: mercure_config: npm_data: npm_letsencrypt: |
5. Testing Your Configuration
Test Mercure directly:
|
1 2 3 4 5 6 7 |
# Test health endpoint curl http://localhost:3000/healthz # Test through NGINX Proxy Manager curl https://mercure.yourdomain.com/.well-known/mercure |
Test CORS:
|
1 2 3 4 5 6 |
# Test CORS preflight curl -I -X OPTIONS https://mercure.yourdomain.com/.well-known/mercure \ -H "Origin: https://yourdomain.com" \ -H "Access-Control-Request-Method: GET" |
Check if Mercure is accessible:
|
1 2 3 |
curl -I https://mercure.yourdomain.com/.well-known/mercure |
Test CORS headers are present:
|
1 2 3 4 |
curl -I https://mercure.yourdomain.com/.well-known/mercure \ -H "Origin: https://yourdomain.com" |
You should see headers like:
|
1 2 3 4 5 |
Access-Control-Allow-Origin: https://yourdomain.com Access-Control-Allow-Credentials: true Access-Control-Allow-Methods: GET, POST, OPTIONS |
JavaScript client example:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// Subscribe to updates const url = new URL('https://mercure.yourdomain.com/.well-known/mercure'); url.searchParams.append('topic', 'https://example.com/my-topic'); const eventSource = new EventSource(url, { withCredentials: true // If using cookies for auth }); eventSource.onmessage = (event) => { console.log('New message:', JSON.parse(event.data)); }; // Publish updates (requires JWT) fetch('https://mercure.yourdomain.com/.well-known/mercure', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_JWT_TOKEN', 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ 'topic': 'https://example.com/my-topic', 'data': JSON.stringify({ message: 'Hello!' }) }) }); |
Test with a simple HTML client:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<!DOCTYPE html> <html> <head> <title>Mercure Test</title> </head> <body> <div id="messages"></div> <script> const url = new URL('https://mercure.yourdomain.com/.well-known/mercure'); url.searchParams.append('topic', 'test'); const eventSource = new EventSource(url, { withCredentials: true }); eventSource.onopen = () => { console.log('Connected to Mercure'); document.getElementById('messages').innerHTML += '<p>Connected!</p>'; }; eventSource.onmessage = (e) => { console.log('Message:', e.data); document.getElementById('messages').innerHTML += `<p>${e.data}</p>`; }; eventSource.onerror = (error) => { console.error('EventSource error:', error); }; </script> </body> </html> |
6. Important Settings Summary
Must-have in NPM Advanced Config:
proxy_buffering off– Critical for SSEproxy_read_timeout 24h– Prevents timeoutadd_header 'X-Accel-Buffering' 'no'– Disables NGINX buffering
In NPM Details Tab:
- Enable “Websockets Support”
- Use HTTP scheme (not HTTPS) if Mercure is on same network
- Set correct port (3000 by default)
7. Troubleshooting Common Issues
SSE Connection Drops:
- Ensure
proxy_buffering offis set - Check
proxy_read_timeoutis set to a high value (24h) - Verify WebSockets support is enabled in NGINX Proxy Manager
CORS Errors:
- Check origins match exactly (including protocol)
- Verify credentials setting matches on both client and server
- Check browser console for specific CORS error messages
Authentication Issues:
- Verify JWT keys match between publisher and Mercure config
- Check JWT token expiration
- Ensure JWT has correct claims for publishing/subscribing
The key is that NPM’s GUI is limited, so it’s often better to let Mercure handle CORS entirely and just ensure NPM properly proxies the SSE connections without buffering.
FAQ SSE
What are Server-Sent Events (SSE)?
How do SSE differ from WebSockets?
What is the connection limit for SSE?
How do I implement SSE on the client side?
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const eventSource = new EventSource('/api/events'); eventSource.onmessage = function(event) { console.log('Received:', event.data); }; eventSource.onerror = function(event) { console.error('SSE error:', event); }; // Close connection eventSource.close(); |
How do I implement SSE on the server side?
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// Node.js/Express example app.get('/api/events', (req, res) => { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*' }); // Send data res.write('data: Hello from server\n\n'); // Send with event type res.write('event: update\n'); res.write('data: {"message": "Update received"}\n\n'); }); |
Why do SSE connections keep disconnecting?
What browsers support SSE?
When should I use SSE instead of WebSockets?
- One-way server-to-client communication only
- Simple implementation with existing HTTP infrastructure
- Automatic reconnection handling
- Real-time updates like news feeds, stock prices, or notifications
- Better compatibility with firewalls and proxies
Can SSE send binary data?
How do I handle SSE reconnection?
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// Server-side: Set retry delay (in milliseconds) res.write('retry: 5000\n\n'); // Client-side: Handle reconnection eventSource.addEventListener('open', function(event) { console.log('Connection opened'); }); eventSource.addEventListener('error', function(event) { if (event.target.readyState === EventSource.CLOSED) { console.log('Connection closed'); } else if (event.target.readyState === EventSource.CONNECTING) { console.log('Reconnecting...'); } }); |
What are common SSE use cases?
- Real-time news feeds and social media updates
- Stock price tickers and financial data
- Live sports scores and updates
- System monitoring dashboards
- Chat applications (receive messages only)
- Progress indicators for long-running tasks
- Live notifications and alerts
- IoT sensor data streaming
How do I send different event types with SSE?
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// Server-side res.write('event: notification\n'); res.write('data: {"type": "info", "message": "Hello"}\n\n'); res.write('event: update\n'); res.write('data: {"status": "completed"}\n\n'); // Client-side eventSource.addEventListener('notification', function(event) { console.log('Notification:', event.data); }); eventSource.addEventListener('update', function(event) { console.log('Update:', event.data); }); |
What are the performance limitations of SSE?
- 6 concurrent connections per domain on HTTP/1.1
- Higher latency compared to WebSockets for frequent updates
- Text-only data transmission (UTF-8)
- One-way communication only
- HTTP overhead for each message
- Potential issues with corporate firewalls or proxies
How do I implement authentication with SSE?
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// Using URL parameters const eventSource = new EventSource('/api/events?token=YOUR_AUTH_TOKEN'); // Using cookies (automatically sent) const eventSource = new EventSource('/api/events'); // Server-side validation app.get('/api/events', (req, res) => { const token = req.query.token || req.headers.authorization; if (!isValidToken(token)) { return res.status(401).send('Unauthorized'); } // Continue with SSE setup }); |
How do I debug SSE connections?
- Browser Developer Tools (Network tab shows EventSource connections)
- curl command:
curl -N -H "Accept: text/event-stream" http://your-server/events - Console logging in JavaScript event handlers
- Server-side logging of connection states
- Monitoring connection counts and error rates
