docker-mcp-server
MCP serverCloud & infraManage Docker (containers, images, Compose, Swarm, registries) via the Docker SDK and CLI.
Unavailable. This server has no hosted endpoint yet, so ahel can't serve it.
Connect ahel once, and every AI you use reads what you have installed.
From the project's README
As published by l337-org/docker-mcp in README.md.
More than just a fully featured MCP server that lets AI agents manage Docker - containers, images, networks, volumes, swarm services, secrets, configs, nodes, plugins, etc., it helps you create workflows to easily manage your Docker environments.
It gives you much more control and flexibility than calling the Docker CLI directly: each operation is exposed as its own typed tool, marked read-only or not, with destructive actions separately flagged. This means a client can auto-approve reads while always confirming anything destructive, and the whole server can also be switched into a read-only or no-destructive mode as a blanket safeguard. Output is bounded rather than left to grow unboundedly - capped with a truncated flag instead of silently overflowing the agent's context.
For simple cases, you can just install and go with no configuration required - once loaded it will discover your local Docker socket and expose the full command surface to your AI agent. For more advanced users it can manage multiple Docker daemons, e.g. both your local dev environment and also a remote production environment over TCP, TLS or SSH in a single session. It can also be configured to mark some daemons as read-only, so you can monitor them without the risk of making accidental changes.
It can even be run on a machine without Docker installed and manage remote daemons over SSH, TLS or TCP (some features require SSH). The AI itself does not require shell or SSH access.
The MCP server also exposes things like logs and stats as resources so that you can monitor and triage, enabling you to answer questions like 'why did my container crash?', 'what is the state of my swarm?', 'am I suffering memory pressure?', 'what is the disk usage of my volumes?', 'what differences are there between my test and production systems?', and more...
Documentation is built for the agent, not just the person configuring it: an MCP resource exposes the Docker SDK reference in-session (with a tool-callable fallback for clients that can't read resources), and a live tool-catalog resource reports exactly what's registered under the current configuration. Each tool's own description names its nearest siblings and when to prefer each, states preconditions and side effects in plain language, and is honest about when it can still fail - so an agent can pick the right tool on the first try among 150+ options, not guess.
docker-mcp-server is optimized to work efficiently with the new generation of MCP clients that support lazy tool loading. For clients that still eagerly load all tools, the server can optionally be configured to exclude tools from a subset of domains (e.g. exclude 'swarm' and 'scout' tools) to reduce the tool list size. It's also possible to put the MCP server into 'read-only' or 'no-destructive' modes that prevent any tools with write or destructive capabilities from being registered, which again reduces the footprint.
The server runs entirely on your machine, either natively, as an mcpb bundle, or containerized, and sends no telemetry. You are entirely in control - see the Privacy Policy.
Requirements
Note: If you're using the containerized MCP server or MCPB bundle, the Python and uv requirements are taken care of for you.
- A running Docker daemon reachable from the host that runs the server (the standard
DOCKER_HOST/ unix socket conventions apply) - Python ≥ 3.14
- uv for dependency management
- Intel (x86_64) macOS only: installing natively (via
uvx/pip, or the.mcpbbundle, both of which resolve dependencies locally) requires Rust and OpenSSL 3.x, becausecryptography- a transitive dependency, viamcp->pyjwt[crypto]- has shipped no x86_64 macOS wheel since version 49.0.0 and must be built from source there. If you'd rather not install a build toolchain, use the container image instead - it runs the same prebuilt Linux binary regardless of your Mac's CPU architecture, so this doesn't apply to it. See Security considerations for more.
Using the server
The server is published to PyPI as docker-mcp-server. Add an entry to your AI tool's MCP configuration (commonly mcp.json or the equivalent in your client) pointing uvx at it - uv will fetch and cache the package on first use:
{
"mcpServers": {
"docker-mcp-server": {
"command": "uvx",
"args": ["docker-mcp-server"],
"env": {}
}
}
}
To pin a specific version, append ==<version> to the package name (e.g. docker-mcp-server==1.5.0). If you'd rather install it onto your PATH, pipx install docker-mcp-server gives you the docker-mcp-server console script (a docker-mcp alias is also installed).
Installing from git instead. To run an unreleased revision straight from this repository:
{
"mcpServers": {
"docker-mcp-server": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/L337-org/docker-mcp.git",
"docker-mcp-server"
],
"env": {}
}
}
}
To pin a specific revision, append @<tag-or-commit> to the git URL.
Install as a Desktop Extension (.mcpb)
For Claude Desktop, a one-click bundle is attached to each
GitHub Release as
docker-mcp-server-<version>.mcpb (with a matching .sha256). Download it and drag it into
Settings > Extensions, or use Settings > Extensions > Advanced settings > Install extension...
and pick the file. The install dialog surfaces a Docker host(s) field and the read-only /
no-destructive / disabled-domain switches, so no manual JSON editing is needed.
It's a uv-type bundle: Claude Desktop's managed
uv resolves the dependencies and runs the server, so the only host prerequisite is Docker itself -
no separate Python, uv, or git. Leave the Docker host(s) field blank to use your default
Docker context; set one endpoint (ssh://user@host) for a remote daemon, or list several (see
Managing several daemons).
Run as a container
Running the server as a container removes the Python / uv / git prerequisites entirely - the only
thing the host needs is Docker, which you already have. Prebuilt multi-arch images (linux/amd64 +
linux/arm64) are published on each release to Docker Hub (gavinlucas/docker-mcp-server) and
GHCR (ghcr.io/l337-org/docker-mcp-server) - the two are identical. Point your MCP client at
docker run:
{
"mcpServers": {
"docker-mcp-server": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"-v", "/var/run/docker.sock:/var/run/docker.sock",
"gavinlucas/docker-mcp-server:latest"
],
"env": {}
}
}
}
-i is required (the server speaks MCP over stdio); --rm cleans up when the client disconnects. To
pin a version, replace :latest with a release tag (e.g. :1.5.1). To pull from GHCR instead, use
ghcr.io/l337-org/docker-mcp-server:latest.
Image renamed. As of 1.5.0 the image is published as
docker-mcp-server(matching the PyPI name). The oldghcr.io/gavinlucas/docker-mcpimage is frozen at 1.4.0 and no longer updated - point new pulls atghcr.io/l337-org/docker-mcp-server.
Image variants. Two variants are published to both registries (gavinlucas/docker-mcp-server on
Docker Hub and ghcr.io/l337-org/docker-mcp-server on GHCR), both built from one Dockerfile. The
CLI-backed domains (Compose, Stack, Buildx, Scout, Context) shell out to the docker CLI and its
plugins.
| Variant | Tags | Approx. size | Includes |
|---|---|---|---|
full (default) | :latest, :<version> | ~510 MB | docker CLI + compose + buildx + scout |
no-scout | :no-scout, :<version>-no-scout | ~315 MB | docker CLI + compose + buildx |
Scout's plugin binary alone accounts for the ~195 MB jump from no-scout to full. The no-scout
image also defaults DOCKER_MCP_SERVER_DISABLE=scout, so the scout tools don't register - the agent is
never offered tools whose CLI plugin isn't present (it sees a smaller, fully-working tool list rather
than scout tools that error on every call). Override at runtime with -e DOCKER_MCP_SERVER_DISABLE=... if you
ever need to change the disabled set (note it replaces, not appends).
Building it yourself. All variants build from the repo's Dockerfile via build args:
docker build -t docker-mcp-server:full . # full (default)
docker build --build-arg INSTALL_SCOUT=0 --build-arg DISABLE_DOMAINS=scout \
-t docker-mcp-server:no-scout . # no-scout
docker build --build-arg INSTALL_CLI=0 -t docker-mcp-server:lite . # lite (SDK-only, ~165 MB)
The lite image (docker-py SDK tools only - Compose/Buildx/Scout/Context degrade to "plugin
unavailable") is buildable but not published.
Reaching the daemon from inside the container. The image defaults DOCKER_HOST to
unix:///var/run/docker.sock, so mounting your host's socket onto that path is all that's needed.
Where the host socket is, however, varies - and the server prints a platform-aware hint to stderr
if it can't connect at startup:
- Linux:
-v /var/run/docker.sock:/var/run/docker.sock(rootless:-v $XDG_RUNTIME_DIR/docker.sock:/var/run/docker.sock). - macOS (Docker Desktop): the real socket is usually
~/.docker/run/docker.sock- mount it onto the in-container path:-v $HOME/.docker/run/docker.sock:/var/run/docker.sock(or enable Settings > Advanced > Allow the default Docker socket and use/var/run/docker.sock). - Windows (Docker Desktop / WSL2): the engine uses a named pipe, not a Unix socket - prefer
-e DOCKER_HOST=tcp://host.docker.internal:2375(enable the TCP endpoint in Docker Desktop). That endpoint is unauthenticated and unencrypted - keep it bound to localhost, disable it when you're not using it, and use TLS orDOCKER_HOST=ssh://...for any remote daemon. - Remote / TLS / SSH daemon: skip the socket mount and pass
-e DOCKER_HOST=...(plus the TLS vars below) - see Talking to a remote daemon.
Host filesystem access. Inside a container, the file-path tools (image_save / container_export
with dest_path, image_load / container_archive_put with from_file, container_archive_get_to_file,
and compose project_dir / files) resolve paths inside the container,
not on your host. Bind-mount any directory you want to exchange files through - using the same
path inside and out keeps host and container paths identical:
-v $HOME/docker-work:$HOME/docker-work
If you call one of these tools with a path that isn't on a bind mount, the server refuses up front
with a message telling you exactly which -v to add - a write to an unmapped path would otherwise be
silently discarded when the container exits. (The in-band byte tools, capped at 32 MiB, need no
mount.) Configuration env vars (DOCKER_MCP_SERVER_READONLY, DOCKER_HOST, etc.) go in the client's env
block exactly as for the uvx install.
Talking to a remote daemon
When DOCKER_HOST is set the server uses it directly (via docker.from_env(), so DOCKER_TLS_VERIFY / DOCKER_CERT_PATH are honoured too). Common overrides via env:
"env": {
"DOCKER_HOST": "tcp://remote-host:2375",
"DOCKER_TLS_VERIFY": "1",
"DOCKER_CERT_PATH": "/path/to/certs"
}
Default daemon (no DOCKER_HOST). With DOCKER_HOST unset, the server resolves the daemon the way the docker CLI does, rather than assuming /var/run/docker.sock: it follows the active Docker context (DOCKER_CONTEXT, else currentContext from ~/.docker/config.json, reading the endpoint from that context's meta.json), and if that yields nothing it probes the well-known socket locations (~/.docker/run/docker.sock for Docker Desktop 4.13+, $XDG_RUNTIME_DIR/docker.sock for rootless, then /var/run/docker.sock). This matters because docker.from_env() alone ignores contexts and would fall back to /var/run/docker.sock - which Docker Desktop 4.13+ no longer creates by default (it uses the desktop-linux context unless you enable Settings > Advanced > Allow the default Docker socket), so a stock Desktop install reachable by your CLI would otherwise fail here. Precedence: a non-empty DOCKER_HOST always wins and goes straight through docker.from_env() (which ignores contexts); DOCKER_CONTEXT / currentContext is consulted only when DOCKER_HOST is unset, and the socket probe only when neither resolves. TLS material attached to a remote context is not applied automatically - a tcp:// + TLS context still needs DOCKER_HOST / DOCKER_CERT_PATH.
Over SSH
DOCKER_HOST=ssh://user@remote-host is supported via a pure-Python transport (paramiko, pulled in by the docker[ssh] dependency) - there is no system ssh binary requirement, so it works the same on the host install and inside the container images. It authenticates with your normal SSH setup:
- Keys / agent. Use key-based auth; load the key into your agent (
ssh-add) and make sureSSH_AUTH_SOCKis set in the server's environment (or place the key at the default~/.ssh/id_*path). - Known hosts. paramiko verifies the host key against
~/.ssh/known_hostsand rejects an unknown host. Add the host key only after verifying its fingerprint through a trusted channel - connect once interactively withssh user@remote-hostand confirm the prompt, or comparessh-keyscan remote-host | ssh-keygen -lf -against a known-good fingerprint before appending it. Avoid blindly pipingssh-keyscanstraight intoknown_hosts, which trusts whatever key is returned (including a MITM's). - In a container. Mount your SSH material read-only -
-v $HOME/.ssh:/root/.ssh:ro(key +known_hosts) - or forward your agent socket; no socket mount and nosshpackage needed.
CLI-backed tools (Compose, Stack, Buildx, Scout, Context) shell out to the docker CLI, which would otherwise use the system ssh binary over an ssh:// endpoint. Instead, run_docker() detects DOCKER_HOST=ssh://... and transparently starts a per-call local TCP proxy (docker_mcp/tools/_ssh_proxy.py) that opens the same paramiko connection docker-py would, runs docker system dial-stdio over it, and points the CLI subprocess at tcp://127.0.0.1:<ephemeral port> for the duration of that one call. So the CLI-backed tools authenticate with the exact same credentials and host-key policy as the docker-py-backed tools above - no system ssh binary for the direct connection, identical on the host install and inside the container images. The one exception is a ProxyCommand in ~/.ssh/config (bastion/jump-host setups): paramiko runs that command as-given, and it's commonly ssh -W %h:%p ..., so a jump-host hop still shells out to the system ssh client even though the direct connection does not.
That ephemeral 127.0.0.1 listener bridges to the remote (root-equivalent) daemon with your SSH credentials for the duration of a single CLI call, so any process sharing the same loopback could reach it during that brief window. The exposure is narrow - localhost-only and torn down when the call returns - and inside a container it's narrower still, reachable only by processes within that container's network namespace. The daemon remains the trust boundary either way (see Security considerations).
No local Docker
If Docker is not installed locally - or the plugin a call needs is missing - most tools will still operate on a remote host exactly as before. A few tool families (Compose, Stack, Buildx, Scout) will be run on the target host itself, over SSH. For these commands TCP and TLS connections will not work without a local Docker install.
Worth knowing:
- It's a fallback, never a preference. A local CLI that can serve the call - binary plus the plugin that call needs - is always used instead, so nothing changes for a normal install.
- Local files a command reads are copied onto the remote first - a Compose project directory, a bake file, a build context (honouring
.dockerignore) - into a private0700temp directory that is removed when the call returns - best-effort, since a dropped SSH connection leaves nothing able to run the cleanup. A survivor is nameddocker-mcp-server.stage.*and the failure is logged. - The remote host's credentials apply. Registry logins come from its
~/.docker/config.json, so a private-registrycompose_pull, astack deploy --with-registry-auth, or most Scout operations may needdocker loginthere.system_logintalks to the daemon and does not write the remote CLI's config. - The whole working directory is copied, because nothing can tell which files a Compose file references (
build:,env_file:,include:all name arbitrary paths). Point tools at a project directory rather than a large parent, or the call is refused with a size-limit error (200 MiB / 50 000 entries). compose_cprelays whichever side of the copy is local, over the same SSH connection: a host source is staged like any other input, and a container result is fetched back to the real destination once the copy succeeds - the actual copy still runs through the realdocker compose cpon the far host, so every parameter behaves as it does locally. The one difference: a container-to-host copy is refused if the local destination already exists, since only this host knows that.- A few things are refused rather than half-done: for
buildx_builda filesystemdest=inoutput/cache_to, a localsrc=incache_from, or anyssh=- each would resolve on the remote machine. - The remote must present a POSIX shell. Any Linux/macOS/BSD host qualifies, and sshd running inside a WSL distro is supported (it is a Linux target - and with Docker Desktop's WSL2 backend the daemon lives there anyway). A Windows-side
cmd/PowerShell sshd is refused with an explanatory error, as is aunamefrom MSYS/MinGW/Cygwin; a Windows dialect is architected but not implemented. A Windows sshd whose shell iswsl.exeruns commands in WSL while its SFTP subsystem stays on the Windows side, so file-copying tools are refused there - the ones that only run a command still work. context_*tools never fall back. They manage this host's CLI context registry, which a remote host knows nothing about.
Managing several daemons
Everything above targets one daemon. To manage several in a single session - e.g. local dev plus a remote production daemon - set DOCKER_MCP_SERVER_HOSTS to a comma-separated list of name=endpoint pairs:
"env": {
"DOCKER_MCP_SERVER_HOSTS": "local=auto, prod=ssh://ops@prod.example.com(ro)"
}
endpointisauto(your default context/socket, as above),local(the platform-local socket, ignoring contexts), or aunix:///tcp:///ssh:///npipe://URL.ssh://is the recommended remote transport (per-host auth via your SSH keys, no TLS cert plumbing). Atcp://daemon over TLS takes a(tls=<dir>)marker pointing at a cert directory, e.g.prod=tcp://prod:2376(tls=/etc/docker/prod). That directory must holdca.pem(the daemon is always verified against it - so a self-signed daemon works, you just pin its cert here); addcert.pemandkey.pemonly if the daemon requires a client certificate (mutual TLS). There is no unverified-TLS mode - a TLS connection always authenticates the daemon, so encryption never comes without verification.(ro)after an endpoint marks that host read-only: mutating and destructive tools refuse to act on it. This is a per-host guard enforced at call time, independent of the server-wideDOCKER_MCP_SERVER_READONLYswitch - mark production(ro)and the agent can inspect it all day but can't change it, while local stays read-write.(nd)marks a host non-destructive: only destructive tools (removals, prunes, kills) refuse to act on it - reads and ordinary mutations (start/stop/create) still work. This mirrors the server-wideDOCKER_MCP_SERVER_NO_DESTRUCTIVEswitch, per host.(ro)already implies it, so(ro)(nd)combines without error but is redundant.- Single daemon, simpler form. A bare value with no
name=is shorthand for one host -DOCKER_MCP_SERVER_HOSTS=ssh://ops@prod(orauto, or blank). So this one field also covers the single-remote case.DOCKER_HOSTkeeps working whenDOCKER_MCP_SERVER_HOSTSis unset, butDOCKER_MCP_SERVER_HOSTStakes over when set (DOCKER_HOSTis then ignored, with a one-time notice to stderr).
How the agent drives it. With two or more hosts, every daemon-targeting tool gains an optional host argument constrained to your configured names: read-only tools default to the first host when you omit it, while mutating and destructive tools require an explicit host (so the agent can't change the wrong daemon by accident). host_list (and the docker-mcp://hosts resource) report the configured hosts and which is the default; the container/service observability resources become host-aware - the default host's log tail is docker-logs:///{id} (note the empty authority) and a named host's is docker-logs://{host}/{id}, with the same pattern for docker-stats://, service-logs:// and service-tasks://; the single-host bare forms (docker-logs://{id}, ...) are not registered once several hosts are configured. The survey_hosts prompt sweeps every host read-only. The auto/local endpoints are resolved to concrete URLs and pinned at startup, so the SDK and CLI always agree on which daemon a name means - restart to re-resolve after changing a Docker context.
What the agent can do
Once loaded, the agent gets MCP tools grouped by Docker domain. A few examples:
Shortened here. Read the whole README on GitHub.
Signals
- GitHub stars
- 7
- Last commit
- Sep 2026
ahel review
S4info
community integration — published by l337-org, not docker
Automated review, not a security audit. Ruleset v1.
Advanced
- Delivery
- docker-mcp-server MCP server → your ahel gateway (mcp.ahel.ai) → every connected AI client.
- Catalog kind
- mcp-server
- Gateway key
io-github-l337-org-docker-mcp-server- Source
- github.com/l337-org/docker-mcp