Skip to content
DevOps AI ToolKit
Newsletter

Kali Linux on Docker · Part 8 of 16

Build a Kali Linux Security Lab With Docker Compose

Difficulty: Intermediate ~18 min Part 8/16
Prerequisites: Kali Linux fundamentalsBasic Docker knowledge
Series progress8 / 16
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 clone the 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 the Dockerfile in the ./kali directory. The build context is the folder Docker sends to the builder; everything the Dockerfile can COPY must live inside it. This is how you fold your reusable custom toolbox image straight into the lab.
  • stdin_open: true — The Compose equivalent of docker 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 of docker run -t. It allocates a pseudo-TTY so the shell behaves like a real terminal, with prompts, colors, and line editing. You almost always set stdin_open and tty together for a container you intend to drop into.
  • volumes: - ./workspace:/workspace — Bind-mounts the ./workspace folder on your host to /workspace inside 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 the security-lab network 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 — Puts web on the same network as kali, which is what lets kali reach it by the name web.

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. The bridge driver is Docker’s standard single-host network driver. Because it is user-defined (not the default bridge), it gives you automatic name-based service discovery between containers — the feature that makes web resolve 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, some nmap scan types or tcpdump), add the narrow cap_add: [NET_RAW] to only the kali service — never reach for privileged: 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 curated apt toolkit 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.

  • up creates and starts everything defined in the file.
  • -d runs 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.

  • exec runs a new command in a container that is already up (as opposed to run, which would start a new one).
  • kali names the service to enter.
  • bash is 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 kali shell, 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 with nmap -Pn web -oN /workspace/web-scan.txt, then exit and confirm the file exists on your host under workspace/. 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 bridge driver and no published ports. Do not reuse this file as-is for anything internet-facing: nginx:alpine here 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 webping: web: Name or service not known

The two services are not on the same user-defined network, so name-based discovery is not available.

  1. Confirm both services list security-lab under networks: in compose.yaml. A service with no networks: block lands on a different default network and will not resolve its peers by name.
  2. Run docker compose ps to verify web is actually running — you cannot resolve a service that never started.
  3. From the kali shell, run getent hosts web. No output means DNS is not resolving the name; recheck step 1. If it resolves but curl http://web still 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.

  1. Confirm you are running the command from the kali-devops-lab/ root, where compose.yaml lives. Compose resolves ./kali relative to the Compose file, so running from the wrong directory breaks the path.
  2. Verify the folder and file exist: ls kali/Dockerfile. If the file is named dockerfile or sits one level too deep, the build cannot find it.
  3. Remember the build context also bounds what COPY can reach — a Dockerfile that copies a file from outside ./kali will 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.)

  1. Find the conflicting process with docker compose ps and a host check like ss -ltnp | grep :8080 (or lsof -i :8080).
  2. 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.
  3. If a previous lab did not shut down cleanly, docker compose down first to release ports it may still be holding.

Where to Go Next

What You Learned

  • A single compose.yaml declares an entire multi-container lab, so docker compose up -d and docker compose down become a reproducible on/off switch for the whole environment.
  • Putting both services on a user-defined bridge network gives you automatic name-based discovery — kali reaches web by hostname with no published ports and no hard-coded IPs.
  • stdin_open + tty are the Compose equivalents of -i + -t, and a ./workspace bind 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_add like NET_RAW only when a specific task requires it, never privileged: true.
  • Systematic troubleshooting — resolution, then reachability, then application — pinpoints Compose failures like cross-network isolation, bad build-context paths, and port conflicts.

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

Related on DevOps AI Toolkit