Skip to main content

Images and Containers

With Docker installed, you are ready to start pulling images and running containers. This chapter walks through the core workflow you will repeat hundreds of times: fetch an image, start a container with the right options, inspect what is running, and clean up when you are done.


Pulling Images

Images live in registries. The docker pull command downloads an image to your local machine so you can start containers from it.

# Pull the latest official nginx image
docker pull nginx

# Pull a specific version (tag)
docker pull nginx:1.27-alpine

# Pull from a non-default registry (GitHub Container Registry)
docker pull ghcr.io/owner/my-app:latest

Image names follow the pattern [registry/][namespace/]name[:tag]. When you omit the registry, Docker defaults to Docker Hub. When you omit the tag, Docker uses latest.

Browsing available tags

Docker Hub shows available tags at https://hub.docker.com/_/nginx. For images you use regularly (Node.js, Postgres, Redis), it is worth checking the tags page to understand what variants exist — alpine variants are smaller, numbered tags are stable, slim variants have fewer tools pre-installed.


Running Your First Real Container

docker run is the most important command in your toolkit. It pulls the image if not cached locally, creates a container from it, and starts it.

# Start an nginx web server, publish port 8080 on the host to 80 in the container
docker run -d -p 8080:80 --name my-nginx nginx

Open http://localhost:8080 in your browser — you should see the nginx welcome page.

Let us unpack the flags:

FlagLong formMeaning
-d--detachRun in the background; print the container ID and return to the prompt
-p 8080:80--publish 8080:80Map host port 8080 to container port 80
--name my-nginxGive the container a human-readable name instead of a random one

Essential Flags

Detached mode (-d)

Without -d, the container runs in the foreground and its output streams to your terminal. This is fine for quick tests but impractical for servers. Use -d for long-running services.

# Foreground (blocks your terminal, Ctrl-C stops the container)
docker run nginx

# Background
docker run -d nginx

Port mapping (-p)

Containers have their own network stack. A service inside a container is not reachable from your host unless you explicitly map a port.

# host_port:container_port
docker run -d -p 3000:3000 node-app

# Map multiple ports
docker run -d -p 8080:80 -p 8443:443 nginx

# Bind to a specific host interface (avoid exposing to the network)
docker run -d -p 127.0.0.1:5432:5432 postgres

Volume mounts (-v)

Containers have an ephemeral writable layer — data written inside a container is lost when the container is removed. Use -v to persist data. Volumes are covered in depth in Chapter 4; here is the syntax:

# Named volume (Docker manages the storage location)
docker run -d -v pgdata:/var/lib/postgresql/data postgres

# Bind mount (map a host directory into the container)
docker run -d -v /home/user/config:/etc/nginx/conf.d:ro nginx

The :ro suffix makes the mount read-only inside the container.

Environment variables (-e)

Most official images are configured through environment variables, following the 12-factor app principle.

docker run -d \
-e POSTGRES_USER=myuser \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=mydb \
-p 5432:5432 \
--name pg \
postgres:16

Naming containers (--name)

Without --name, Docker generates a random name like cranky_einstein. Always name containers you will reference later — it is much easier to type docker stop my-nginx than docker stop 3f9b8c21a0e2.


Interactive Mode (-it)

Some tasks require an interactive shell inside a container — exploring the filesystem, running ad-hoc commands, debugging.

# Start an Alpine Linux shell
docker run -it alpine sh

# Start a bash shell in an Ubuntu container
docker run -it ubuntu bash

The flags:

  • -i (--interactive) — Keep stdin open so you can type commands
  • -t (--tty) — Allocate a pseudo-TTY (terminal), giving you a proper shell prompt

Once inside, you are in an isolated environment. Try:

# Inside the container
cat /etc/os-release
ls /
exit

When you type exit, the process exits and the container stops.

Exec into a running container

To open a shell in an already running container (without stopping it):

docker exec -it my-nginx bash
# or for minimal images that only have sh:
docker exec -it my-nginx sh

docker exec runs an additional process inside the container's namespace. The container keeps running when you exit this shell.


Listing Containers

# List running containers
docker ps

# List all containers (including stopped ones)
docker ps -a

# Show only container IDs (useful in scripts)
docker ps -q

# Combine: IDs of all stopped containers
docker ps -aq -f status=exited

Sample output of docker ps:

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3f9b8c21a0e2 nginx:latest "/docker-entrypoint.…" 2 minutes ago Up 2 minutes 0.0.0.0:8080->80/tcp my-nginx
a1b2c3d4e5f6 postgres:16 "docker-entrypoint.s…" 5 minutes ago Up 5 minutes 127.0.0.1:5432->5432 pg

Stopping and Removing Containers

# Gracefully stop a container (sends SIGTERM, waits, then SIGKILL)
docker stop my-nginx

# Stop immediately (SIGKILL)
docker kill my-nginx

# Remove a stopped container
docker rm my-nginx

# Stop and remove in one step
docker rm -f my-nginx

# Remove all stopped containers
docker container prune
tip

A stopped container still exists (you can docker start it again) and still occupies disk space in the writable layer. If you will never need the container again, remove it.


Managing Images

# List images on your machine
docker images
# Equivalent:
docker image ls

# Filter by repository name
docker images nginx

# Show image IDs only
docker images -q

# Remove an image
docker rmi nginx:1.27-alpine

# Remove all unused images (not referenced by any container)
docker image prune

# Remove ALL images, including those used by stopped containers
docker image prune -a

Removing an image only removes the local copy. The image still exists in the registry and can be pulled again any time.

Inspecting an image

# See layers, environment variables, exposed ports, default command
docker inspect nginx

# Just the configuration section
docker inspect --format '{{json .Config}}' nginx | jq

Viewing Container Logs

# Print all logs
docker logs my-nginx

# Stream logs live (like tail -f)
docker logs -f my-nginx

# Last 50 lines
docker logs --tail 50 my-nginx

# With timestamps
docker logs -t my-nginx

A Practical Workflow Example

Here is the full lifecycle for running a local Postgres database for development:

# 1. Start Postgres with named volume for persistence
docker run -d \
--name dev-postgres \
-e POSTGRES_USER=dev \
-e POSTGRES_PASSWORD=devpassword \
-e POSTGRES_DB=myapp \
-p 127.0.0.1:5432:5432 \
-v dev-pgdata:/var/lib/postgresql/data \
postgres:16

# 2. Check it's running
docker ps

# 3. Connect to it via psql inside the container
docker exec -it dev-postgres psql -U dev -d myapp

# 4. View logs if something looks wrong
docker logs dev-postgres

# 5. Stop without losing data (volume persists)
docker stop dev-postgres

# 6. Start it again later
docker start dev-postgres

# 7. When you're done with the project entirely
docker rm -f dev-postgres
docker volume rm dev-pgdata

Cleaning Up

Over time, stopped containers, unused images, and dangling build cache accumulate. The system prune command clears all of it:

# Remove all stopped containers, unused networks, dangling images, build cache
docker system prune

# Also remove unused volumes (careful — this deletes data!)
docker system prune --volumes

# Check how much space Docker is using
docker system df

Summary

CommandWhat it does
docker pull <image>Download an image
docker run -d -p host:ctr --name n <image>Start a named container in the background
docker run -it <image> shStart an interactive shell
docker ps / docker ps -aList running / all containers
docker stop <name>Gracefully stop a container
docker rm <name>Remove a stopped container
docker imagesList local images
docker rmi <image>Remove a local image
docker logs -f <name>Stream container logs
docker exec -it <name> shOpen a shell in a running container
docker system pruneClean up unused resources

You now have the vocabulary and the commands to work with containers day-to-day. The next step is building your own images with a Dockerfile.