GitLab MCP Server supports two approaches for secure HTTPS connections:
- Direct TLS Termination - Server handles TLS with certificate files
- Reverse Proxy - External proxy (nginx, Envoy, Caddy, Traefik) handles TLS
| Approach | Best For | HTTP/2 | Auto-Renewal |
|---|---|---|---|
| Direct TLS | Development, simple deployments | No | Manual |
| Reverse Proxy | Production, enterprise | Yes | Yes (Let's Encrypt) |
The server can directly handle TLS/HTTPS using certificate files. This is suitable for simple deployments or development environments.
| Variable | Description | Required |
|---|---|---|
SSL_CERT_PATH |
Path to PEM certificate file | Yes |
SSL_KEY_PATH |
Path to PEM private key file | Yes |
SSL_CA_PATH |
Path to CA certificate chain (for client cert validation) | No |
SSL_PASSPHRASE |
Passphrase for encrypted private keys | No |
# Generate self-signed certificate (for testing only)
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout server.key -out server.crt \
-subj "/CN=gitlab-mcp.local"docker run -d \
-e PORT=3000 \
-e SSL_CERT_PATH=/certs/server.crt \
-e SSL_KEY_PATH=/certs/server.key \
-e GITLAB_TOKEN=your_token \
-e GITLAB_API_URL=https://gitlab.com \
-v $(pwd)/certs:/certs:ro \
-p 3000:3000 \
ghcr.io/structured-world/gitlab-mcp:latest{
"mcpServers": {
"gitlab": {
"type": "streamable-http",
"url": "https://your-server.com:3000/mcp"
}
}
}For production deployments, use a reverse proxy to handle TLS termination. This provides:
- HTTP/2 support with proper ALPN negotiation
- Automatic certificate renewal (Let's Encrypt via Certbot, Caddy, etc.)
- Load balancing capabilities
- Better security (proxy filters traffic before reaching application)
- Centralized TLS management for multiple services
When behind a reverse proxy, configure TRUST_PROXY to properly handle X-Forwarded-* headers:
| Variable | Description |
|---|---|
TRUST_PROXY |
Enable Express trust proxy for X-Forwarded-* headers |
Trust Proxy Values:
| Value | Description |
|---|---|
true or 1 |
Trust all proxies (use only if you control all proxies) |
false or 0 |
Disable trust proxy |
loopback |
Trust loopback addresses (127.0.0.1, ::1) |
linklocal |
Trust link-local addresses (169.254.0.0/16, fe80::/10) |
uniquelocal |
Trust unique-local addresses (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7) |
Number (e.g., 1) |
Trust the nth hop from the front-facing proxy |
| IP addresses | Trust specific proxy IPs (comma-separated) |
Full nginx configuration with HTTP/2 and SSE support.
upstream gitlab_mcp {
server 127.0.0.1:3002;
keepalive 32;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name gitlab-mcp.example.com;
# TLS configuration
ssl_certificate /etc/letsencrypt/live/gitlab-mcp.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/gitlab-mcp.example.com/privkey.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
# Modern TLS configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# HSTS (optional, recommended)
add_header Strict-Transport-Security "max-age=63072000" always;
# Proxy settings for MCP
location / {
proxy_pass http://gitlab_mcp;
proxy_http_version 1.1;
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_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# SSE support (critical for MCP)
proxy_set_header Connection '';
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
chunked_transfer_encoding off;
}
}
# Redirect HTTP to HTTPS
server {
listen 80;
listen [::]:80;
server_name gitlab-mcp.example.com;
return 301 https://$server_name$request_uri;
}version: '3.8'
services:
gitlab-mcp:
image: ghcr.io/structured-world/gitlab-mcp:latest
environment:
- PORT=3002
- HOST=0.0.0.0
- TRUST_PROXY=true
- GITLAB_TOKEN=${GITLAB_TOKEN}
- GITLAB_API_URL=https://gitlab.com
expose:
- "3002"
networks:
- internal
nginx:
image: nginx:alpine
ports:
- "443:443"
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/letsencrypt:ro
depends_on:
- gitlab-mcp
networks:
- internal
networks:
internal:Envoy proxy with HTTP/2 and TLS support.
static_resources:
listeners:
- name: listener_https
address:
socket_address:
address: 0.0.0.0
port_value: 443
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
codec_type: AUTO
http2_protocol_options:
max_concurrent_streams: 100
route_config:
name: local_route
virtual_hosts:
- name: gitlab_mcp
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: gitlab_mcp_cluster
timeout: 0s # Disable timeout for SSE
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
common_tls_context:
tls_certificates:
- certificate_chain:
filename: /etc/envoy/certs/server.crt
private_key:
filename: /etc/envoy/certs/server.key
alpn_protocols: ["h2", "http/1.1"]
clusters:
- name: gitlab_mcp_cluster
connect_timeout: 30s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: gitlab_mcp_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: gitlab-mcp
port_value: 3002version: '3.8'
services:
gitlab-mcp:
image: ghcr.io/structured-world/gitlab-mcp:latest
environment:
- PORT=3002
- HOST=0.0.0.0
- TRUST_PROXY=true
- GITLAB_TOKEN=${GITLAB_TOKEN}
- GITLAB_API_URL=https://gitlab.com
expose:
- "3002"
networks:
- internal
envoy:
image: envoyproxy/envoy:v1.28-latest
ports:
- "443:443"
volumes:
- ./envoy.yaml:/etc/envoy/envoy.yaml:ro
- ./certs:/etc/envoy/certs:ro
depends_on:
- gitlab-mcp
networks:
- internal
networks:
internal:Caddy automatically obtains and renews TLS certificates via Let's Encrypt.
gitlab-mcp.example.com {
reverse_proxy gitlab-mcp:3002 {
flush_interval -1 # Required for SSE
}
}
version: '3.8'
services:
gitlab-mcp:
image: ghcr.io/structured-world/gitlab-mcp:latest
environment:
- PORT=3002
- HOST=0.0.0.0
- TRUST_PROXY=true
- GITLAB_TOKEN=${GITLAB_TOKEN}
- GITLAB_API_URL=https://gitlab.com
expose:
- "3002"
networks:
- internal
caddy:
image: caddy:alpine
ports:
- "443:443"
- "80:80"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
depends_on:
- gitlab-mcp
networks:
- internal
volumes:
caddy_data:
networks:
internal:Traefik with automatic Let's Encrypt certificates.
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
http2:
maxConcurrentStreams: 250
certificatesResolvers:
letsencrypt:
acme:
email: admin@example.com
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: webhttp:
routers:
gitlab-mcp:
rule: "Host(`gitlab-mcp.example.com`)"
entryPoints:
- websecure
service: gitlab-mcp
tls:
certResolver: letsencrypt
services:
gitlab-mcp:
loadBalancer:
servers:
- url: "http://gitlab-mcp:3002"version: '3.8'
services:
gitlab-mcp:
image: ghcr.io/structured-world/gitlab-mcp:latest
environment:
- PORT=3002
- HOST=0.0.0.0
- TRUST_PROXY=true
- GITLAB_TOKEN=${GITLAB_TOKEN}
- GITLAB_API_URL=https://gitlab.com
labels:
- "traefik.enable=true"
- "traefik.http.routers.gitlab-mcp.rule=Host(`gitlab-mcp.example.com`)"
- "traefik.http.routers.gitlab-mcp.entrypoints=websecure"
- "traefik.http.routers.gitlab-mcp.tls.certresolver=letsencrypt"
- "traefik.http.services.gitlab-mcp.loadbalancer.server.port=3002"
networks:
- internal
traefik:
image: traefik:v2.10
ports:
- "443:443"
- "80:80"
volumes:
- ./traefik.yml:/etc/traefik/traefik.yml:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- letsencrypt:/letsencrypt
networks:
- internal
volumes:
letsencrypt:
networks:
internal:HTTP/2 is best supported via reverse proxy for several reasons:
| Feature | Direct TLS | Reverse Proxy |
|---|---|---|
| HTTP/2 | Limited | Full support |
| ALPN Negotiation | Manual | Automatic |
| Connection Multiplexing | Basic | Optimized |
| HTTP/1.1 Fallback | Manual | Automatic |
| SSE Compatibility | Works | Works |
- ALPN Negotiation - Reverse proxies handle HTTP/2 protocol negotiation properly
- Connection Multiplexing - Proxies optimize HTTP/2 stream management
- Fallback Support - Automatic fallback to HTTP/1.1 for incompatible clients
- SSE Compatibility - Server-Sent Events work over HTTP/2 when properly configured
Note: Direct HTTP/2 support from Node.js Express requires additional setup and may have compatibility issues with some MCP clients. Using a reverse proxy is the recommended approach.
- Use TLS 1.2+ only - Disable TLS 1.0 and 1.1
- Modern cipher suites - Prefer ECDHE with AES-GCM
- Enable HSTS -
Strict-Transport-Securityheader - Certificate chain - Include full chain in certificate file
- Bind to localhost when using reverse proxy -
HOST=127.0.0.1 - Configure TRUST_PROXY correctly - Only trust proxies you control
- Use firewall rules - Restrict direct access to backend port
- Separate networks - Use Docker networks to isolate services
- Use Let's Encrypt for automatic renewal (Caddy, Certbot)
- Monitor expiration - Set up alerts for certificate expiry
- Rotate certificates - Don't use certificates beyond their validity
- Secure private keys - Restrict file permissions (chmod 600)
- TLS 1.2+ only, modern cipher suites
- HSTS header enabled
- Certificate auto-renewal configured
- Backend bound to localhost or internal network
- TRUST_PROXY set appropriately
- Firewall rules in place
- Monitoring and alerting configured
"Certificate not trusted"
- Ensure full certificate chain is included
- Check certificate matches domain name
- Verify certificate is not expired
"Unable to read certificate"
- Check file permissions (readable by server process)
- Verify paths are correct and absolute
- Ensure certificate is in PEM format
"Connection refused"
- Check server is running and listening on correct port
- Verify firewall allows traffic
- Check HOST binding (0.0.0.0 vs 127.0.0.1)
"SSE connection drops"
- Disable proxy buffering (
proxy_buffering off) - Set long read timeout (
proxy_read_timeout 86400s) - Clear connection header (
proxy_set_header Connection '')
"req.ip shows proxy IP instead of client IP"
- Ensure TRUST_PROXY is set correctly
- Verify proxy sends X-Forwarded-For header
- Check proxy hop count if using numeric value
| Variable | Description | Default |
|---|---|---|
SSL_CERT_PATH |
PEM certificate file path | - |
SSL_KEY_PATH |
PEM private key file path | - |
SSL_CA_PATH |
CA certificate chain path | - |
SSL_PASSPHRASE |
Private key passphrase | - |
TRUST_PROXY |
Trust proxy setting | - |
HOST |
Server bind address | 127.0.0.1 |
PORT |
Server listen port | 3002 |