Appearance
Deploy to production
The local profile is wide open by design — no auth on the dashboard or the API. Before you expose Merido Gateway to anything but your own machine, do the security checklist first.
1. Security checklist (do this first)
Gate the dashboard and /api/*
Set a dashboard password hash. Generate it with the built-in helper (argon2):
bash
merido hash-password --password 'a-strong-password'It prints the env var to set:
bash
MERIDO_DASHBOARD_PASSWORD_HASH='$argon2id$...'With this set, the dashboard and the control-plane /api/* endpoints require a login session.
Require an API key on /v1/*
bash
MERIDO_REQUIRE_API_KEY=trueNow anonymous data-plane requests are rejected — callers must present a valid client (gateway) key.
Keep your master key stable
Encryption-at-rest (provider credentials, OAuth tokens) is sealed with the master key. Set MERIDO_MASTER_KEY to a stable base64 32-byte value and never rotate it casually — changing it orphans every stored secret. (Locally it lives at data_dir/master.key if you don't set the env var.)
2. Docker
The repo ships a multi-stage Dockerfile that builds the dashboard with bun, then the release binary:
bash
docker build -t merido .
docker run -p 8788:8788 \
-e MERIDO_DASHBOARD_PASSWORD_HASH='$argon2id$...' \
-e MERIDO_REQUIRE_API_KEY=true \
-e MERIDO_MASTER_KEY='...' \
meridoFor a shared deployment, run the cloud profile with Postgres:
bash
-e MERIDO_PROFILE=cloud \
-e DATABASE_URL='postgres://user:pass@host/merido'A bare DATABASE_URL (Postgres' own convention) is accepted. For high availability, also set REDIS_URL — see High availability.
3. Fly.io
Merido Gateway includes a Fly.io setup: fly.toml, scripts/deploy-fly.sh, and a smoke test (scripts/smoke-fly.sh).
Copy the secrets template and fill it in:
bashcp .env.fly.example .env.fly # edit .env.fly — it's gitignored; never commit secretsIt carries
DATABASE_URL,MERIDO_MASTER_KEY,MERIDO_DASHBOARD_PASSWORD_HASH,MERIDO_REQUIRE_API_KEY, and (optionally)REDIS_URL/MERIDO_RESEND_API_KEY.Deploy — the script pushes secrets and ships the app:
bash./scripts/deploy-fly.shVerify hardening:
bash./scripts/smoke-fly.sh
CircleCI
.circleci/config.yml builds this same Dockerfile on a CircleCI Linux machine executor (with Docker Layer Caching enabled) for pushes to main, pushes the image to Fly's registry as registry.fly.io/merido:<commit-sha>, and then deploys the manifest digest that this job just pushed (flyctl deploy --app merido --image registry.fly.io/merido@sha256:<digest> --yes), so the deployed bytes are the ones built here rather than whatever the tag points to later. The job is serialized (serial-group), so back-to-back pushes queue instead of racing an in-flight deploy. The SHA tag is kept for discoverability: while Fly still holds the image, you can roll back with flyctl deploy --app merido --image registry.fly.io/merido:<older-sha> — that restores the image only, not database or secret state.
One-time setup:
Create an app-scoped deploy token locally — it only manages the
meridoapp:bashfly tokens create deploy --app merido --expiry 720hIn CircleCI, create an empty context named
fly-production(Organization Settings → Contexts). Do not put the token in it yet.Restrict that context first — a fresh context is available to every project in the organization and branch checks alone do not scope it to this repo. Set a project restriction listing only this project, then add the expression restriction
pipeline.git.branch == "main" and not job.ssh.enabled and not (pipeline.config_source starts-with "api").Now add
FLY_API_TOKENtofly-productionwith the token from step 1, and rotate it before its expiry. In the project's settings keep Pass secrets to builds from forked pull requests disabled (fork builds must not receive contexts or env vars).Set Up CircleCI: connect the GitHub repo, make sure the checkout/config source is this repo, and keep a push trigger covering
main— otherwise the pipeline never runs. CircleCI then reads.circleci/config.ymland runs thedeploy-productionworkflow.
What the pipeline does not do:
- It does not create the app or upload runtime secrets.
--imageskips the image build, but the deploy still applies the checked-outfly.tomlto the app, and the existing secrets and volumes stay as they are. Run./scripts/deploy-fly.shonce (or whenever a secret in.env.flychanges) to push secrets; the pipeline only ships code. - It does not deploy tags, other branches, or anything from
.github/workflows/(the GitHubrelease.ymlworkflow builds CLI artifacts only, and stays as-is).
Operational notes: the pipeline only runs when a trigger fires and the job is admitted to the serial group — a job can wait in that queue for up to five hours before CircleCI cancels it, and CircleCI skips an older pipeline that tries to join while a newer one is queued or running. Do not use the Release serial group button in the web app if serialization has to hold.
Build cost: the first run compiles the whole Rust workspace plus the three web bundles, so check the timing on the first push before sizing resource_class. Docker Layer Caching costs 200 credits per job run and reuses unchanged image layers (for example the bun stages). Do not count on BuildKit cache-mount contents surviving between jobs, and budget for a full workspace rebuild whenever the compile layer changes. If CircleCI retires the pinned machine.image tag, bump it or switch to ubuntu-2604:current.
4. Verify it's locked down
After deploy, confirm the gates are live:
bash
curl https://your-app.example/healthz # 200, reports profile + db/redis status
curl https://your-app.example/v1/models # 401 without a client key
curl https://your-app.example/api/usage # 401 without a session/mgmt token5. Backup & restore
Merido Gateway can export a portable configuration document (POST /api/backup/export) and re-apply it on another instance (POST /api/backup/import).
Restore is additive and non-destructive — it is not a wipe-and-replace. Importing a backup creates only entries that don't already exist and upserts allow-listed settings; anything that already exists on the target is left unchanged (the response reports how many entries were skipped because they already existed). Restoring into a populated instance therefore merges the backup in rather than overwriting the current state.
Secrets are never in the backup, so a few things can't be recreated by a restore: gateway-key material is not exported (keys are reported but not restored), restored provider connections come back without credentials (re-add each API key), and proxy-pool URLs are not restored. Treat a restore as "re-create the shape of the config, then re-enter the secrets," not as a full clone.
See Backup & restore for the full export contents and the restore flow.
Next
- Multi-tenancy — let users self-serve with isolated orgs.
- High availability — run more than one instance.
- Backup & restore — export and re-apply your configuration.
- Exposing the gateway and TLS interception — networking.
- Monitoring and Audit log & SIEM — operate and govern.