Skip to content
DevOps AI ToolKit
Newsletter

Kali Linux on Docker · Part 4 of 16

Build a Custom Kali Linux Docker Image

Difficulty: Beginner ~15 min Part 4/16
Prerequisites: Kali Linux fundamentalsBasic Docker knowledge
Series progress4 / 16
Series curriculum (16 lessons)

Installing tools by hand every time you start a container works for a quick check, but it does not scale. The moment you close that container, the tools are gone, and the next person who runs your image has to remember the exact same apt-get install line you did. The fix is a Dockerfile — a small text file that describes, step by step, how to build an image with your tools already baked in. In this lesson you will write a Dockerfile for a lean Kali toolbox, build it, run it, and understand every instruction so you can extend it later.

What a Dockerfile actually is

A Dockerfile is a recipe. Docker reads it top to bottom, runs each instruction, and saves the result as a reusable image — a frozen filesystem plus a bit of metadata about how to start it. Once built, that image is a fixed artifact: anyone who pulls it gets exactly the tools you put in, in exactly the versions the base image pinned. That is the whole point. Instead of “install these ten packages and hope you typed it right,” you ship one file, and the build is repeatable on any machine with Docker.

Here is the toolbox we are going to build. Create an empty directory, drop this in a file named Dockerfile (no extension), and we will walk through it line by line.

FROM kalilinux/kali-rolling

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
       curl \
       dnsutils \
       iproute2 \
       iputils-ping \
       jq \
       nmap \
       openssl \
       tcpdump \
       traceroute \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /workspace

CMD ["bash"]

That is a complete, working image in four instructions. Let’s break down what each one does.

FROM kalilinux/kali-rolling

Every image starts from a base image, and FROM names it. Here we build on top of kalilinux/kali-rolling, the official Kali rolling-release image. Docker pulls that image first, then layers your changes on top. You are not building Kali from scratch — you are inheriting a full Kali userland and adding exactly the packages you care about. FROM must be the first real instruction in every Dockerfile.

💡 Notekali-rolling is a moving target: it tracks Kali’s rolling release, so rebuilding next month may pull newer package versions. That is usually what you want for a security toolbox, but see the DevOps Perspective below on pinning when you need bit-for-bit reproducibility.

RUN apt-get update && apt-get install ...

RUN executes a command at build time and saves the result as a new layer in the image. This is the instruction that actually installs your tools. A few things are deliberate here:

  • apt-get update && apt-get install are chained in one RUN. They are joined with && and line-continuation backslashes (\) so the whole thing is a single instruction that produces a single layer. If you split update and install into two separate RUN lines, Docker can cache the update layer and later reuse a stale package index — you would install old or missing packages. Keeping them together avoids that classic trap.
  • -y answers “yes” to apt’s prompts automatically. A build is non-interactive; there is no human to type “Y”, so without this the build hangs or fails.
  • --no-install-recommends tells apt to install only the packages you asked for, not the long list of “recommended” extras Debian would normally pull in. On a toolbox image that trims a lot of weight — smaller image, faster pulls, less to keep patched.
  • apt-get clean && rm -rf /var/lib/apt/lists/* deletes apt’s downloaded package archives and the package index it fetched during update. You do not need them once the tools are installed, and because this happens in the same RUN layer, the deleted files never get baked into the image. Do the cleanup in a later RUN and it would not help — the files would already be frozen in an earlier layer.

The tools themselves are a focused DevOps troubleshooting kit: curl (HTTP), dnsutils (dig/nslookup), iproute2 (ip/ss), iputils-ping, jq (JSON), nmap (port/service scanning), openssl (TLS inspection), tcpdump (packet capture), and traceroute (path tracing). Nothing you will not use.

WORKDIR /workspace

WORKDIR sets the working directory for everything that follows — and for the shell you land in when you run the container. Here we set /workspace, so when you start the container you begin in /workspace rather than /. It is the natural place to mount a volume later (covered in volumes and persistence) so your scan output survives after the container exits. If the directory does not exist, WORKDIR creates it.

CMD ["bash"]

CMD sets the default command that runs when someone starts a container from this image without specifying one. ["bash"] means “drop me into a Bash shell.” We use the JSON-array form (called exec form) because it runs bash directly as the container’s main process, without wrapping it in a shell — cleaner signal handling and a clearer process tree. Unlike RUN, CMD does not execute during the build; it only defines what happens at run time, and you can always override it on the command line.

Why layer caching matters

Each instruction in a Dockerfile produces a layer, and Docker caches layers. When you rebuild, Docker reuses cached layers for any instruction whose inputs have not changed, and only re-runs from the first line that did change downward. That is why the order of instructions matters: put the things that rarely change (your base image, your package install) near the top, and things that change often (copying in your own scripts, for example) near the bottom. In this small Dockerfile the payoff is simple — if you have not edited the file, a rebuild finishes almost instantly because every layer is a cache hit.

This is also why the single-RUN install block is written the way it is. It is one layer: change one package in that list and Docker rebuilds the whole install step (correct — you want a fresh, consistent set of tools), but leave it alone and the layer is reused untouched.

🛠️ DevOps Perspective — This Dockerfile is the deliverable. A reproducible, version-controlled toolbox beats a container you hand-tuned once and can never recreate. Commit the Dockerfile to git and your whole team builds the identical environment from one source of truth. When you need true bit-for-bit reproducibility — say, for a CI job that must behave the same in six months — pin the base image to a digest (FROM kalilinux/kali-rolling@sha256:...) instead of the moving latest, and pin package versions in the install line (nmap=7.94...). Rolling gives you fresh tools; digests and version pins give you a build you can reproduce exactly. Choose per use case.

Build it

From the directory that contains your Dockerfile, run:

docker build -t kali-devops-toolbox .
  • docker build reads the Dockerfile and produces an image.
  • -t kali-devops-toolbox tags the image with a friendly name so you can refer to it later instead of a random ID. (You can add a version too, e.g. -t kali-devops-toolbox:1.0.)
  • . is the build context — the directory Docker sends to the builder and where it looks for the Dockerfile. The dot means “the current directory.”

You will watch Docker pull the base image (the first time), run your apt-get install, and finish with a line like naming to ... kali-devops-toolbox. Confirm it landed:

docker images kali-devops-toolbox

Run it

Start a container from your new image:

docker run --rm -it kali-devops-toolbox
  • --rm deletes the container automatically when you exit, so you do not accumulate dead containers. The image stays; only the disposable running instance is cleaned up.
  • -i keeps STDIN open so you can type into the shell.
  • -t allocates a TTY so the shell is interactive and formatted like a normal terminal.

Together -it gives you an interactive shell. Because your CMD is ["bash"], you land directly in Bash — inside /workspace, with every tool ready. Try a couple:

dig +short example.com
curl -sI https://example.com
nmap --version

No installing, no waiting. That is the difference between a hand-configured container and a built image.

🔐 Security Note — Only scan, inspect, or test systems you own or have explicit permission to assess. example.com is a safe, public endpoint intended for exactly this kind of demonstration. Point these tools at your own lab services, not third parties.

💡 Note — Some tools in this toolbox (notably tcpdump and certain nmap scan modes) need extra Linux capabilities to capture raw packets. Rather than reaching for --privileged, which strips away most of the container’s isolation, grant only the narrow capability the task needs — docker run --cap-add=NET_RAW .... Least privilege is covered in the networking and packet-capture lessons of this series.

This is the start of your kali-devops-lab project

Do not think of this Dockerfile as a throwaway. It is the foundation of a small project you will grow across this series. As you go, you will build out a layout like:

kali-devops-lab/
└── kali/
    └── Dockerfile      ← the file you just wrote

In later lessons you will add a compose.yaml beside it to wire the toolbox into lab networks, mount volumes into /workspace so results persist, and eventually assemble a full multi-service security lab. Everything builds on this one image. Keep kali-devops-lab/kali/Dockerfile in version control and treat it as the canonical definition of your toolbox — the build-your-DevOps-toolbox lesson pulls the whole thing together.

Try It Yourself

  1. Create a fresh directory and add the Dockerfile above.
  2. Build it: docker build -t kali-devops-toolbox .
  3. Add one more tool — say netcat-openbsd — to the install list, rebuild, and watch which layers are cache hits and which rebuild.
  4. Run docker run --rm -it kali-devops-toolbox and verify your new tool is present.
  5. Compare image sizes with and without --no-install-recommends (docker images) to see how much the recommends actually add.

🧪 Try It — Delete the image (docker rmi kali-devops-toolbox) and rebuild it. Because your Dockerfile is the source of truth, the toolbox comes back identical. That round-trip is what “reproducible” means in practice.

Common Problems

Build fails at apt-get update with hash or “Could not resolve” errors. This is almost always DNS or network on the build host, or a temporarily stale mirror. Check the host can reach the internet, then rebuild. If it persists, re-run to pull a fresh package index — a mid-flight mirror update can cause a transient hash mismatch.

Unable to locate package <name> during install. The package name is wrong or the index is stale. Because apt-get update and install are chained in one RUN, a fresh build always refreshes the index — but double-check the exact Debian/Kali package name (for example the DNS tools live in dnsutils, and modern netcat is netcat-openbsd). A typo in the list fails the whole layer.

pull access denied or manifest unknown / “image not found” on FROM. Docker cannot find kalilinux/kali-rolling. Confirm the name is spelled correctly, that you are online, and that you can pull it directly with docker pull kalilinux/kali-rolling. Behind a proxy or on a locked-down build agent, the registry may simply be unreachable.

The build “hangs.” Usually apt waiting on a prompt — confirm -y is present so installs are non-interactive. It can also just be a slow first-time pull of the base image; give it a moment on the initial build.

docker build says it can’t find the Dockerfile. You are not in the directory that contains it, or the file is misnamed (it must be exactly Dockerfile, capital D, no extension). Run ls to confirm, and remember the trailing . in docker build -t name . points at that directory.

🔎 Troubleshooting Tip — Think in expected vs observed state. You expect a tool to be present after the build; if command not found appears at run time, the problem is at build time. Re-read the build output for the install step — a failed package leaves a red line there, even if the build otherwise “succeeded” enough to produce an image.

Where to go next

What You Learned

  • A Dockerfile turns “install these tools by hand” into a repeatable, version-controlled build.
  • What FROM, RUN, WORKDIR, and CMD each do — and why the install is a single chained RUN.
  • Why --no-install-recommends plus same-layer apt-get clean && rm -rf /var/lib/apt/lists/* keeps the image small.
  • How layer caching makes rebuilds fast and why instruction order matters.
  • How to build (docker build -t kali-devops-toolbox .) and run (docker run --rm -it kali-devops-toolbox) your custom image.
  • That this Dockerfile is the reusable foundation of the kali-devops-lab/kali/Dockerfile project you extend across the series.

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