How images, containers, volumes, and networks fit together in Docker's runtime model, and the mental shift from "installing software" to "running a packaged image" that trips up newcomers.
Docker packages an application together with everything it needs to run - a base OS layer, a runtime, dependencies, and the app itself - into a single, portable image. Running that image starts a container: an isolated process with its own filesystem view, network namespace, and process tree, sharing the host's kernel rather than virtualizing an entire OS the way a VM does. That's the core mental shift for anyone coming from "installing software": you don't configure a machine to run your app, you build an image once and run it, identically, on a laptop, a CI runner, or a production host. Everything else - volumes for persistent data, networks for container-to-container communication, Compose for running several containers as one unit - exists to manage the boundary between that disposable, reproducible container and the stateful, connected world around it.
Quick Reference
Concept
Purpose
Lifetime
Image
Read-only, layered filesystem snapshot + metadata
Immutable once built
Container
A running (or stopped) instance of an image, plus a writable layer
Disposable, recreated freely
Volume
Named, Docker-managed persistent storage, mounted into a container
Long-lived, survives container removal
Network
Virtual network connecting containers, with built-in DNS by name
Long-lived
Compose service
One entry in a compose.yaml, describing how to run a container
Declarative, versioned with the project
The commands below are grouped the way you'd reach for them day to day, not an exhaustive CLI reference (see the official Docker CLI reference for every flag), but the ones that cover almost all daily Docker work.
Command
Description
Copy
docker version
Show the Docker client and server (Engine) version.
docker info
Show system-wide information: storage driver, containers, images, resource limits.
docker login
Authenticate to a registry (Docker Hub by default).
docker logout
Remove locally stored registry credentials.
docker search nginx
Search Docker Hub for images matching a term.
Command
Description
Copy
docker run -d -p 8080:80 --name web nginx
Run a container in the background, publish host port 8080 to container port 80, and name it "web".
docker ps
List running containers.
docker ps -a
List all containers, including stopped ones.
docker stop <container>
Gracefully stop a running container (SIGTERM, then SIGKILL after a timeout).
docker start <container>
Start a previously stopped container.
docker restart <container>
Stop then start a container.
docker exec -it <container> sh
Open an interactive shell inside a running container.
docker logs -f <container>
Stream a container's logs (add -f to follow).
docker rm <container>
Remove a stopped container.
Command
Description
Copy
docker pull nginx:1.27
Download an image from a registry, at a specific tag.
docker images
List locally stored images.
docker build -t app:1.0 .
Build an image from the Dockerfile in the current directory, tagged app:1.0.
docker tag app:1.0 registry.example.com/app:1.0
Add another tag to an existing image, typically before pushing to a registry.
docker push registry.example.com/app:1.0
Upload an image to a registry.
docker rmi app:1.0
Remove a local image.
docker image prune
Remove dangling (untagged) images.
Command
Description
Copy
docker network ls
List networks.
docker network create app-net
Create a user-defined bridge network; containers on it resolve each other by container name.
docker network inspect app-net
Show a network's configuration and which containers are attached.
docker network connect app-net web
Attach a running container to an additional network.
docker network rm app-net
Remove a network (fails if containers are still attached).
Command
Description
Copy
docker volume ls
List volumes.
docker volume create db-data
Create a named, Docker-managed volume.
docker run -v db-data:/var/lib/postgresql/data postgres
Mount a named volume into a container at a given path.
docker volume inspect db-data
Show a volume's mountpoint and metadata.
docker volume rm db-data
Remove a volume - irreversible if nothing else has a copy of the data.
Command
Description
Copy
docker compose up -d
Create and start every service in compose.yaml, in the background.
docker compose ps
List containers managed by this Compose project.
docker compose logs -f
Stream logs from every service.
docker compose exec api sh
Open a shell inside a running Compose service.
docker compose build
Build (or rebuild) images for services with a build: section.
docker compose down
Stop and remove containers and networks created by up.
Command
Description
Copy
docker build --no-cache -t app:1.0 .
Rebuild ignoring every cached layer.
docker history app:1.0
Show the layer history and size contribution of each layer.
docker system prune
Remove stopped containers, dangling images, and unused networks.
docker system prune -a
Also remove every unused image, not just dangling ones.
docker volume prune
Remove all unused volumes - irreversible data loss for anything unmounted.
Syntax
dockerfile
FROM node:20-alpineWORKDIR /appCOPY package.json package-lock.json ./RUN npm ci --omit=devCOPY . .EXPOSE 3000CMD ["node", "server.js"]
Writing data a container needs to keep into its own writable layer instead of a volume or bind mount; it's gone the moment docker rm runs.
Confusing EXPOSE in a Dockerfile with actually publishing a port; EXPOSE only documents intent, -p host:container on docker run is what makes a port reachable from outside.
Running containers as the default root user in production images, which turns a container escape into a host-level root compromise.
Tagging every build latest and deploying that tag, so "which version is actually running" becomes unanswerable - pin explicit version tags instead.
Running docker system prune -a on a shared host without checking what's still in use; it deletes any image not backing a currently running container.
Performance
Docker layers a Dockerfile top to bottom and caches each layer; ordering rarely-changing steps (installing dependencies) before frequently-changing ones (copying source code) means most rebuilds only re-run the last few layers.
Multi-stage builds (FROM ... AS build, then a second FROM that copies only the compiled output) keep the final image small by discarding build-time tooling that isn't needed at runtime.
Smaller base images (alpine, distroless) reduce image pull time and attack surface, but can lack libraries (like glibc) that some binaries assume are present - worth checking before committing to one.
Unbounded containers can starve a host of memory or CPU; --memory and --cpus on docker run (or deploy.resources.limits in Compose) cap what a single container can consume.
Best Practices
Use multi-stage builds and a .dockerignore file so build context and final images only contain what's actually needed at runtime.
Pin base image and dependency versions explicitly; never rely on a moving latest tag for anything beyond local experimentation.
Run as a non-root user inside the container (USER node, or an equivalent line for your base image) unless there's a specific reason not to.
Keep one primary process per container; if two unrelated services need to scale or fail independently, they belong in two containers, not one with a supervisor process.
Reach for volumes (not bind mounts) for data a container owns and manages itself, and bind mounts mainly for local development, where you want host file changes to show up instantly.
Interview questions
What is the difference between a Docker image and a Docker container?
An image is a read-only, layered filesystem snapshot plus metadata (entrypoint, exposed ports, environment); it never changes once built and can be shared through a registry. A container is a running (or stopped) instance of an image: Docker adds a thin writable layer on top of the image's read-only layers and starts a process inside an isolated namespace. You can start many independent containers from the same image, each with its own writable layer and state, the same way many processes can run from the same binary on a normal OS.
Why does data written inside a container disappear when the container is removed?
Anything a container writes lands in its own writable layer, which is deleted along with the container by `docker rm`. That is deliberate; it is what makes containers disposable and reproducible, a fresh container from the same image always starts from the same known state. Anything that actually needs to survive a container's lifecycle (a database's data files, uploaded assets) has to live outside that writable layer, in a named volume or a bind mount, which Docker mounts into the container at a chosen path but manages independently of the container itself.
Why is publishing a port with `-p 8080:80` different from the container just "having" port 80?
A container's ports exist only on its own private network namespace by default; nothing on the host or outside can reach them until Docker explicitly forwards a host port to it. `-p 8080:80` tells Docker's network layer to forward the host's port 8080 to port 80 inside the container's namespace, host port first, container port second. Leaving a port `EXPOSE`d in a Dockerfile only records metadata/documentation, it has no effect on connectivity at all: another container on the same Docker network can already reach any port the first container is listening on, EXPOSE or not. Publishing to the host is the one thing that always requires an explicit `-p`.