Self-Hosting¶
Run Meridian on your own infrastructure. Choose the path that matches how much control you want versus how much setup you are willing to do.
Choose a setup path¶
The fastest path for local or small-team usage. No Python required.
Windows
- Download
meridian.exe - Double-click to run
- Meridian opens your browser at
http://localhost:7878
macOS (Apple Silicon)
curl -Lo meridian https://github.com/meridianmcp/Meridian/releases/latest/download/meridian-mac-arm64
chmod +x meridian
xattr -d com.apple.quarantine meridian 2>/dev/null || true
./meridian
Linux
Use the published Docker Hub image when you want a clean containerized setup.
docker pull meridianmcp/meridian:latest
docker run --rm -p 7878:7878 --env-file .env \
-v "$(pwd)/data:/app/data" \
meridianmcp/meridian:latest
If you prefer source-based Compose:
By default, SQLite data is persisted in ./data/meridian.db.
Best for contributors and active development.
For hot reload:
Confirm the server is healthy:
The dashboard is at http://localhost:7878.
Postgres setup with Neon¶
Neon is a good default when you want a hosted Postgres backend for a self-hosted Meridian server.
- Create a project at neon.tech
- Copy the Postgres connection string
- Set
MERIDIAN_DB_URL - Start Meridian normally
export MERIDIAN_DB_URL="postgresql://neondb_owner:...@ep-xxx.us-east-2.aws.neon.tech/neondb?sslmode=require"
pixi run start
Meridian auto-detects Postgres from the URL prefix and switches to psycopg3
(the connection pool, autocommit, and adapter translation live in
meridian/pg_adapter.py; SQLite remains the default for local installs).
Environment variables¶
See the full Configuration Reference.
Key variables for self-hosting:
| Variable | What to set |
|---|---|
SESSION_SECRET |
Long random string used to sign session cookies |
APP_URL |
Public base URL such as https://meridian.yourdomain.com |
MERIDIAN_DB_URL |
Optional Postgres URL; SQLite is fine for smaller installs |
SITE_PASSWORD |
Optional password gate for preview deployments |
Example .env:
SESSION_SECRET=replace-me
# APP_URL=https://meridian.example.com
# MERIDIAN_DB_URL=postgresql://user:pass@host/dbname
# SITE_PASSWORD=preview-password
Expose localhost for browser clients¶
Claude remote connectors and ChatGPT custom apps need a public HTTPS MCP
endpoint. If you are running Meridian on localhost, a quick test path is a
Cloudflare Tunnel:
Cloudflare prints a temporary public URL such as https://random-name.trycloudflare.com.
Use that host plus /mcp in Claude or ChatGPT:
Note
This is great for demos and short-lived testing. For a stable setup, put
Meridian behind your own domain and reverse proxy, then set APP_URL
accordingly.
Reverse proxy setup¶
nginx¶
server {
listen 80;
server_name meridian.yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name meridian.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/meridian.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/meridian.yourdomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:7878;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
}
}
WebSocket support
Meridian uses WebSockets for the live dashboard. Keep the Upgrade and
Connection headers in place.
Caddy¶
meridian.yourdomain.com {
reverse_proxy localhost:7878 {
header_up Host {host}
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Proto {scheme}
}
}
Caddy handles TLS automatically.
Proxy on another host¶
Meridian believes X-Forwarded-For and X-Forwarded-Proto only when they come
from loopback (127.0.0.1, ::1) or a private range (10.0.0.0/8,
172.16.0.0/12, 192.168.0.0/16, fc00::/7). The nginx and Caddy examples
above reach Meridian from the same host or the Docker gateway, so they are
covered.
If your reverse proxy or load balancer runs on another host with a public
address, its forwarded headers are ignored. Set MERIDIAN_FORWARDED_ALLOW_IPS
to that proxy's address or range:
The value replaces the default list rather than adding to it. If anything on loopback or the Docker gateway still needs forwarded headers, list those too. Leave the variable unset or empty to keep the default. Hosted deployments ignore it, and it does not change the Zotero connect route, which refuses forwarded requests either way.
Residual risk
Docker's default userland proxy can make traffic on a published port look
as if it came from a private gateway address such as 172.17.0.1. That
address is in the default list, so a client that reaches the port directly
can send its own X-Forwarded-For and X-Forwarded-Proto and have them
believed. The docker-compose.yml in this repo publishes port 7878 on all
interfaces. Bind it to 127.0.0.1 in ports, or firewall it, so only your
proxy can reach it.
Zotero connect¶
The Zotero connect route is off by default on a self-hosted server. To turn
it on, set MERIDIAN_ZOTERO_CONNECT_ENABLED=1 (true and yes also work) in
the environment that starts Meridian, then restart Meridian. With the switch
unset, the route answers 403.
- Run Meridian directly on the same machine as Zotero (for example with
pixi run start), not in the Docker container. The approval is stored in the OS keyring of the machine that runs Meridian. A container has no such keyring, and requests reaching it usually arrive from the Docker gateway rather than127.0.0.1, so the route refuses them. - The Zotero desktop app must run on that same machine, with its local API turned on in Zotero's Advanced settings.
- The route accepts a request only from
127.0.0.1or::1, and only when the request carries no forwarding headers. - A reverse proxy in front of Meridian on the same machine will not work with
this route, because forwarded requests are refused. The nginx and Caddy
examples above send
X-Forwarded-Forand similar headers, and the route answers 403 to any request that carries them. Open Meridian athttp://localhost:7878when you connect to Zotero.
Upgrading¶
cd Meridian
git pull origin main
pixi install # or: docker pull meridianmcp/meridian:latest
pixi run start # or: docker compose up -d
Meridian runs database migrations automatically on startup.
Backup and restore¶
SQLite¶
# Backup
cp data/meridian.db "data/meridian-$(date +%Y%m%d).db"
# Restore
cp data/meridian-20260101.db data/meridian.db
Automated daily backup example:
0 2 * * * cp /path/to/Meridian/data/meridian.db \
/backups/meridian-$(date +%Y%m%d).db && \
find /backups -name "meridian-*.db" -mtime +7 -delete
Postgres¶
pg_dump "$MERIDIAN_DB_URL" -Fc -f meridian-backup-$(date +%Y%m%d).dump
pg_restore -d "$MERIDIAN_DB_URL" meridian-backup-20260101.dump
Running multiple instances¶
SQLite does not support multiple writers
If you need multiple Meridian instances for HA or load balancing, move to Postgres first.
services:
meridian-1:
image: meridianmcp/meridian:latest
ports: ["7878:7878"]
environment:
- MERIDIAN_DB_URL=postgresql://...
meridian-2:
image: meridianmcp/meridian:latest
ports: ["7879:7878"]
environment:
- MERIDIAN_DB_URL=postgresql://...
Put nginx or Caddy in front once you scale past a single instance.