Skip to main content

09 โ€” Deployment

Distributionโ€‹

  • OCI image: ghcr.io/developerz-ai/db-mcp-gateway:<version> on GitHub Container Registry. Multi-arch (amd64, arm64). Distroless base. Single static binary inside. Public, free, no Docker Hub rate limits.
  • Release trigger: pushing a v* git tag fires .github/workflows/release.yml, which builds the multi-arch image, pushes it to GHCR, and cuts a GitHub Release with auto-generated notes.
  • GitHub Releases: prebuilt static binaries for Linux amd64 / arm64 and macOS (dev convenience) attached to the same release.
  • Helm chart: published alongside the image on the same tag (roadmap, phase 8).
  • Example repo: a separate small repo with docker-compose.yml + skeleton config.yaml + provisioning SQL for the DB roles. Fork-and-edit shape.

Minimum viable deploymentโ€‹

# docker-compose.yml โ€” minimal
services:
gateway:
image: ghcr.io/developerz-ai/db-mcp-gateway:latest
ports: ["8443:8443"]
environment:
STATE_DB_URL: postgres://gateway:${STATE_DB_PW}@state-db:5432/gateway
OIDC_CLIENT_ID: ${OIDC_CLIENT_ID}
OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET}
volumes:
- ./config.yml:/etc/gateway/config.yml:ro
depends_on: [state-db]

state-db:
image: postgres:16
environment:
POSTGRES_DB: gateway
POSTGRES_USER: gateway
POSTGRES_PASSWORD: ${STATE_DB_PW}
volumes:
- state-db-data:/var/lib/postgresql/data

volumes:
state-db-data:

That's the whole thing. Put it behind your existing TLS-terminating reverse proxy or expose 8443 directly with the gateway's built-in TLS.

Provisioning the target databasesโ€‹

The repo ships SQL templates for the read-only roles, per DB engine. For Postgres:

-- run once per database the gateway will access
create role mcp_gateway_prod_app_ro with login password :'pw';
grant connect on database app to mcp_gateway_prod_app_ro;
grant usage on schema public to mcp_gateway_prod_app_ro;
grant select on all tables in schema public to mcp_gateway_prod_app_ro;
alter default privileges in schema public
grant select on tables to mcp_gateway_prod_app_ro;
alter role mcp_gateway_prod_app_ro set statement_timeout = '30s';
alter role mcp_gateway_prod_app_ro set idle_in_transaction_session_timeout = '60s';

Operators run this with their own admin credentials. The gateway never gets superuser.

State DBโ€‹

Co-deployed Postgres. Tiny โ€” audit log is the bulky table and it's pruned (see 07-logging-retention). One disk, hourly backups, nothing exotic. Can be the host's existing Postgres if there's a good reason; not recommended because mixing security-sensitive audit data with general app data is asking for trouble.

TLSโ€‹

  • Gateway terminates TLS in-process via rustls. Reads cert + key paths from TLS_CERT_PATH / TLS_KEY_PATH.
  • Refuses to boot without TLS unless TLS_DISABLED=true (dev-only escape; loud WARN log on every startup).
  • SIGHUP reloads the cert + key from the same paths without dropping live connections โ€” rustls re-reads the handshake material atomically, in-flight sessions keep the old cert until they close. Matches cert-manager's rotation contract.
  • Reload failures (malformed PEM after a swap, wrong permissions) log the error and keep the previous cert serving; the next SIGHUP retries.
  • Cert-manager + the Kustomize stack in developerz-ai/infrastructure mounts the cert as a Certificate resource. Cert-manager does NOT send SIGHUP itself โ€” a reload controller (e.g. stakater/reloader) or a lifecycle/sidecar hook must be wired in to translate the renewal into a SIGHUP on the pod. Without that wiring the gateway will keep serving the previous cert until restart.
  • For docker-compose without cert-manager, mount your PEMs into the container and docker exec ... kill -HUP 1 after rotating them.

Networkingโ€‹

Two paths must be open:

FromToWhy
Developer laptopGatewayAgents talk to gateway over HTTPS
GatewayTarget DBsGateway connects to each DB on its port
GatewayIdPOIDC discovery + token validation
GatewaySecrets backendPull DB credentials
GatewayObject storage (optional)Audit archive

Run the gateway in the network segment that already reaches the DBs. Most installs put it next to existing internal tools.

Connection pooling and proxiesโ€‹

Rule: When fronting databases with a connection pool proxy (e.g. pgcat), route the gateway through the proxy endpoint and set TLS based on network scope.

Scenarioconfig.servers[*].hostporttlsNotes
Direct to Postgres (no proxy)prod-rw.db.example.com5432requiredStandard database connection.
Via cluster-internal proxy (Kubernetes)pgcat.pgcat.svc.cluster.local6432insecureGateway โ†’ proxy โ†’ DB legs are mesh traffic; TLS terminates at gateway's public ingress (Traefik). Logs WARN every minute (tls: insecure is loud by design).
Via cloud-managed proxy (RDS Proxy)prod-proxy.proxy.us-east-1.rds.amazonaws.com5432requiredEncryption in-transit expected; network isolation managed by cloud provider.

Verify the proxy endpoint and port with your infrastructure team. Proxy unavailability will appear in the gateway's startup logs and connection-attempt logs, or the proxy's own health endpoint โ€” not in the gateway's /readyz probe (which only checks state DB reachability and the shutdown flag).

Upgradesโ€‹

  • Containers: tag-pinned pulls; the operator bumps the tag and redeploys.
  • Migrations: the gateway runs its own state-DB migrations on startup; safe to roll across versions one minor at a time.
  • Backward compat: config schema changes that drop or rename fields ship with a one-version deprecation window; the gateway warns when it sees the old name.

Observabilityโ€‹

  • /healthz (liveness) โ€” always returns 200 if the binary is up.
  • /readyz (readiness) โ€” 200 only when the state DB is reachable (and the gateway is not shutting down).
  • /metrics (Prometheus): per-tool latency histograms, per-database connection counts, auth failures, audit write latency.
  • Structured JSON logs to stdout.

See 11-roadmap for what's deferred (k8s operator, multi-replica leader election, etc.).