Kali Linux on Docker · Part 8 of 16
Build a Kali Linux Security Lab With Docker Compose
Series curriculum (16 lessons)
So far you have run individual containers by hand. That works for a single throwaway shell, but a realistic troubleshooting or security-testing scenario has moving parts: a Kali toolbox and something to point it at, both wired onto the same network. Typing a chain of docker run commands with matching --network flags every time is tedious and easy to get subtly wrong. Docker Compose fixes that. In this lesson you will describe a small two-service lab in a single compose.yaml file and bring the whole thing up — reproducibly — with one command.
🛠️ DevOps Perspective — A Compose file is your lab. Because the entire environment is declared in one version-controlled file, anyone on your team can
git clonethe project and get a byte-for-byte identical setup. That is the difference between “I think I ran some commands last week” and a lab you can rebuild on demand.
Only scan, inspect, or test systems you own or have explicit permission to assess. Everything in this lab is a container you create locally, so you are always testing your own infrastructure.
The Lab Architecture
The lab is two containers on a private bridge network. One is your Kali DevOps toolbox; the other is a benign web service that gives the toolbox something to probe, resolve, and connect to — without ever touching a real third party.
Docker Host
security-lab network
|
+------------+------------+
| |
kali web
DevOps Toolbox Test Service
The security-lab network is a user-defined bridge that Compose creates for you. Both containers attach to it, which means they share a private subnet and — importantly — can reach each other by service name. From the kali container, the hostname web resolves to the web container’s IP automatically. That built-in DNS is the whole reason this setup is convenient: no hard-coded IPs, no host ports required for the two services to talk.
The Compose File
Here is the complete lab definition. Save it as compose.yaml at the root of your project:
services:
kali:
build:
context: ./kali
stdin_open: true
tty: true
volumes:
- ./workspace:/workspace
networks:
- security-lab
web:
image: nginx:alpine
networks:
- security-lab
networks:
security-lab:
driver: bridge
Let’s walk through it piece by piece, because every line is doing a specific job.
The kali service
build: context: ./kali— Instead of pulling a prebuilt image, Compose builds this service from theDockerfilein the./kalidirectory. The build context is the folder Docker sends to the builder; everything the Dockerfile canCOPYmust live inside it. This is how you fold your reusable custom toolbox image straight into the lab.stdin_open: true— The Compose equivalent ofdocker run -i. It keeps STDIN open so you can type into the container’s shell interactively. Without it, an interactive shell would immediately see end-of-input and exit.tty: true— The equivalent ofdocker run -t. It allocates a pseudo-TTY so the shell behaves like a real terminal, with prompts, colors, and line editing. You almost always setstdin_openandttytogether for a container you intend to drop into.volumes: - ./workspace:/workspace— Bind-mounts the./workspacefolder on your host to/workspaceinside the container. Anything you save there — scan output, notes, scripts — lives on your host and survives the container being destroyed. This is your durable evidence directory.networks: - security-lab— Attaches the container to thesecurity-labnetwork defined below.
The web service
image: nginx:alpine— Pulls a small, ordinary Nginx web server. This is your deliberately benign test target: it listens on port 80 and serves a default page, giving you something real to resolve, curl, port-scan, and inspect. Using a plain public image (not a “vulnerable-on-purpose” one) keeps the lab safe and legal to run anywhere.networks: - security-lab— Putswebon the same network askali, which is what letskalireach it by the nameweb.
Notice the web service declares no ports: mapping. It does not need one. Port publishing only matters when you want to reach a container from the host. Here, container-to-container traffic stays entirely on the internal security-lab network, so kali can hit web:80 without exposing anything to your host or your LAN.
The networks block
security-lab: driver: bridge— Declares a user-defined bridge network. Thebridgedriver is Docker’s standard single-host network driver. Because it is user-defined (not the defaultbridge), it gives you automatic name-based service discovery between containers — the feature that makeswebresolve as a hostname.
🔐 Security Note — This lab needs no elevated privileges. There is no
--privileged, no added capabilities, no host networking, and no bind of the Docker socket. Everything runs as an ordinary bridge-networked container, and the two services are isolated on their own network away from your other containers. If a later exercise needs raw sockets (for example, somenmapscan types ortcpdump), add the narrowcap_add: [NET_RAW]to only thekaliservice — never reach forprivileged: true, which strips nearly all container isolation and is almost never the right answer. Least privilege means granting the one capability a task needs and nothing more.
The Reusable Project Layout
The Compose file references ./kali and ./workspace, so the file alone is not the whole lab. The real deliverable is a small, self-contained project directory you can reuse and share:
kali-devops-lab/
├── compose.yaml
├── kali/
│ └── Dockerfile
└── workspace/
compose.yaml— the environment definition you just wrote.kali/Dockerfile— builds your custom toolbox image (the curatedapttoolkit from earlier lessons). A minimal version looks like this:
FROM kalilinux/kali-rolling
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl dnsutils iproute2 iputils-ping nmap openssl jq && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
WORKDIR /workspace
CMD ["bash"]
Each instruction earns its place: FROM sets the Kali base image; RUN installs a curated toolkit in a single layer, then cleans the apt cache and lists in the same layer so the deletion actually shrinks the image (a separate cleanup layer would not); --no-install-recommends skips optional extras to keep the image lean; WORKDIR /workspace makes your mounted evidence directory the default landing spot; and CMD ["bash"] drops you into a shell. Keeping the whole apt sequence in one RUN also plays nicely with layer caching — change the package list and only this layer rebuilds.
workspace/— an initially empty folder that becomes the shared, persistent evidence directory via the bind mount.
🏭 Why This Matters in Production — This exact pattern scales. A staging environment, an integration test rig, or a CI security-scan job is the same idea with more services: declare everything in
compose.yaml, keep custom images in versioned build contexts, and treat the whole thing as a reproducible artifact. When your lab is a directory in Git, “works on my machine” stops being a debugging excuse.
Bringing the Lab Up and Working In It
From inside the kali-devops-lab/ directory, four commands run your entire lab lifecycle.
docker compose up -d
docker compose up -d
Reads compose.yaml, builds the kali image from its context, pulls nginx:alpine, creates the security-lab network, and starts both containers.
upcreates and starts everything defined in the file.-druns detached — the containers start in the background and hand your terminal back, instead of streaming their logs and holding the session. That is what you want for a lab you plan to exec into.
docker compose exec kali bash
docker compose exec kali bash
Opens an interactive bash shell inside the already-running kali container.
execruns a new command in a container that is already up (as opposed torun, which would start a new one).kalinames the service to enter.bashis the command to run — your shell.
Because you set stdin_open and tty, this shell is fully interactive. From here, verify the lab is wired correctly:
ping -c1 web
curl -sI http://web
ping -c1 web sends one packet (-c1 = count of 1) to prove name resolution and reachability work — web should resolve to the web container’s private IP. curl -sI http://web makes a request and prints only the response headers (-s silences the progress meter, -I fetches headers only). A 200 OK from Nginx confirms the two services can talk over the security-lab network.
🧪 Try It — Inside the
kalishell, run a quick service check against your own lab target:nmap -Pn web. It should report port 80 open, served by Nginx. Save the output to your persistent workspace withnmap -Pn web -oN /workspace/web-scan.txt, then exit and confirm the file exists on your host underworkspace/. You have just produced durable evidence from a disposable environment — and every target here is a container you created.
docker compose ps
docker compose ps
Lists the services in this project with their current state, so you can confirm both kali and web show as running (or Up). This is your expected state vs. observed state check: you expect two running services; if one is missing or restarting, that is your first diagnostic signal.
docker compose down
docker compose down
Stops and removes both containers and the security-lab network Compose created. The lab is torn down cleanly, leaving no stray containers or networks behind.
- Your
workspace/folder and its contents stay put — they live on the host, not in the containers. - To also delete named volumes, you would add
-v; this lab uses a bind mount, so there is nothing extra to clean up.
The whole point: docker compose up -d and docker compose down are a reproducible on/off switch for a complete, isolated environment.
⛔ Production Warning — This lab is intentionally scoped to a single host with the
bridgedriver and no published ports. Do not reuse this file as-is for anything internet-facing:nginx:alpinehere serves a default page as a test target, not a hardened public endpoint, and a real deployment needs TLS, resource limits, and a proper reverse-proxy configuration. Treat this as a disposable lab, not a production template.
Common Problems
Symptom: the kali container cannot reach web — ping: web: Name or service not known
The two services are not on the same user-defined network, so name-based discovery is not available.
- Confirm both services list
security-labundernetworks:incompose.yaml. A service with nonetworks:block lands on a different default network and will not resolve its peers by name. - Run
docker compose psto verifywebis actually running — you cannot resolve a service that never started. - From the
kalishell, rungetent hosts web. No output means DNS is not resolving the name; recheck step 1. If it resolves butcurl http://webstill fails, the name is fine and the problem is the service itself (see port conflicts below).
🔎 Troubleshooting Tip — Isolate the layers in order: name resolution (
getent hosts web), then reachability (ping -c1 web), then the application (curl -sI http://web). Whichever step first fails tells you the layer to fix, instead of guessing. That is expected-state-vs-observed-state thinking applied to a Compose lab.
Symptom: docker compose up fails with a build error like unable to prepare context: path "./kali" not found
The build: context: path does not point at a real directory containing a Dockerfile.
- Confirm you are running the command from the
kali-devops-lab/root, wherecompose.yamllives. Compose resolves./kalirelative to the Compose file, so running from the wrong directory breaks the path. - Verify the folder and file exist:
ls kali/Dockerfile. If the file is nameddockerfileor sits one level too deep, the build cannot find it. - Remember the build context also bounds what
COPYcan reach — aDockerfilethat copies a file from outside./kaliwill fail even if the context path itself is correct.
Symptom: docker compose up fails with Bind for 0.0.0.0:8080 failed: port is already allocated
Something on your host already holds a port you tried to publish. (The lab above publishes none, but you will hit this the moment you add a ports: mapping to reach a service from the host.)
- Find the conflicting process with
docker compose psand a host check likess -ltnp | grep :8080(orlsof -i :8080). - Either stop the other process, or change the host side of the mapping —
ports: ["8081:80"]publishes container port 80 on host port 8081 instead. Only the left-hand (host) number has to be free; the container-side port can stay the same. - If a previous lab did not shut down cleanly,
docker compose downfirst to release ports it may still be holding.
Where to Go Next
- Kali Docker networking — how containers get addresses, routes, and name resolution, which is the machinery behind service discovery in this lab.
- Nmap service discovery in Docker — scan the
webservice you just built and map what is actually listening. - HTTP and API troubleshooting — take
curl http://webfurther into real request/response debugging. - TLS certificate troubleshooting — add HTTPS to a lab service and inspect its certificate chain.
- Docker Compose Generator — a visual builder for assembling and validating
compose.yamlfiles when your lab grows beyond two services.
What You Learned
- A single
compose.yamldeclares an entire multi-container lab, sodocker compose up -danddocker compose downbecome a reproducible on/off switch for the whole environment. - Putting both services on a user-defined
bridgenetwork gives you automatic name-based discovery —kalireacheswebby hostname with no published ports and no hard-coded IPs. stdin_open+ttyare the Compose equivalents of-i+-t, and a./workspacebind mount keeps your evidence on the host after disposable containers are gone.- The reusable
kali-devops-lab/project (compose.yaml+kali/Dockerfile+workspace/) folds your custom toolbox image into the lab via a build context and is safe to commit and share. - This lab needs no elevated privileges; grant a narrow
cap_addlikeNET_RAWonly when a specific task requires it, neverprivileged: true. - Systematic troubleshooting — resolution, then reachability, then application — pinpoints Compose failures like cross-network isolation, bad build-context paths, and port conflicts.
Recommended Reading
- View Book on Amazon Affiliate link
Kali Linux Penetration Testing Bible
A comprehensive reference for structured security-testing workflows with Kali.
- View Book on Amazon Affiliate link
The Ultimate Kali Linux Book
A broad Kali Linux reference covering installation, configuration, and its security tooling.
Affiliate Disclosure: Some links on this page are affiliate links. If you purchase through one of these links, DevOps AI Toolkit may earn a commission at no additional cost to you. See our affiliate disclosure.
← Back to Kali Linux on Docker Back to Kali Linux