Docker with PostgreSQL Deployment
Run GitLab MCP with an external PostgreSQL database for OAuth session persistence and multi-instance support.
PostgreSQL ships in a separate image
The default gitlab-mcp image uses in-memory or file storage and does not include PostgreSQL (keeping it lightweight). The PostgreSQL backend lives in the optional gitlab-mcp-db image, layered on top of the core image — it adds the Prisma-based backend. Use ghcr.io/structured-world/gitlab-mcp-db and set OAUTH_STORAGE_TYPE=postgresql to enable it.
When to Use
- Production deployments with OAuth
- Teams needing persistent user sessions
- Multi-instance GitLab configurations
- When you already have a PostgreSQL server
Prerequisites
- Docker installed and running
- PostgreSQL server (14+) accessible from the container
- GitLab OAuth application(s) registered
Setup
1. Prepare PostgreSQL
Create a database for GitLab MCP:
CREATE DATABASE gitlab_mcp;
CREATE USER gitlab_mcp WITH PASSWORD 'your_secure_password';
GRANT ALL PRIVILEGES ON DATABASE gitlab_mcp TO gitlab_mcp;2. Run the Container
Use the gitlab-mcp-db image (it carries the PostgreSQL backend) and set OAUTH_STORAGE_TYPE=postgresql:
docker run -d --name gitlab-mcp \
-e PORT=3002 \
-e HOST=0.0.0.0 \
-e OAUTH_ENABLED=true \
-e OAUTH_ISSUER=https://mcp.example.com \
-e OAUTH_STORAGE_TYPE=postgresql \
-e OAUTH_STORAGE_POSTGRESQL_URL="postgresql://gitlab_mcp:your_secure_password@db-host:5432/gitlab_mcp" \
-e OAUTH_SESSION_SECRET="$(openssl rand -hex 32)" \
-e GITLAB_API_URL=https://gitlab.com \
-e OAUTH_CLIENT_ID=your_oauth_app_id \
-p 3333:3002 \
ghcr.io/structured-world/gitlab-mcp-db:latest3. Configure Clients
Clients connect to OAUTH_ISSUER with /mcp appended; the discovery metadata names that URL as the resource, so any other address fails discovery.
For example, Claude Code uses this HTTP configuration:
{
"mcpServers": {
"gitlab": {
"type": "http",
"url": "https://mcp.example.com/mcp"
}
}
}For local testing set OAUTH_ISSUER=http://localhost:3333 and connect to http://localhost:3333/mcp.
Other clients use different configuration formats. Follow the client-specific instructions.
Environment Variables
| Variable | Required | Description |
|---|---|---|
PORT | Yes | Internal HTTP port |
OAUTH_ENABLED | Yes | Set to true to enable per-user OAuth (required for the database backend) |
OAUTH_ISSUER | Yes | Public URL clients connect to; issuer of every token |
OAUTH_STORAGE_TYPE | Yes | Set to postgresql to use the database backend (requires the gitlab-mcp-db image) |
OAUTH_STORAGE_POSTGRESQL_URL | Yes | PostgreSQL connection string (DATABASE_URL is also accepted as a fallback) |
OAUTH_SESSION_SECRET | Yes | Secret for session encryption |
OAUTH_CLIENT_ID | Yes | GitLab OAuth Application ID |
OAUTH_CLIENT_SECRET | No | GitLab OAuth Application Secret (only for confidential apps; PKCE public apps omit it) |
GITLAB_API_URL | No | Default GitLab instance URL |
Database Schema
The database package ships its Prisma schema; server startup does not apply schema changes. Apply the packaged schema through your database deployment process before starting an upgraded server. It defines oauth_sessions, oauth_device_flows, oauth_auth_code_flows, oauth_authorization_codes and oauth_mcp_session_mappings.
The client-contract update adds nullable JSONB columns for granted session scopes and the scopes originally requested in each authorization flow. Existing databases must add these columns before using the updated database package:
ALTER TABLE oauth_sessions ADD COLUMN gitlab_scopes JSONB;
ALTER TABLE oauth_device_flows ADD COLUMN requested_gitlab_scopes JSONB;
ALTER TABLE oauth_auth_code_flows ADD COLUMN requested_gitlab_scopes JSONB;Leave existing rows as SQL NULL: the upstream grant is unknown until GitLab reports it during authentication or token refresh. An empty JSON array means a known grant with no scopes. Do not copy MCP client scopes into this column or infer full GitLab access for older sessions.
New flows retain the scopes sent to GitLab so an omitted token-response scope can be interpreted as the unchanged requested grant, as required by OAuth 2.0 §5.1. Leave older flows' requested scopes as SQL NULL rather than inferring them from configuration that may have changed since the flow began.
Migrations
The package ships Prisma migrations in prisma/migrations and a migration command (connection string from OAUTH_STORAGE_POSTGRESQL_URL or DATABASE_URL). Run it before starting an upgraded server:
cd node_modules/@structured-world/gitlab-mcp-db
node dist/src/migrate.jsIt waits up to a minute for the database to accept connections, then applies the pending migrations with prisma migrate deploy:
- New database: every table is created.
- Existing database created from the packaged schema before migrations were shipped (including the
ALTER TABLEstatements above): OAuth tables without migration history are detected, the baseline0_initis marked applied, and the remaining migrations are deployed.
The gitlab-mcp-db image runs the same command before the server containers start:
docker run --rm \
-e OAUTH_STORAGE_POSTGRESQL_URL="postgresql://gitlab_mcp:your_secure_password@db-host:5432/gitlab_mcp" \
-w /app/node_modules/@structured-world/gitlab-mcp-db \
--entrypoint node \
ghcr.io/structured-world/gitlab-mcp-db:latest \
dist/src/migrate.jsThe account-linking migration only adds nullable columns and the oauth_clients table, so existing sessions and flows stay valid. It stores Dynamic Client Registrations, the resource, scope and GitLab instance each authorization is bound to, and the device flow's client state and poll schedule, so every replica sees the same registrations, codes and sessions.
Multi-Instance Support
Add multiple GitLab instances via the CLI:
gitlab-mcp docker add-instance gitlab.com
gitlab-mcp docker add-instance gitlab.company.comEach instance can have its own OAuth application and default preset. The CLI writes the instance list for the server to instances.env (read by the container at every start, so docker compose restart picks up added instances), and an instance's OAuth secret is read from the variable the CLI names, which goes into .env.
Scaling
For high-availability deployments:
- Use a managed PostgreSQL service (RDS, Cloud SQL, etc.)
- Run multiple container replicas behind a load balancer
- Set
OAUTH_SESSION_SECRETto the same value across all replicas - With the PostgreSQL backend the OAuth endpoints (
/register,/authorize,/oauth/callback,/oauth/poll,/token,/revoke) need no session affinity: client registrations, authorization codes, refresh-token rotation and device flows are stored and redeemed atomically in the database - MCP transport sessions (
/mcpwithMcp-Session-Id,/sseand/messages) live in the replica that opened them: route them with session affinity
Security Notes
- Store
OAUTH_STORAGE_POSTGRESQL_URLand secrets in environment variables, never in docker-compose.yml - Use a dedicated database user with minimal privileges
- Enable SSL for the PostgreSQL connection:
?sslmode=requirein the connection string - Rotate
OAUTH_SESSION_SECRETperiodically (invalidates existing sessions)
See Also
- OAuth Authentication
- Docker Compose — bundled PostgreSQL
- TLS/HTTPS Configuration
