Content
# Linux MCP Server
A lightweight, disposable Ubuntu 24.04 container used as a **test target** for
a Linux MCP (Model Context Protocol) server. It boots in seconds, carries a
small and well-known set of command-line tools, and is meant to be created
and destroyed frequently during development and CI -- it is **not** a
production runtime.
## Table of contents
- [Overview](#overview)
- [What's installed](#whats-installed)
- [Prerequisites](#prerequisites)
- [Repository layout](#repository-layout)
- [Building the image](#building-the-image)
- [Starting the container](#starting-the-container)
- [Entering the container](#entering-the-container)
- [Stopping the container](#stopping-the-container)
- [Restarting the container](#restarting-the-container)
- [Mounted volumes](#mounted-volumes)
- [Built-in example MCP server](#built-in-example-mcp-server)
- [Updating installed packages](#updating-installed-packages)
- [Optional Docker CLI](#optional-docker-cli)
- [Troubleshooting](#troubleshooting)
- [Cleaning up Docker resources](#cleaning-up-docker-resources)
- [Validation checklist](#validation-checklist)
- [Design notes](#design-notes)
## Overview
An MCP client/server that automates Linux systems needs somewhere realistic
but safe to point itself at. This image provides that: a minimal Ubuntu
base, a non-root `dev` user, a mounted `/workspace`, and just enough
shell/network/dev tooling to exercise typical MCP tool calls (running
commands, reading/writing files, checking processes, using git, etc.)
without dragging in a full desktop or server stack.
The container's only job is to **stay alive** (`sleep infinity`) so an MCP
client can attach at any time with `docker exec`.
A small example MCP server is baked into the image itself (see
[Built-in example MCP server](#built-in-example-mcp-server)), so there's
something real to connect to the moment the container starts, without any
manual setup.
## What's installed
| Category | Packages |
|---|---|
| Shell & core utils | `bash`, `coreutils`, `findutils`, `grep`, `sed`, `gawk`, `procps`, `psmisc`, `util-linux`, `file`, `which`, `less`, `nano`, `vim-tiny` |
| Networking | `curl`, `wget`, `iproute2`, `iputils-ping`, `dnsutils`, `net-tools`, `openssh-client` |
| Development | `git`, `python3`, `python3-pip`, `python3-venv`, `make`, `build-essential` |
| System utilities | `jq`, `tree`, `lsof`, `zip`, `unzip`, `tar`, `gzip`, `bzip2`, `xz-utils` |
| TLS support | `ca-certificates` (needed for `curl`/`wget`/`git` over HTTPS -- added beyond the base list for that reason) |
Deliberately **not** installed: systemd, snap, desktop packages, web
servers, databases, PHP, Node.js, Java, or the Docker Engine. The Docker
*CLI* is available as an opt-in build argument -- see
[Optional Docker CLI](#optional-docker-cli).
Beyond apt packages, the image also bakes in one Python dependency --
[`mcp[cli]`](https://pypi.org/project/mcp/) -- installed into its own
virtualenv at `/opt/mcp-server/venv` for the bundled example server. See
[Built-in example MCP server](#built-in-example-mcp-server).
## Prerequisites
- Docker Engine 24+ (or Docker Desktop)
- Docker Compose v2 (`docker compose`, bundled with recent Docker installs)
- `make` (optional, only needed if you use the provided `Makefile`)
## Repository layout
```
.
├── Dockerfile # Image definition
├── docker-compose.yml # Compose service definition
├── .dockerignore # Files excluded from the build context
├── .gitignore
├── Makefile # Shortcuts for common commands
├── README.md
├── mcp-server/ # Example MCP server, baked into the image at build time
│ ├── server.py
│ └── requirements.txt
└── workspace/ # Bind-mounted into the container at /workspace
```
## Building the image
```bash
docker compose build
```
or, without Compose:
```bash
docker build -t linux-mcp-test:latest .
```
To pin or change the Ubuntu base version:
```bash
docker compose build --build-arg UBUNTU_VERSION=24.04
```
## Starting the container
```bash
docker compose up -d
```
This starts the container in the background, named `linux-mcp-test`, with a
TTY allocated and stdin kept open, restarting automatically
(`unless-stopped`) if it crashes or the host reboots -- until you explicitly
stop it.
Check that it's running:
```bash
docker ps --filter name=linux-mcp-test
docker logs linux-mcp-test
```
## Entering the container
```bash
docker exec -it linux-mcp-test bash
```
You'll land in `/workspace` as the non-root `dev` user. This is also the
entry point an MCP server would use to run commands against the container.
## Stopping the container
```bash
docker compose down
```
This stops and removes the container (the image and the `./workspace`
contents on your host are left untouched). To stop without removing it:
```bash
docker stop linux-mcp-test
```
## Restarting the container
```bash
docker compose restart
```
or, via the Makefile:
```bash
make restart
```
## Mounted volumes
`./workspace` on the host is bind-mounted to `/workspace` in the container
and owned by the `dev` user (UID/GID 1000). Anything placed in either
location is visible in both -- use it to hand files to the container or to
pull artifacts back out. Because it's a bind mount, its contents persist
across `docker compose down` / `up` cycles; they're only lost if you delete
the `workspace/` folder on the host yourself.
## Built-in example MCP server
The image bakes in a small example MCP server from `./mcp-server/` (see
[mcp-server/README.md](mcp-server/README.md) for what it exposes) so
there's something real to test against immediately -- no manual copy or
`pip install` step required after `docker compose up`.
At build time, `server.py` and `requirements.txt` are copied to
`/opt/mcp-server/` and installed into their own virtualenv there (kept
separate from system Python, and sidesteps Ubuntu 24.04's PEP 668
restriction on global `pip install`). A one-line wrapper at
`/usr/local/bin/mcp-linux-example` runs it, and `WORKSPACE_ROOT` is set to
`/workspace` automatically, so the server's file/command tools operate on
the same bind-mounted directory described above.
**Try it with the MCP Inspector:**
```bash
npx -y @modelcontextprotocol/inspector \
docker exec -i linux-mcp-test mcp-linux-example
```
**Point an MCP client (e.g. Claude Desktop) at it:**
```json
{
"mcpServers": {
"linux-mcp-test": {
"command": "docker",
"args": ["exec", "-i", "linux-mcp-test", "mcp-linux-example"]
}
}
}
```
No `env` block needed in the client config -- `WORKSPACE_ROOT` is already
set inside the container.
**To change the server itself:** edit `mcp-server/server.py` (or its
`requirements.txt`), then rebuild:
```bash
docker compose build
docker compose up -d
```
The running container always reflects whatever was baked in at the last
build, not a live view of the file on your host.
## Updating installed packages
The image intentionally does not run `apt-get upgrade` at build time, to
keep builds deterministic. To pick up new package versions:
```bash
docker compose build --no-cache
docker compose up -d
```
To add or remove packages, edit the package list in the `RUN apt-get
install ...` block in the `Dockerfile`, then rebuild as above.
To patch a *running* container ad hoc (not persisted -- for one-off
debugging only):
```bash
docker exec -it linux-mcp-test bash -lc "sudo -n true 2>/dev/null; apt-get update"
```
Note: the `dev` user has no sudo access by design (see
[Design notes](#design-notes)), so persistent package changes belong in the
`Dockerfile`, not in a live container.
## Optional Docker CLI
The Docker *client* binary (not the Docker Engine/daemon) can be included
for scenarios where the container needs to talk to a host or sibling Docker
daemon (e.g. via a mounted `/var/run/docker.sock`):
```bash
docker compose build --build-arg INSTALL_DOCKER_CLI=true
```
It's off by default to keep the standard image as small as possible.
## Troubleshooting
**Container exits immediately after `docker compose up -d`**
Check the logs: `docker logs linux-mcp-test`. Since the container's command
is just `sleep infinity`, an immediate exit almost always means the image
failed to build correctly -- rebuild with `docker compose build --no-cache`
and watch for errors.
**`docker exec` says the container is not running**
Confirm status with `docker ps -a --filter name=linux-mcp-test`. If it's
`Exited`, start it with `docker compose up -d` (or `docker start
linux-mcp-test`) before exec'ing in.
**Permission denied writing to `/workspace`**
The container's `dev` user is UID/GID 1000. If your host user has a
different UID, files created on the host may not be writable from inside
the container or vice versa. Either run your host commands as UID 1000, or
adjust ownership with `sudo chown -R 1000:1000 ./workspace` on the host.
**Build fails with `groupadd: GID '1000' already exists`**
This means you're on an older cached layer or a modified Dockerfile that
skips the cleanup step. Current `ubuntu:24.04` base images ship with a
pre-created `ubuntu` user/group already at UID/GID 1000 (introduced in
24.04; 22.04 and earlier don't have it), which collides with this image's
own `dev` user. The Dockerfile removes that shipped account first -- if
you still hit this, run `docker compose build --no-cache` to rule out a
stale cache, and confirm the `userdel`/`groupdel` guard step wasn't
accidentally removed.
**`ModuleNotFoundError: No module named 'mcp'`**
You're running the server with system Python instead of its dedicated
venv. Use the wrapper (`mcp-linux-example`) or the venv interpreter
directly (`/opt/mcp-server/venv/bin/python3 /opt/mcp-server/server.py`) --
plain `python3 /opt/mcp-server/server.py` won't see the venv's packages.
**`apt-get` fails during build with DNS/network errors**
This is a host networking issue, not an image issue -- confirm the Docker
daemon has outbound internet access (`docker run --rm ubuntu:24.04
getent hosts archive.ubuntu.com`).
**Need to pass a proxy for `apt-get` during build**
Use standard Docker build args, e.g. `docker compose build --build-arg
http_proxy=... --build-arg https_proxy=...` (you'll need to add matching
`ARG`/`ENV` lines to the `Dockerfile` if you rely on this regularly).
## Cleaning up Docker resources
Remove just this project's container and image:
```bash
docker compose down --rmi local
```
Remove container, local image, and any anonymous volumes/orphans:
```bash
make clean
# equivalent to:
docker compose down -v --rmi local --remove-orphans
```
Broader Docker housekeeping (affects *all* stopped containers, dangling
images, and unused networks on the host -- use with care):
```bash
docker system prune
```
## Validation checklist
After building, the following should all succeed:
```bash
docker compose build # image builds
docker compose up -d # container starts
docker ps --filter name=linux-mcp-test # container stays running
docker exec -it linux-mcp-test whoami # -> dev
docker exec -it linux-mcp-test bash -lc 'touch /workspace/ok && ls /workspace'
docker exec -it linux-mcp-test python3 --version
docker exec -it linux-mcp-test git --version
docker exec -it linux-mcp-test curl -sI https://example.com
docker exec -it linux-mcp-test ping -c1 1.1.1.1
docker exec -it linux-mcp-test which mcp-linux-example
docker exec -it linux-mcp-test /opt/mcp-server/venv/bin/python3 -c "import mcp; print('mcp SDK OK')"
docker images linux-mcp-test --format '{{.Size}}' # sanity-check image size
```
For an actual protocol-level check (not just "the process can start"), use
the MCP Inspector command in
[Built-in example MCP server](#built-in-example-mcp-server) -- it performs
a real MCP handshake and lets you call each tool interactively.
## Design notes
- **Single apt layer.** All package installation happens in one `RUN`, with
`rm -rf /var/lib/apt/lists/*` and `apt-get clean` in the same layer, so
cache and metadata never persist in the final image.
- **`--no-install-recommends`** everywhere, to avoid pulling in optional
dependencies (docs, extra fonts, etc.) that add size without adding
testing value.
- **No passwordless sudo.** The task only calls for a working non-root
user; adding sudo (passwordless or otherwise) would only widen the
container's effective privileges without a corresponding need. If your
MCP tests specifically require privilege-escalation scenarios, add
`sudo` and a scoped `/etc/sudoers.d/dev` entry deliberately, rather than
granting blanket root access.
("Disposable" here means the *container* is cheap to recreate, not that
its user should run as `root`.)
- **Fixed UID/GID 1000** for `dev`, so bind-mounted files line up with a
typical single-user host account without extra `chown` steps. This suits
single-developer setups; for shared/team use, consider parameterizing the
UID/GID via build args instead.
- **`sleep infinity` as PID 1, exec form.** No shell wrapper, so the
process responds directly and cleanly to `docker stop`.
- **The example MCP server lives in its own venv under `/opt`, not
system Python.** This avoids fighting Ubuntu 24.04's PEP 668 guard on
`pip install` into the system interpreter, and keeps its one dependency
(`mcp[cli]`) from mixing with whatever else ends up installed via
`python3`/`pip` at the top level over the container's lifetime. The
`/usr/local/bin/mcp-linux-example` wrapper hides that detail from
whoever's configuring an MCP client.
- **Deterministic builds.** The Ubuntu version is pinned via a build arg
(default `24.04`) rather than tracking `latest`, and no
`apt-get upgrade` runs at build time -- rebuilding today and next month
from the same commit installs the same package versions from the same
base image tag.
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available