Skip to content

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.

-> Getting Started


Choose a setup path

The fastest path for local or small-team usage. No Python required.

Windows

  1. Download meridian.exe
  2. Double-click to run
  3. 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

curl -Lo meridian https://github.com/meridianmcp/Meridian/releases/latest/download/meridian-linux
chmod +x meridian
./meridian

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:

git clone https://github.com/meridianmcp/Meridian
cd Meridian
docker compose up -d

By default, SQLite data is persisted in ./data/meridian.db.

Best for contributors and active development.

git clone https://github.com/meridianmcp/Meridian
cd Meridian
pixi install
pixi run start

For hot reload:

pixi run dev

Use this when pixi is not available.

git clone https://github.com/meridianmcp/Meridian
cd Meridian
python -m venv .venv
source .venv/bin/activate   # Windows: .venv\\Scripts\\activate
pip install -e ".[full]"
python -m meridian

Confirm the server is healthy:

curl http://localhost:7878/health
# -> {"status": "ok", "version": "...", "db": "sqlite"}

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.

  1. Create a project at neon.tech
  2. Copy the Postgres connection string
  3. Set MERIDIAN_DB_URL
  4. 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:

cloudflared tunnel --url http://localhost:7878

Cloudflare prints a temporary public URL such as https://random-name.trycloudflare.com. Use that host plus /mcp in Claude or ChatGPT:

https://random-name.trycloudflare.com/mcp

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:

services:
  meridian:
    environment:
      - MERIDIAN_FORWARDED_ALLOW_IPS=203.0.113.10

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 than 127.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.1 or ::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-For and similar headers, and the route answers 403 to any request that carries them. Open Meridian at http://localhost:7878 when 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.