A tool for every job
Instead of installing tools and all of their dependencies on your host OS Docker lets you keep your host machine clean. Containers let you easily run tools with different versions and different userlands.
The version problem, gone
An exploit needs Python 3.11. Another needs 3.12. Your distro ships 3.13 and
your system tooling depends on it. On the host, that is pyenv, a virtualenv,
and an afternoon.
$ docker run --rm -it python:3.11 python -V
Python 3.11.16
$ docker run --rm -it python:3.12 python -V
Python 3.12.14
Both, right now, without touching your host Python. Drop the python -V and
you get a shell in that image instead:
$ docker run --rm -it python:3.11 bash
root@7709f12587bf:/# python
Python 3.11.16 (main, Aug 13 2026, 19:40:55) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
The same trick covers every runtime you have ever had a version fight with:
node:20, openjdk:11, golang:1.22, ruby:3.2, php:7.4 when you need to
reproduce something ancient.
Tags matter, latest lies
An image reference is repository:tag. The tag is a label someone moved to a
build, not a version guarantee, and latest is just the tag applied when you
do not ask for one. It is not “the newest release”, it is whatever was pushed
there last, which may be a nightly, or two years stale.
Be specific when you care:
$ docker run --rm ubuntu:22.04 cat /etc/os-release | head -2
PRETTY_NAME="Ubuntu 22.04.5 LTS"
NAME="Ubuntu"
When you really care, pin the digest. That is content addressed and cannot move under you:
$ docker run --rm ubuntu@sha256:941f1899488c45cd4657f7872ddec86b47907e3e2165b944a1219c80b63c283d uname -m
x86_64
Where images come from
That ubuntu:22.04 reference is missing a piece. The full form is
registry/repository:tag, and when you leave the registry off, Docker fills in
Docker Hub. These two are the same pull:
$ docker pull ubuntu:22.04
$ docker pull docker.io/library/ubuntu:22.04
A registry is just a server that stores images. docker pull fetches from
one, docker run pulls first if the image is not already local, and docker push sends one back. You saw the first pull happen in lesson 1 when
hello-world came down from Docker Hub.
Hub is the default, it isn’t the only one. The address in front of the repository names the server, so an image can come from anywhere:
$ docker pull ghcr.io/astral-sh/uv:latest
latest: Pulling from astral-sh/uv
2efb1cc6cef2: Pull complete
549437617bd5: Pull complete
Digest: sha256:e85be844203885286c60ffad8a858d48afb6c5a5c237ca0e67f12e74b8f174b1
Status: Downloaded newer image for ghcr.io/astral-sh/uv:latest
The registries you will meet most:
docker.io, Docker Hub, the default.library/is the namespace for official images, which is whyubuntuexpands todocker.io/library/ubuntu.ghcr.io, GitHub Container Registry. A lot of security tooling ships here now, tied to the project’s GitHub repo.quay.io, Red Hat’s registry.gcr.ioand its regional variants, plus AWS ECR and Azure ACR, which is where an organisation’s own images tend to live.
Because the name carries the whole address, running someone’s tool is a copy and
paste. That cuts both ways. docker run will fetch and execute code from a
server named in a string, so read the string. docker.io/library/nginx is the
official image, docker.io/evilcorp/nginx is a stranger’s, and typosquatted
repository names are a real supply-chain trick. The image runs as root by
default, so treat an unknown image the way you would treat curl | sh.
Once an image is local it stays cached, and a pull is a one-time cost. That is why the tools in this course still work on a machine with no internet, as long as you pulled them first:
$ docker run --rm --network none alpine:3.20 echo "ran from local cache with no network"
ran from local cache with no network
Public images pull with no account. Private ones need docker login first, and
without it the daemon refuses:
$ docker pull ghcr.io/offensivecontext/private-tool:latest
Error response from daemon: error from registry: denied
docker login ghcr.io with a token fixes that. Pushing your own image to a
registry, and the reasons to keep it private, is lesson 9.
If you would rather browse than guess, docker search queries Hub from the
command line:
$ docker search --limit 5 nmap
NAME DESCRIPTION STARS OFFICIAL
securecodebox/nmap A Docker image containing the NMAP security … 31
demisto/nmap 0
parrotsec/nmap Official Parrot container for nmap 1
instrumentisto/nmap Nmap ("Network Mapper") Docker Image 31
frapsoft/nmap nmap on Alpine Linux (6 MB) 3
STARS and OFFICIAL are the only trust signal it gives you, and neither is
much of one. Prefer an image whose source Dockerfile you can read, which for
ghcr.io images is usually one click away in the same GitHub repo.
Tools, not just runtimes
Most tools publish images, security tools included which means you can easily run one without installing it or its dependency tree:
$ docker run --rm instrumentisto/nmap -V
Nmap version 7.98 ( https://nmap.org )
Platform: x86_64-unknown-linux-gnu
Compiled with: liblua-5.4.8 openssl-3.5.5 libssh2-1.11.1 libz-1.3.1 nmap-libpcre2-10.45 libpcap-1.10.5 nmap-libdnet-1.18.0 ipv6
Notice we just used -V, not nmap -V. That image
sets nmap as its entrypoint, so anything you type is passed to it as arguments.
Images built from a base OS usually do not, which is why the Ubuntu examples
all end in an explicit bash.
There is another catch worth knowing, the scan runs from the container’s
network, not your host’s. Lesson 6 covers this, for now know --network host
is the flag that makes this alias behave more like a localls installed nmap.
A container isolated
The other half of the value is containment. When you have to run something you do not trust, an install script, a sample, a random ELF from a machine you are looking at, do not run it as you.
$ docker run --rm -it --network none -v "$PWD/sample:/s:ro" ubuntu:22.04 bash
Three things happened there. --network none gave it a network stack with nothing but
loopback, so it cannot phone home. :ro on the mount made your copy of the
sample read only. --rm means whatever it did to its filesystem evaporates on
exit.
This a lightweight sandbox. It is the same kernel, lesson 8 is a tour
of some of the ways out. Against malware that is actively looking for a way onto your
host, use a VM. Against a build script you would rather not have writing to
~/.ssh, this is exactly right and takes one second to set up.
Next: Cross compiling and getting files in and the results back out.