Section 04

Kali in Docker — Method 1 (manual CLI)

Step-by-step Docker setup using docker run and docker exec. Slower than Method 2, but every flag is visible and every step is something you can reason about. Read this once even if you plan to use Method 2 — knowing what the compose file is doing makes the whole lab easier to debug.

For sponsors: what this means in one paragraph

Before any audit happens, the engineer has to build a controlled environment to do the work inside. This section walks through that build by hand, one command at a time, on a regular laptop — the audit toolkit ends up isolated inside a "container" (a sealed-off process the laptop runs but does not share its files with). The reason to build it manually first, rather than running an automation script, is accountability: if you ever need to ask the engineer what a particular setting does and why, the answer cannot be "the script handled it" — they will have made that choice deliberately. Method 2 (the next section) automates the same build, but this section is the proof that the engineer understands what the automation is doing.

Cross-platform compatibility

Docker behaves differently on Linux, Windows, and macOS. On Linux, the container can share the host's network stack and NIC directly via --network host. On Windows and macOS, Docker runs inside a lightweight Linux VM, so true host networking is not available — the container uses bridge mode with port mapping instead. The MCP server still works the same way on all three; only the network plumbing differs. Tabs below switch between the platforms.

Step 0 — Install Docker

sudo apt update
sudo apt install -y docker.io docker-compose-plugin
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
newgrp docker
# Install Docker Desktop for Windows from:
#   https://www.docker.com/products/docker-desktop
# During installation, choose the WSL2 backend
# (recommended over Hyper-V).
# Install Docker Desktop for Mac from:
#   https://www.docker.com/products/docker-desktop
# Apple Silicon (M1/M2/M3/M4) is fully supported.

Step 1 — Pull the official Kali Linux image

The image is the same on all three platforms.

docker pull kalilinux/kali-rolling

Step 2 — Run the container

This is where the platforms diverge. Pick your tab. Each command boots /sbin/init as PID 1 so that systemd is available inside the container — the MCP server will run as a systemd service later.

docker run -d \
  --network host \
  --privileged \
  --tmpfs /run \
  --tmpfs /run/lock \
  -v /sys/fs/cgroup:/sys/fs/cgroup:rw \
  --cgroupns=host \
  --name kali-mcp \
  kalilinux/kali-rolling \
  /sbin/init
docker run -d `
  --privileged `
  --tmpfs /run `
  --tmpfs /run/lock `
  -p 5000:5000 `
  --name kali-mcp `
  kalilinux/kali-rolling `
  /sbin/init

PowerShell uses the backtick (`) as its line-continuation character — the equivalent of \ on Linux/macOS. The cgroup volume mount is omitted because Docker Desktop manages cgroups via WSL2 automatically.

docker run -d \
  --privileged \
  --tmpfs /run \
  --tmpfs /run/lock \
  -p 5000:5000 \
  --name kali-mcp \
  kalilinux/kali-rolling \
  /sbin/init

The cgroup volume mount is omitted because Docker Desktop on macOS manages cgroups inside its internal Linux VM.

What the flags do

  • --network host (Linux only) — container shares the host's network stack and NIC directly, no separate IP.
  • --privileged — required for systemd to manage cgroups inside the container.
  • --tmpfs /run, --tmpfs /run/lock — systemd needs these as writable tmpfs mounts.
  • -v /sys/fs/cgroup:/sys/fs/cgroup:rw (Linux only) — gives systemd the host cgroup filesystem.
  • --cgroupns=host (Linux only) — uses the host cgroup namespace; required for systemd.
  • -p 5000:5000 (Windows/macOS) — maps the MCP server port to the host.
  • /sbin/init — boots systemd as PID 1 instead of a shell.

Step 3 — Update apt and install the Kali toolset

Same on all three platforms.

docker exec kali-mcp apt update
docker exec kali-mcp apt install -y kali-linux-default

Substitute kali-linux-headless for a lighter install, or kali-linux-everything for the full suite. The default metapackage is what most readers will want.

Step 4 — Install the official Kali MCP server

docker exec kali-mcp apt install -y mcp-kali-server

Step 5 — Create the systemd service file

Writing the unit file with cat > ... << EOF from inside a docker exec call avoids any line-ending issues on Windows hosts.

docker exec kali-mcp bash -c 'cat > /etc/systemd/system/kali-mcp-server.service << EOF
[Unit]
Description=Kali MCP Server
After=network.target

[Service]
ExecStart=/usr/bin/kali-server-mcp
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
EOF'

Step 6 — Enable and start the service

docker exec kali-mcp systemctl daemon-reload
docker exec kali-mcp systemctl enable kali-mcp-server
docker exec kali-mcp systemctl start  kali-mcp-server

Step 7 — Verify the service is running

docker exec kali-mcp systemctl status kali-mcp-server

You should see active (running). The MCP server now listens on:

PlatformAddress
Linux127.0.0.1:5000
Windowslocalhost:5000
macOSlocalhost:5000

Step 8 — Commit the container as a reusable image

Bake everything you have configured into a new image so subsequent runs do not repeat the install steps.

docker commit kali-mcp kali-mcp-ready

Step 9 — Future launches

docker run -d \
  --network host \
  --privileged \
  --tmpfs /run \
  --tmpfs /run/lock \
  -v /sys/fs/cgroup:/sys/fs/cgroup:rw \
  --cgroupns=host \
  --name kali-mcp \
  kali-mcp-ready \
  /sbin/init
docker run -d `
  --privileged `
  --tmpfs /run `
  --tmpfs /run/lock `
  -p 5000:5000 `
  --name kali-mcp `
  kali-mcp-ready `
  /sbin/init
docker run -d \
  --privileged \
  --tmpfs /run \
  --tmpfs /run/lock \
  -p 5000:5000 \
  --name kali-mcp \
  kali-mcp-ready \
  /sbin/init

The kali-mcp-server service comes up automatically every time because of the systemd WantedBy=multi-user.target on the unit.

Quick reference

TaskCommand
Check container is runningdocker ps
Open a shell in the containerdocker exec -it kali-mcp /bin/bash
Check MCP server statusdocker exec kali-mcp systemctl status kali-mcp-server
Stop the MCP serverdocker exec kali-mcp systemctl stop kali-mcp-server
Restart the MCP serverdocker exec kali-mcp systemctl restart kali-mcp-server
Stop the containerdocker stop kali-mcp
Remove the containerdocker rm kali-mcp
Operational note

--privileged grants the container elevated access to the host. Run this lab on a machine you treat as a single-purpose audit workstation, not on a shared development laptop. The same point applies to Method 2.

Check yourself

Three questions on the manual setup

You can copy and paste the commands and end up with a working container. You shouldn't rely on copy-paste for the bits where being wrong has real-world consequences — these three test the parts that matter.

Q1 / 3

Why is --privileged on the docker run command, and what is the cost of using it?

It runs the container as root inside; there is no cost — every container runs that way.
No Running as root inside a container is the default; --privileged goes much further. It is not free.
It is required so systemd can manage cgroups inside the container; the cost is that the container has meaningfully elevated access to the host, so the lab should live on a single-purpose audit workstation.
Yes systemd needs it to function; the host pays for it with weakened isolation. The mitigation is operational: keep this lab off shared machines.
It enables faster networking inside the container.
No Network performance is unrelated to --privileged. The flag is about kernel capabilities and device access, not throughput.
Q2 / 3

Why does the container boot /sbin/init as PID 1 instead of a shell?

Because shells cannot be PID 1 inside Docker containers.
No Most containers run a shell or an application as PID 1. There is nothing technically stopping it.
So that systemd is available, which is what runs mcp-kali-server as a managed service that auto-starts on every container boot.
Yes No systemd, no service management; no service management, no auto-start. The MCP server's WantedBy=multi-user.target only works if systemd is PID 1.
To prevent the container from being controlled with docker exec.
No docker exec works exactly the same whether PID 1 is a shell or systemd. The choice is about service management, not about access.
Q3 / 3

After Step 7, where is the MCP server reachable from?

From anywhere on your LAN — the port is published to your NIC.
No The server binds to loopback inside the container. On Windows/macOS, Docker's port map exposes it to the host's loopback only, not the LAN. On Linux with --network host, it is still loopback-bound by the server itself.
Only from the host machine, via loopback (127.0.0.1:5000 or localhost:5000).
Yes The MCP server is loopback-bound — the first trust boundary in Section 03. An attacker on your network cannot reach it.
From any device on the public internet, since Docker publishes the port.
No A Docker port map exposes a service to the host's interfaces; it does not put it on the public internet unless the host itself is publicly routable.