Docker for Security Labs: Building and Running Isolated Targets
Updated July 27, 2026 · Written by PWNMI — see About.
Every CVE lab on this site, and most home lab setups worth building, run on Docker. It's the fastest way to stand up an exact, specific vulnerable version of real software — a particular WordPress plugin release, a specific Apache Syncope build — without installing it directly on your own machine, and to tear it down completely afterward with no leftover state. If you've followed one of the CVE lab walkthroughs and typed the commands without fully knowing what they were doing, this guide is the piece that was missing.
Images, containers, and compose
Three terms, three different things. An image is a frozen snapshot of a filesystem plus what to run — think of it as a template. A container is a running (or stopped) instance of an image — the same relationship as a class and an object. Compose is a tool for defining and running multiple containers together as one unit, described in a docker-compose.yml file, so a database and an application server can be started, networked, and torn down as a single command instead of managed by hand one at a time.
Every lab on this site ships a docker-compose.yml rather than a bare docker run command specifically because real vulnerable applications almost always need more than one moving part — a database alongside the actual target, at minimum.
Installation
Docker ships pre-installed on some Kali images; check first with docker --version. Elsewhere:
# Debian/Ubuntu
curl -fsSL https://get.docker.com | sh
# add your user to the docker group so you don't need sudo for every command
sudo usermod -aG docker $USER
Log out and back in (or run newgrp docker) for the group change to take effect.
The isolation pattern this site uses
Every CVE lab on pwnmi.com follows the same networking pattern, and it's worth understanding why rather than just copying it:
networks:
lab:
driver: bridge
No ports: section anywhere. That's deliberate, not an oversight. On a normal Docker bridge network, the Docker host (your machine) can already reach any container's own IP address directly, on any port it's listening on — publishing a port with ports: is only necessary to expose a service to something other than the Docker host itself, like the rest of your LAN or the internet. Skipping ports: keeps a genuinely vulnerable, freshly-booted application reachable only from your own machine, never accidentally exposed to your network.
To reach a container this way, find its IP first:
docker inspect $(docker compose ps -q <service-name>) --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
Then point curl, a browser, or your scanning tools at that IP directly, on whatever port the service's own documentation says it listens on internally — not a host-mapped port, since there isn't one.
Bringing a lab up and down
docker compose up -d
-d runs it detached (in the background) rather than tying up your terminal. Give it a moment — a database container often needs several seconds before the application container that depends on it can actually connect.
docker compose logs <service-name>
docker compose ps -a
logs is the first place to look if something isn't responding. ps -a (not plain ps) matters more than it looks like it should: a container that crashed or ran out of memory disappears from the default view but still shows up with -a, which is often the only visible sign that something failed to start at all.
docker compose down -v
Tear a lab down completely when you're done — -v also removes the named volumes, so the next up starts from a genuinely clean state rather than reusing a database that still has your last session's data in it.
Common mistakes
- Assuming a container that isn't crashing is working. A service can be "up" and still not doing anything useful — always confirm with a real request (
curl, a health check, an application-specific readiness signal), not justdocker compose ps. - Publishing ports out of habit. Most lab work never needs a
ports:section at all — see the isolation pattern above. Only add one when you specifically need the service reachable from somewhere other than the Docker host itself. - Forgetting
-von teardown.docker compose downalone leaves named volumes behind. The next time you bring the same lab up, you may be looking at stale data from a previous run and not realize it. - Not checking
docker compose ps -a. A silently-crashed container is one of the most common reasons a lab "isn't working" — the plainpsview can hide it entirely.
Next step
For the command-level quick reference once you're comfortable with the concepts here, see the Docker Cheat Sheet. To see this exact pattern used against a real, disclosed vulnerability, start with CVE-2026-62183 (Apache Syncope).
Get new write-ups in your inbox
New roadmaps, tool walkthroughs, and lab write-ups. No spam. Unsubscribe anytime.