Tearing it down
Two loose ends. Docker will eat your disk if nobody stops it, and the image you built is stuck on the machine where you built it. Both are five minute problems once you know the commands.
Putting the lab away
$ docker compose down
Container dfh-lab-kali-1 Removed
Container dfh-lab-web-dvwa-1 Removed
Container dfh-lab-juice-shop-1 Removed
Network dfh-lab_private Removed
Network dfh-lab_public Removed
Containers and networks gone, images kept, so docker compose up -d tomorrow is
instant. Add -v to delete named volumes too, which is how you reset a target
that has stored state, and --rmi all to delete the images as well.
Run it from the directory that holds the compose file, because that is what
names the project. From anywhere else, docker compose down finds nothing and
says so cheerfully.
Where the disk went
$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 20 0 10.36GB 8.453GB (81%)
Containers 0 0 0B 0B
Local Volumes 0 0 0B 0B
Build Cache 17 0 2.557GB 49.31kB
Ten gigabytes of images and two and a half of build cache, from one course. This
is the normal steady state, and it is why df -h surprises people three months
in. Nothing here is cleaned up automatically.
Targeted removal first, since it is the safe kind:
$ docker rm -f demo # one container, running or not
$ docker rmi builder # one image
$ docker network rm recon # one network
$ docker image prune # dangling images only, no tags
$ docker builder prune # build cache
docker image prune without -a only removes untagged layers left behind by
rebuilds, which is the cleanup you can run without thinking about it.
$ docker system prune -a
WARNING! This will remove:
- all stopped containers
- all networks not used by at least one container
- all images without at least one container associated to them
- all build cache
Are you sure you want to continue? [y/N]
Read that second to last line carefully. With -a it means every image not
currently backing a running container, including the ones you spent an hour
building and the base images you will re-download at 400MB a go. Fine on a
laptop with fast internet, painful on a plane.
If a lab has gone sideways and you want everything gone right now:
docker kill $(docker ps -q) 2>/dev/null
docker rm $(docker ps -aq) 2>/dev/null
docker rmi $(docker images -q) 2>/dev/null
Moving an image to a machine with no internet
You built the attackbox image. Now you need it on a jump host that cannot pull from Docker Hub, or on a laptop going somewhere with no useful connectivity.
docker save writes an image to a tar stream:
$ docker save attackbox | gzip > attackbox.tar.gz
$ ls -lh attackbox.tar.gz
-rw-rw-r-- 1 evan evan 73M Aug 18 02:59 attackbox.tar.gz
73MB, from a 282MB image. Copy it over by whatever means you have and load it on the far side:
$ docker load -i attackbox.tar.gz
Loaded image: attackbox:latest
Straight down a pipe works too, and skips writing the file at all:
docker save attackbox | gzip | ssh jump 'gunzip | docker load'
save and load work on images. The pair you want for a container’s
filesystem is docker export and docker import, which flattens it and drops
the history and metadata. Reach for those when you want a container’s contents,
not a rebuildable image.
Or push it somewhere
With connectivity, a registry is less work. Tag the image with the registry path and push:
docker tag attackbox ghcr.io/yourname/attackbox:2026-08
docker push ghcr.io/yourname/attackbox:2026-08
Two things to get right before you do that. Make the repository private unless
you have read every layer of that image, because docker history shows what was
run and any file you ever COPYd is in there permanently, even if a later layer
deleted it. And tag with something meaningful, 2026-08 or a git SHA, not
latest, so the image you pull on the engagement is the image you tested.
The same rule applies to a Dockerfile you commit: build arguments and env vars are visible in the image metadata. Secrets go in at run time, not build time.
Keep the lab
The whole thing is a directory with three files in it: compose.yaml, a
Dockerfile for your image, and whatever tooling you COPY in. Commit it. Add
targets as you meet them, break them, docker compose down, and it costs you
nothing until the next time you need a network to practise on.