dockerMCP-ChatGPT
Docker MCP ChatGPT Sandbox is a self-hosted remote MCP server for ChatGPT that runs shell commands and edits code inside a hardened Ubuntu container, with OAuth via Keycloak and optional Cloudflare Tunnel.
README
Docker MCP ChatGPT Coding Runner
A self-hosted remote MCP server for ChatGPT that can run shell commands, edit code, install Ubuntu packages, use Git, and authenticate with GitHub CLI. OAuth is bundled with Keycloak. Cloudflare can expose the stack through either a zero-account Quick Tunnel for testing or a stable named tunnel for regular use.
What this reuses
This project avoids rebuilding mature infrastructure:
- Model Context Protocol Python SDK provides FastMCP, Streamable HTTP, OAuth protected-resource metadata, bearer-token middleware, and tool annotations.
- Keycloak provides OAuth/OIDC, login UI, authorization-code flow, PKCE, refresh tokens, and client management.
- Caddy provides reverse proxying so MCP and OAuth use one public hostname.
- Cloudflare Tunnel provides optional outbound-only public routes, including Quick Tunnels and named tunnels.
- Ubuntu 24.04 supplies Bash, Python, Node.js, Git, GitHub CLI (
gh), compilers, ripgrep, fd, jq, and curl.
There was no existing application code in this repository to preserve; the original repository contained only an empty README file.
Architecture
ChatGPT
|
| HTTPS / OAuth
v
Cloudflare Quick Tunnel or named tunnel
|
v
Caddy gateway :8080
|-- /mcp and RFC 9728 metadata --> non-root FastMCP service
| |
| | Unix domain socket only
| v
| Ubuntu command runner
| - root user
| - writable container filesystem
| - outbound internet access
| - Git and GitHub CLI
| - persistent /workspace and /root
|
`-- /realms/mcp/* --> Keycloak --> PostgreSQL
The public MCP service and privileged command runner are separate containers. The MCP service remains non-root, read-only, capability-dropped, and connected to the runner only through a Unix socket. Commands requested through MCP execute as root in the runner container.
Exposed MCP tools
sandbox_info: report root, network, filesystem, GitHub CLI, and execution-limit state.run_command: execute a root Bash command starting under/workspace.read_file: read a bounded text file from/workspace.write_file: create or explicitly overwrite a text file under/workspace.list_files: list a bounded directory tree without following symlinks.
The dedicated file tools remain confined to /workspace. The run_command tool is intentionally not confined to workspace file access because root package installation requires access to the container filesystem.
Included development tools
The image includes:
bash, build-essential, curl, fd, git, gh, jq,
nodejs, npm, python3, pip, venv, ripgrep
Check them from the runner:
docker compose exec runner git --version
docker compose exec runner gh --version
Requirements
- Ubuntu host with Docker Engine and Docker Compose v2.
- A ChatGPT plan or workspace that supports custom remote MCP apps.
- For a stable deployment: a public HTTPS hostname managed by Cloudflare or another reverse proxy.
- For temporary testing: no Cloudflare account or domain is required when using the Quick Tunnel helper.
Initial setup
Copy the environment template:
cp .env.example .env
Generate independent secrets and put them in .env:
openssl rand -hex 32
At minimum, replace POSTGRES_PASSWORD, KEYCLOAK_ADMIN_PASSWORD, OAUTH_CLIENT_SECRET, and MCP_USER_PASSWORD.
For a stable deployment, set PUBLIC_BASE_URL to the final HTTPS origin:
PUBLIC_BASE_URL=https://mcp.example.com
Do not include /mcp in PUBLIC_BASE_URL.
For Quick Tunnel testing, the initial value can remain a placeholder. scripts/quick-tunnel.sh replaces it with the generated trycloudflare.com origin and recreates the URL-sensitive services.
Obtain the ChatGPT callback URL
While creating the custom app in ChatGPT, select OAuth and copy the exact callback URL shown by ChatGPT. It looks similar to:
https://chatgpt.com/connector/oauth/<callback_id>
Set the full value as CHATGPT_CALLBACK_URL. Do not replace the callback ID, add a trailing slash, or use a wildcard.
The OAuth values entered in ChatGPT must match .env:
MCP endpoint: https://PUBLIC_HOST/mcp
Client ID: value of OAUTH_CLIENT_ID
Client secret: value of OAUTH_CLIENT_SECRET
Scopes: openid profile email offline_access mcp:tools
The authorization-server issuer is:
https://PUBLIC_HOST/realms/mcp
The initial realm user is configured by MCP_USER and MCP_USER_PASSWORD. Keycloak forces a password change on first login.
Run locally without Cloudflare Tunnel
docker compose up -d --build
Endpoints:
MCP: http://localhost:8080/mcp
Health: http://localhost:8080/health
Keycloak admin: http://localhost:8081/admin/
OAuth issuer URLs must match the token issuer exactly. For a ChatGPT connection, use the public HTTPS origin rather than the local URL.
Quick Tunnel: no Cloudflare account or token
Cloudflare Quick Tunnels create a random https://*.trycloudflare.com URL without a Cloudflare account, domain, DNS record, or tunnel token. They are intended only for development and testing.
After setting the required secrets and ChatGPT callback URL in .env, run:
bash scripts/quick-tunnel.sh
The helper performs the complete bootstrap sequence:
- builds and starts the local application;
- recreates the
cloudflared-quickcontainer to obtain a fresh hostname; - reads the generated URL from the Cloudflare logs;
- writes that origin to
PUBLIC_BASE_URLin.env; - recreates Keycloak, MCP, and Caddy so OAuth metadata and hostname validation use the new URL;
- prints the MCP endpoint and OAuth issuer.
Example output:
Public origin: https://random-words.trycloudflare.com
MCP endpoint: https://random-words.trycloudflare.com/mcp
OAuth issuer: https://random-words.trycloudflare.com/realms/mcp
Enter the printed MCP endpoint in ChatGPT.
The Quick Tunnel can also be started directly, but direct startup does not automatically update PUBLIC_BASE_URL:
docker compose --profile quick-tunnel up -d --build
docker compose logs -f cloudflared-quick
Important limitations:
- A new Quick Tunnel process receives a new random hostname.
- Rerunning the helper requires updating the MCP endpoint in ChatGPT.
- Quick Tunnels are testing infrastructure, not a production service.
- Cloudflare documents a 200-concurrent-request limit and no Server-Sent Events support for Quick Tunnels.
- This MCP server uses stateless JSON responses rather than SSE, but the temporary hostname and testing-only service limits still apply.
- Do not enable the
tunnelandquick-tunnelprofiles at the same time.
Use a named tunnel when the ChatGPT connector must survive container restarts without reconfiguration.
Stable named Cloudflare Tunnel
Create a named tunnel in Cloudflare Zero Trust and configure one public hostname:
Hostname: mcp.example.com
Service: http://gateway:8080
Copy the tunnel token into CLOUDFLARE_TUNNEL_TOKEN, then start the profile:
docker compose --profile tunnel up -d --build
Without --profile tunnel, the named cloudflared container does not start.
The tunnel does not replace OAuth. It only publishes the gateway through an outbound connection. Do not put another interactive login in front of this hostname unless ChatGPT can satisfy it; Keycloak already protects MCP.
Install software at runtime
The runner executes as root, has outbound network access, and has a writable root filesystem. Install packages from the host:
docker compose exec runner bash -lc \
'apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y shellcheck'
The same command can be invoked through the MCP run_command tool:
apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y shellcheck
Runtime-installed packages remain while the current runner container exists, but disappear when Docker recreates that container. For permanent dependencies, add them to the Dockerfile and rebuild:
docker compose up -d --build --force-recreate runner mcp
GitHub login
The official GitHub CLI is installed as gh. Authenticate interactively from the host terminal rather than sending credentials through ChatGPT:
docker compose exec runner gh auth login \
--hostname github.com \
--git-protocol https \
--web
Then configure Git to use GitHub CLI as its credential helper:
docker compose exec runner gh auth setup-git
docker compose exec runner gh auth status
For a headless token login, keep the token out of shell history:
printf '%s' "$GH_TOKEN" | docker compose exec -T runner gh auth login --with-token
docker compose exec runner gh auth setup-git
GitHub CLI configuration, Git configuration, and SSH material under /root persist in the runner_home Docker volume. The workspace persists separately in the workspace volume.
After authentication, commands can use both tools:
docker compose exec runner git clone https://github.com/OWNER/REPOSITORY.git /workspace/REPOSITORY
docker compose exec runner gh repo view OWNER/REPOSITORY
Workspace management
Import a project:
docker compose cp ./my-project/. runner:/workspace/
Export it:
docker compose cp runner:/workspace/. ./workspace-export
Open a root shell:
docker compose exec runner bash
OAuth and realm changes
Keycloak imports the realm only when the mcp realm does not already exist. Changing CHATGPT_CALLBACK_URL, the OAuth client secret, or the bootstrap user in .env does not rewrite an existing realm.
Changing PUBLIC_BASE_URL does take effect after recreating Keycloak and MCP. The Quick Tunnel helper performs that recreation automatically.
For other existing-realm changes, update the client in the local Keycloak admin console. During disposable development, stop the stack and remove only the PostgreSQL data volume to re-import the realm. Do not remove workspace or runner_home unless you intend to delete source files or GitHub credentials.
Remaining isolation
- The public MCP service is non-root and read-only.
- MCP and runner are separate processes in separate containers.
- The runner has no Docker socket.
- The runner is not attached to the internal Keycloak/PostgreSQL network.
- The runner reaches MCP only through a shared Unix socket.
- CPU, memory, PID, file-descriptor, output-size, file-size, and timeout limits remain enabled.
- Workspace path traversal and symlink escape checks remain enabled for dedicated file tools.
- OAuth access-token signature, issuer, audience, expiry, and scope validation remain enabled.
- Keycloak admin endpoints are blocked from the public gateway and bound separately to loopback.
- Both Cloudflare tunnel containers remain disabled unless their profile or helper is selected.
Critical security warning
This configuration gives an OAuth-authenticated LLM a remote root shell with outbound internet access. That is not a strong sandbox. A malicious instruction, prompt injection, compromised dependency, or stolen OAuth session can:
- modify the runner operating system;
- read persisted GitHub credentials;
- push code or change repositories using your GitHub identity;
- download and execute arbitrary software;
- exfiltrate workspace data;
- attack services reachable through the host or internet.
Use a dedicated low-privilege GitHub account or narrowly scoped token. Do not mount the Docker socket, host filesystem, personal SSH agent, cloud credentials, or production secrets. Keep ChatGPT action confirmations enabled.
Docker containers share the host kernel. For hostile or multi-user workloads, use gVisor, Kata Containers, or a Firecracker-style microVM and isolate the runner on a separate machine or disposable VM.
Useful commands
# Start a fresh Quick Tunnel and update OAuth URLs
bash scripts/quick-tunnel.sh
# Show status
docker compose ps
# Follow application logs
docker compose logs -f mcp runner keycloak gateway
# Follow Quick Tunnel logs
docker compose --profile quick-tunnel logs -f cloudflared-quick
# Check privilege and tools
docker compose exec runner bash -lc 'id && git --version && gh --version'
# Stop services without deleting data
docker compose down
# Stop all services including profile containers
docker compose --profile tunnel --profile quick-tunnel down
# Rebuild after source or Dockerfile changes
docker compose up -d --build
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.