Build a Security Lab With Docker
Updated September 20, 2026: loopback bindings, versioned database example and scoped cleanup.
0 / 7 steps
The goal
Stand up a real, deliberately vulnerable target on your own laptop in one command, poke at it, and tear the whole thing down leaving no mess behind. By the end you will know why Docker is the fastest way to build a throwaway security lab — and you will have the exact commands the rest of the StashGrid labs assume you already know.
Only attack what you own. Everything here runs on your machine, bound to localhost. The target you launch (OWASP Juice Shop) is intentionally vulnerable — that is the point, but it also means you must never expose it to the public internet or run it on a shared server. Use Docker Engine 28.0.0 or newer with its normal bridge/NAT configuration. Loopback publishing limits host-port access; containers still have outbound access and this is not complete isolation. Do not use host networking, a public tunnel, or custom direct routing. Docker documents a localhost exposure issue in older releases.
Why Docker instead of a VM? A virtual machine boots a whole operating system to run one tool — gigabytes of disk, a minute to start, and a chore to reset. A container shares your machine's kernel and ships only the app and its dependencies, so it starts in seconds, weighs megabytes, and vanishes cleanly when you are done. For labs you spin up, break, and throw away, that difference is everything.
| Virtual machine | Container | |
|---|---|---|
| Contains | A full guest OS + the app | Just the app + its libraries |
| Size | Gigabytes | Tens to hundreds of MB |
| Startup | ~30–60 seconds | 1–3 seconds |
| Reset | Restore a snapshot, or rebuild | Delete it and run again |
| Best for | A different kernel, full desktop, kernel-level tooling | Throwaway targets, single tools, reproducible labs |
Steps
01Install Docker
Grab Docker Desktop (Windows / macOS) or Docker Engine (Linux) from the official install page, then confirm it runs. On Windows and macOS, launch Docker Desktop once so the engine is running before you use the command line.
$ docker version
You want a version line back, e.g. Server Engine 28.0.0 or newer. If the command is not found, Docker is not installed or (on Win/macOS) Docker Desktop is not started yet.
Why this first. Every other command in every Docker lab — here, Splunk, the DNS resolver lab — assumes docker is on your PATH and the engine is running. Get this green before moving on.
02Prove it works with a throwaway container
Before a real target, run the smallest possible container. Docker downloads a tiny test image, runs it, prints a message, and the container exits — a complete life cycle in one line.
$ docker run --rm hello-world
You will see Hello from Docker! and a short explanation. That single command pulled an image from Docker Hub, created a container from it, ran it, and stopped it.
The mental model. An image is the frozen template (like an installer); a container is a running copy of it. You can start many containers from one image, and deleting a container never touches the image — so you can break a container freely and recreate it in seconds.
03Launch a real vulnerable target
Before starting, run docker ps -a and docker volume ls. If juice, juice2 or labdb already belongs to another project, choose new names and use them consistently in every command below.
Now the payoff: OWASP Juice Shop is a modern web app built to be full of realistic bugs for practice. One command pulls it and runs it in the background, mapped to a port on your machine.
$ docker run -d -p 127.0.0.1:3000:3000 --name juice bkimminich/juice-shop
The first run downloads the image (a minute or two). When it returns a long container ID, open http://localhost:3000 in your browser — a working shop, yours to attack. -d runs it detached (in the background); --name juice gives it a friendly handle for the commands below.
Verify the binding. Run docker port juice; expect 3000/tcp -> 127.0.0.1:3000. If it shows 0.0.0.0 or [::], stop your lab container and recreate it with the loopback command. Use http://127.0.0.1:3000 on the Docker host.
What just happened. You deployed a full Node.js application, with no Node, no npm, and no dependency hell on your host — all of it lives inside the container. This is the whole reason Docker labs are painless: the target brings its own environment.
04Understand the port mapping
That -p 127.0.0.1:3000:3000 is the bridge between your machine and the container. The format used here is host-IP : host-port : container-port — left side is the door on your laptop, right side is the port the app listens on inside the container.
$ docker run -d -p 127.0.0.1:8080:3000 --name juice2 bkimminich/juice-shop
This second copy answers on http://localhost:8080 even though the app inside still uses 3000. Keep 127.0.0.1 unchanged and change only the host port (8080 here) to move where a target shows up on your machine — handy when two labs both want the same internal port.
Why it matters for labs. Other containers on the same network, and the Docker host, may reach container services without a published port. A loopback port mapping gives this lesson an explicit local browser address. Forgetting -p is the number-one reason "it's running but I can't connect." (Tear down this extra copy in Step 7.)
05See what is running, and read its logs
Two commands you will use constantly: list the live containers, and tail a container's output to see what it is doing or why it failed.
$ docker ps
Shows each running container's ID, image, ports, and the name you gave it. To watch a target's output live:
$ docker logs -f juice
-f follows the log like tail -f; press Ctrl+C to stop watching (that does not stop the container).
Your debugging pair. When a lab target seems dead, docker ps tells you if it is even running and on which port, and docker logs tells you what it is complaining about. Reach for these before assuming anything is broken.
06Keep data with a volume (when you want to)
By default a container is disposable: delete it and everything inside is gone — usually exactly what you want in a lab. When you do need data to survive (a database, saved findings), mount a volume: a folder that lives on your host and is handed to the container.
$ docker run -d -p 127.0.0.1:5432:5432 -e POSTGRES_PASSWORD=labpass -v labdb:/var/lib/postgresql/data --name labdb postgres:17
This exercise uses postgres:17 with /var/lib/postgresql/data. PostgreSQL 18+ official images use a different volume layout; do not substitute an unversioned image for this persistence exercise. See the official image documentation. The example password is for this disposable local lab only.
The -v labdb:/var/lib/postgresql/data stores the database in a named volume called labdb. Destroy and recreate the container all you like — point a new one at the same volume and the data is still there.
The trade-off. No volume = a clean slate every run, perfect for repeatable exercises. A volume = state that outlives the container, needed for anything with a real backend. Knowing which you want is half of designing a lab.
07Tear it all down cleanly
Remove only the containers you created for this exercise. First run docker ps -a and verify ownership. If any example name already exists before starting, choose different names throughout the lesson. Keep shared images; decide separately whether to erase the database volume.
$ docker rm -f juice juice2 labdb
The named containers are removed. Downloaded images and the labdb database volume remain. To retain your practice database, keep that volume. To erase only this lab database, inspect docker volume inspect labdb and verify ownership before deliberately running docker volume rm labdb after its container is removed. Volume deletion is permanent. Do not run whole-machine prune commands for this exercise.
You can now deploy a target, reach it, watch it, persist its data when needed, and wipe it clean — the exact skills every hands-on lab here builds on.
What you learned
- Image vs container — a frozen template vs a running, disposable copy of it.
- docker run -d -p 127.0.0.1:host-port:container-port — launch a target in the background and map it onto a port you can reach.
- docker ps / logs — your first two debugging moves when a target misbehaves.
- Volumes — opt in to data that survives the container, opt out for a clean slate.
- Scoped cleanup — remove your named lab containers; retain shared images and decide separately whether to erase the database volume.
NextSplunk Docker lab — practise searching sample logs→
Go deeper
- Scan the Juice Shop target you just launched with the Nmap cheat sheet — point it at localhost -p 3000.
- Once you are comfortable here, the Bandit and Natas labs give you targets to practise the attacks on.
- Official docs: Docker Get Started and OWASP Juice Shop.
First in a Docker lab track — the Splunk-at-home SIEM lab builds straight on this. Spotted a command that has drifted, or want a target added? Tell me and I will fix it.