Deploy ContextForge on IBM Cloud with Code EngineΒΆ
This guide covers two supported deployment paths for the ContextForge:
- Makefile automation - a single-command workflow that wraps
ibmcloudCLI. - Manual IBM Cloud CLI - the raw commands the Makefile executes, for fine-grained control.
1 - PrerequisitesΒΆ
| Requirement | Details |
|---|---|
| IBM Cloud account | Create one if needed |
| Docker or Podman | Builds the production container image locally |
| IBM Cloud CLI β₯ 2.16 | Installed automatically with make ibmcloud-cli-install |
| Code Engine project | Create or select one in the IBM Cloud console |
.env file | Runtime secrets & config for the gateway |
.env.ce file | Deployment credentials & metadata for Code Engine / Container Reg. |
2 - Environment filesΒΆ
Both files are already in .gitignore. Templates named .env.example and .env.ce.example are included; copy them:
cp .env.example .env # runtime settings (inside the container)
cp .env.ce.example .env.ce # deployment credentials (CLI only)
.env - runtime settingsΒΆ
This file is mounted into the container (via --env-file=.env), so its keys live inside Code Engine at runtime. Treat it as an application secret store.
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Core gateway settings
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
AUTH_REQUIRED=true
# Generate once: openssl rand -hex 32
JWT_SECRET_KEY=eef5e9f70ca7fe6f9677ad2acaf4d32c55e9d98e9cb74299b33f5c5d1a3c8ef0
HOST=0.0.0.0
PORT=4444
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Database configuration - choose ONE block
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
## (A) Local SQLite (good for smoke-tests / CI only)
## --------------------------------------------------
## - SQLite lives on the container's ephemeral file system.
## - On Code Engine every new instance starts fresh; scale-out, restarts or
## deploys will wipe data. **Not suitable for production.**
## - If you still need file persistence, attach Code Engine's file-system
## mount or an external filesystem / COS bucket.
#CACHE_TYPE=database
#DATABASE_URL=sqlite:////tmp/mcp.db
## (B) Managed PostgreSQL on IBM Cloud (recommended for staging/production)
## --------------------------------------------------------------------------
## - Provision an IBM Cloud Databases for PostgreSQL instance (see below).
## - Use the service credentials to build the URL.
## - sslmode=require is mandatory for IBM Cloud databases.
CACHE_TYPE=database
DATABASE_URL=postgresql+psycopg://pguser:pgpass@my-pg-host.databases.appdomain.cloud:32727/mcpgwdb?sslmode=require
# β β β β β
# β β β β ββ database name
# β β β ββ hostname:port
# β β ββ password
# β ββ username
# ββ scheme
The JWT_SECRET_KEY variable is used to generate a Bearer token used to access the APIs. To access the APIs you need to generate your JWT token using the same JWT_SECRET_KEY, for example:
# Generate a one-off token for the default admin user
export MCPGATEWAY_BEARER_TOKEN=$(python3 -m mcpgateway.utils.create_jwt_token -u admin@example.com)
echo ${MCPGATEWAY_BEARER_TOKEN} # Check that the key was generated
.env.ce - Code Engine deployment settingsΒΆ
These keys are only consumed by Makefile / CLI. They never reach the running container.
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# IBM Cloud / Code Engine deployment variables
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
IBMCLOUD_REGION=us-south
IBMCLOUD_RESOURCE_GROUP=default
IBMCLOUD_PROJECT=my-codeengine-project
IBMCLOUD_CODE_ENGINE_APP=mcpgateway
# Image details
IBMCLOUD_IMAGE_NAME=us.icr.io/myspace/mcpgateway:latest # target in IBM Container Registry
IBMCLOUD_IMG_PROD=mcpgateway/mcpgateway # local tag produced by Make
# Authentication
IBMCLOUD_API_KEY=***your-api-key*** # leave blank to use SSO flow at login
# Resource combo - see https://cloud.ibm.com/docs/codeengine?topic=codeengine-mem-cpu-combo
IBMCLOUD_CPU=1 # vCPU for the container
IBMCLOUD_MEMORY=4G # Memory (must match a valid CPU/MEM pair)
# Registry secret in Code Engine (first-time creation is automated)
IBMCLOUD_REGISTRY_SECRET=my-regcred
Tip: run
make ibmcloud-check-envto verify every requiredIBMCLOUD_*key is present in.env.ce.
Verify environment filesΒΆ
# Confirm both env files exist and contain required keys
test -f .env && echo "OK: .env exists" || echo "MISSING: .env"
test -f .env.ce && echo "OK: .env.ce exists" || echo "MISSING: .env.ce"
grep -q DATABASE_URL .env && echo "OK: DATABASE_URL set" || echo "WARNING: DATABASE_URL not set in .env"
grep -q JWT_SECRET_KEY .env && echo "OK: JWT_SECRET_KEY set" || echo "WARNING: JWT_SECRET_KEY not set in .env"
3 - Workflow A - Makefile targetsΒΆ
| Target | Action it performs |
|---|---|
podman / docker | Build the production image ($IBMCLOUD_IMG_PROD). |
ibmcloud-cli-install | Install IBM Cloud CLI + container-registry and code-engine plugins. |
ibmcloud-check-env | Ensure all IBMCLOUD_* vars exist in .env.ce; abort if any are missing. |
ibmcloud-login | ibmcloud login - uses API key or interactive SSO. |
ibmcloud-ce-login | ibmcloud ce project select --name $IBMCLOUD_PROJECT. |
ibmcloud-list-containers | Show ICR images and existing Code Engine apps. |
ibmcloud-tag | podman tag $IBMCLOUD_IMG_PROD $IBMCLOUD_IMAGE_NAME. |
ibmcloud-push | ibmcloud cr login + podman push to ICR. |
ibmcloud-deploy | Create or update the app, set CPU/MEM, attach registry secret, expose port 4444. |
ibmcloud-ce-status | ibmcloud ce application get - see route URL, revisions, health. |
ibmcloud-ce-logs | ibmcloud ce application logs --follow - live log stream. |
ibmcloud-ce-rm | Delete the application entirely. |
Typical first deploy
make ibmcloud-check-env
make ibmcloud-cli-install
make ibmcloud-login
make ibmcloud-ce-login
make podman # or: make docker
make ibmcloud-tag
make ibmcloud-push
make ibmcloud-deploy
Redeploy after code changes
4 - Workflow B - Manual IBM Cloud CLIΒΆ
# 1 - Install CLI + plugins
curl -fsSL https://clis.cloud.ibm.com/install/linux | sh
ibmcloud plugin install container-registry -f
ibmcloud plugin install code-engine -f
# 2 - Login
ibmcloud login --apikey "$IBMCLOUD_API_KEY" -r "$IBMCLOUD_REGION" -g "$IBMCLOUD_RESOURCE_GROUP"
ibmcloud resource groups # list resource groups
# 3 - Target Code Engine project
ibmcloud ce project list # list current projects
ibmcloud ce project select --name "$IBMCLOUD_PROJECT"
# 4 - Build + tag image
podman build -t "$IBMCLOUD_IMG_PROD" .
podman tag "$IBMCLOUD_IMG_PROD" "$IBMCLOUD_IMAGE_NAME"
# 5 - Push image to ICR
ibmcloud cr login
ibmcloud cr namespaces # Ensure your namespace exists
podman push "$IBMCLOUD_IMAGE_NAME"
ibmcloud cr images # list images
Verify image in registryΒΆ
# Confirm the image is present in ICR
ibmcloud cr images --restrict "$(echo "$IBMCLOUD_IMAGE_NAME" | cut -d/ -f2)"
# Expected: your image listed with "No Issues" vulnerability status
# 6 - Create registry secret (first time)
ibmcloud ce registry create-secret --name "$IBMCLOUD_REGISTRY_SECRET" \
--server "$(echo "$IBMCLOUD_IMAGE_NAME" | cut -d/ -f1)" \
--username iamapikey --password "$IBMCLOUD_API_KEY"
ibmcloud ce secret list # list every secret (generic, registry, SSH, TLS, etc.)
ibmcloud ce secret get --name "$IBMCLOUD_REGISTRY_SECRET" # add --decode to see clear-text values
# 7 - Deploy / update
if ibmcloud ce application get --name "$IBMCLOUD_CODE_ENGINE_APP" >/dev/null 2>&1; then
ibmcloud ce application update --name "$IBMCLOUD_CODE_ENGINE_APP" \
--image "$IBMCLOUD_IMAGE_NAME" \
--cpu "$IBMCLOUD_CPU" --memory "$IBMCLOUD_MEMORY" \
--registry-secret "$IBMCLOUD_REGISTRY_SECRET"
else
ibmcloud ce application create --name "$IBMCLOUD_CODE_ENGINE_APP" \
--image "$IBMCLOUD_IMAGE_NAME" \
--cpu "$IBMCLOUD_CPU" --memory "$IBMCLOUD_MEMORY" \
--port 4444 \
--registry-secret "$IBMCLOUD_REGISTRY_SECRET"
fi
Verify application is runningΒΆ
# Check application status
ibmcloud ce application get --name "$IBMCLOUD_CODE_ENGINE_APP" | grep -E "Status|URL|Ready"
# Expected: Status showing "Ready", URL showing the public endpoint
# Quick health check
APP_URL=$(ibmcloud ce application get --name "$IBMCLOUD_CODE_ENGINE_APP" --output url)
curl -s "${APP_URL}/health" | jq .
# Expected: {"status": "healthy"}
# 8 - Status & logs
ibmcloud ce application get --name "$IBMCLOUD_CODE_ENGINE_APP"
ibmcloud ce application events --name "$IBMCLOUD_CODE_ENGINE_APP"
ibmcloud ce application get --name "$IBMCLOUD_CODE_ENGINE_APP"
ibmcloud ce application logs --name "$IBMCLOUD_CODE_ENGINE_APP" --follow
5 - Accessing the gatewayΒΆ
Open the returned URL (e.g. https://mcpgateway.us-south.codeengine.appdomain.cloud/admin) and log in with the basic-auth credentials from .env.
Test the API endpoints with the generated MCPGATEWAY_BEARER_TOKEN:
# Generate a one-off token for the default admin user
export MCPGATEWAY_BEARER_TOKEN=$(python3 -m mcpgateway.utils.create_jwt_token -u admin@example.com)
# Call a protected endpoint. Since there are no tools, initially this just returns `[]`
curl -H "Authorization: Bearer ${MCPGATEWAY_BEARER_TOKEN}" \
https://mcpgateway.us-south.codeengine.appdomain.cloud/tools
# Check the logs
make ibmcloud-ce-logs
6 - CleanupΒΆ
# via Makefile
make ibmcloud-ce-rm
# or directly
ibmcloud ce application delete --name "$IBMCLOUD_CODE_ENGINE_APP" -f
7 - Using IBM Cloud Databases for PostgreSQLΒΆ
Need durable data, high availability, and automated backups? Provision IBM Cloud Databases for PostgreSQL and connect ContextForge to it.
###############################################################################
# 1 - Provision PostgreSQL
###############################################################################
# Choose a plan: standard (shared) or enterprise (dedicated). For small
# workloads start with: standard / 1 member / 4 GB RAM.
ibmcloud resource service-instance-create mcpgw-db \
databases-for-postgresql standard $IBMCLOUD_REGION
###############################################################################
# 2 - Create service credentials
###############################################################################
ibmcloud resource service-key-create mcpgw-db-creds Administrator \
--instance-name mcpgw-db
###############################################################################
# 3 - Retrieve credentials & craft DATABASE_URL
###############################################################################
creds_json=$(ibmcloud resource service-key mcpgw-db-creds --output json)
host=$(echo "$creds_json" | jq -r '.[0].credentials.connection.postgres.hosts[0].hostname')
port=$(echo "$creds_json" | jq -r '.[0].credentials.connection.postgres.hosts[0].port')
user=$(echo "$creds_json" | jq -r '.[0].credentials.connection.postgres.authentication.username')
pass=$(echo "$creds_json" | jq -r '.[0].credentials.connection.postgres.authentication.password')
db=$(echo "$creds_json" | jq -r '.[0].credentials.connection.postgres.database')
DATABASE_URL="postgresql+psycopg://${user}:${pass}@${host}:${port}/${db}?sslmode=require"
###############################################################################
# 4 - Store DATABASE_URL as a Code Engine secret
###############################################################################
ibmcloud ce secret create --name mcpgw-db-url \
--from-literal DATABASE_URL="$DATABASE_URL"
###############################################################################
# 5 - Mount the secret into the application
###############################################################################
ibmcloud ce application update --name "$IBMCLOUD_CODE_ENGINE_APP" \
--env-from-secret mcpgw-db-url
Verify PostgreSQL connectivityΒΆ
# Confirm the secret is mounted
ibmcloud ce application get --name "$IBMCLOUD_CODE_ENGINE_APP" | grep mcpgw-db-url
# Expected: secret listed in environment references
# Test the application can reach the database (wait ~30s for restart)
APP_URL=$(ibmcloud ce application get --name "$IBMCLOUD_CODE_ENGINE_APP" --output url)
curl -s "${APP_URL}/health" | jq .
# Expected: health check passes β if it fails, check application logs:
# ibmcloud ce application logs --name "$IBMCLOUD_CODE_ENGINE_APP" --tail 50
Choosing the right PostgreSQL sizeΒΆ
| Workload profile | Suggested plan | Members Γ RAM | Notes |
|---|---|---|---|
| Dev / PoC | standard | 1 Γ 4 GB | Cheapest; no HA; easy to scale later |
| Prod small | standard | 2 Γ 8 GB | Two members enable HA & automatic fail-over |
| Prod heavy | enterprise | 3 Γ 16 GB | Dedicated bare-metal; highest performance & isolation |
Scale up at any time with:
ibmcloud cdb deployment-scaling-set mcpgw-db \
--members 3 --memory-gb 16
# Update the number of maximum connections:
ibmcloud cdb deployment-configuration YOUR_DB_CRN '{"configuration":{"max_connections":215}}'
# show max_connections;
The gateway will reconnect transparently because the host name remains stable. See the documentation for more details.
Local SQLite vs. Managed PostgreSQLΒΆ
| Aspect | Local SQLite (sqlite:////tmp/mcp.db) | Managed PostgreSQL |
|---|---|---|
| Persistence | None - lost on restarts / scale-out | Durable & backed-up |
| Concurrency | Single-writer lock | Multiple writers |
| Scale-out ready | No - state is per-pod | Yes |
| Best for | Unit tests, CI pipelines | Staging & production |
For production workloads you must switch to a managed database or mount a persistent file system.
8 - Adding IBM Cloud Databases for Redis (optional cache layer)ΒΆ
Need a high-performance shared cache? Provision IBM Cloud Databases for Redis and point ContextForge at it.
###############################################################################
# 1 - Provision Redis
###############################################################################
# Choose a plan: standard (shared) or enterprise (dedicated).
ibmcloud resource service-instance-create mcpgw-redis \
databases-for-redis standard $IBMCLOUD_REGION
###############################################################################
# 2 - Create service credentials
###############################################################################
ibmcloud resource service-key-create mcpgw-redis-creds Administrator \
--instance-name mcpgw-redis
###############################################################################
# 3 - Retrieve credentials & craft REDIS_URL
###############################################################################
creds_json=$(ibmcloud resource service-key mcpgw-redis-creds --output json)
host=$(echo "$creds_json" | jq -r '.[0].credentials.connection.rediss.hosts[0].hostname')
port=$(echo "$creds_json" | jq -r '.[0].credentials.connection.rediss.hosts[0].port')
pass=$(echo "$creds_json" | jq -r '.[0].credentials.connection.rediss.authentication.password')
REDIS_URL="rediss://:${pass}@${host}:${port}/0" # rediss = TLS-secured Redis
###############################################################################
# 4 - Store REDIS_URL as a Code Engine secret
###############################################################################
ibmcloud ce secret create --name mcpgw-redis-url \
--from-literal REDIS_URL="$REDIS_URL"
###############################################################################
# 5 - Mount the secret and switch cache backend
###############################################################################
ibmcloud ce application update --name "$IBMCLOUD_CODE_ENGINE_APP" \
--env-from-secret mcpgw-redis-url \
--env CACHE_TYPE=redis
Verify Redis connectivityΒΆ
# Confirm both secrets are mounted
ibmcloud ce application get --name "$IBMCLOUD_CODE_ENGINE_APP" | grep -E "mcpgw-(db|redis)"
# Expected: both mcpgw-db-url and mcpgw-redis-url listed
# Verify the application restarts and is healthy
APP_URL=$(ibmcloud ce application get --name "$IBMCLOUD_CODE_ENGINE_APP" --output url)
curl -s "${APP_URL}/health" | jq .
# Expected: health check passes with Redis cache active
Choosing the right Redis sizeΒΆ
| Use-case | Plan | Memory | Notes |
|---|---|---|---|
| Dev / CI | standard | 256 MB | Minimum footprint, single member |
| Small production | standard | 1 GB | Two-member HA cluster |
| High-throughput | enterprise | β₯4 GB | Dedicated nodes, persistence, AOF |
Scale later with:
Once redeployed, the gateway will use Redis for request-level caching, reducing latency and database load.
9 - Gunicorn configuration (optional tuning)ΒΆ
The container starts gunicorn with the settings defined in gunicorn.config.py found at the project root. If you need to change worker counts, ports, or time-outs, edit this file before you build the image (make podman or make docker). The settings are baked into the container at build time.
# -*- coding: utf-8 -*-
"""
Gunicorn configuration
Docs: https://docs.gunicorn.org/en/stable/settings.html
"""
# Network interface / port ββββββββββββββββββββββββββββββββββββββββββββββ
bind = "0.0.0.0:4444" # Listen on all interfaces, port 4444
# Worker processes ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
workers = 8 # Rule of thumb: 2-4 Γ NUM_CPU_CORES
# Request/worker life-cycle βββββββββββββββββββββββββββββββββββββββββββββ
timeout = 600 # Kill a worker after 600 s of no response
max_requests = 10000 # Restart worker after N requests
max_requests_jitter = 100 # Add randomness to avoid synchronized restarts
# Logging & verbosity βββββββββββββββββββββββββββββββββββββββββββββββββββ
loglevel = "info" # "debug", "info", "warning", "error", "critical"
# Optimisations βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
preload_app = True # Load app code once in parent, fork workers (saves RAM)
reuse_port = True # SO_REUSEPORT for quicker restarts
# Alternative worker models (uncomment ONE and install extras) ----------
# worker_class = "gevent" # pip install "gunicorn[gevent]"
# worker_class = "eventlet" # pip install "gunicorn[eventlet]"
# worker_class = "tornado" # pip install "gunicorn[tornado]"
# threads = 2 # If using the 'sync' worker with threads
# TLS certificates (if you terminate HTTPS inside the container)
# certfile = 'certs/cert.pem'
# keyfile = 'certs/key.pem'
# Server hooks (logging examples) βββββββββββββββββββββββββββββββββββββββ
def when_ready(server):
server.log.info("Server is ready. Spawning workers")
def post_fork(server, worker):
server.log.info("Worker spawned (pid: %s)", worker.pid)
def worker_exit(server, worker):
server.log.info("Worker exit (pid: %s)", worker.pid)
Typical tweaks
| Scenario | Setting(s) to adjust |
|---|---|
| High-latency model calls β time-outs | timeout (e.g. 1200 s) |
| CPU-bound workload on 4-core instance | workers = 8 β workers = 16 |
| Memory-limited instance | Reduce workers or disable preload_app |
| Websocket / async traffic | Switch worker_class to gevent or eventlet |
After changing the file, rebuild and redeploy:
10 - TroubleshootingΒΆ
| Symptom | Cause | Fix |
|---|---|---|
ibmcloud ce application get shows "Failed" | Image pull error β wrong registry secret or image path | Verify IBMCLOUD_IMAGE_NAME matches the pushed image: ibmcloud cr images |
| Application starts then crashes (OOMKilled) | Insufficient memory for gunicorn workers | Increase IBMCLOUD_MEMORY in .env.ce or reduce workers in gunicorn.config.py |
connection refused to PostgreSQL | Database not yet provisioned or wrong hostname | Verify with: ibmcloud resource service-instance mcpgw-db and check credentials JSON |
SSL: CERTIFICATE_VERIFY_FAILED on database connection | Missing sslmode=require in DATABASE_URL | Ensure DATABASE_URL ends with ?sslmode=require |
| Redis connection timeout | Security group or allowlist blocking Code Engine IPs | IBM Cloud Databases allowlist must include Code Engine's outbound IPs, or use private endpoints |
Application starts but /tools returns [] | Database is empty β first deploy needs bootstrap | Access /admin to configure gateways and virtual servers, or use the API |
ibmcloud ce application logs shows no output | Application scaled to zero β no instances running | Send a request to wake it: curl $APP_URL/health then retry logs |
| Slow cold starts (>10s) | Code Engine scales from zero by default | Set --min-scale 1 to keep at least one instance warm |
Checking application logs
Most deployment issues are visible in the application logs. Use these commands to investigate:
# Recent logs (last 50 lines)
ibmcloud ce application logs --name "$IBMCLOUD_CODE_ENGINE_APP" --tail 50
# Follow logs in real-time
ibmcloud ce application logs --name "$IBMCLOUD_CODE_ENGINE_APP" --follow
# System events (scheduling, scaling, image pull)
ibmcloud ce application events --name "$IBMCLOUD_CODE_ENGINE_APP"