Where to start First steps track · 15 stages
Linux Labs

Docker — containers: running, ports, volumes, networks

23 min Docker 29.1 · Ubuntu 24.04

Docker runs programs in isolated containers started from images and manages their ports, volumes and networks.

Cheat sheet

12 commands Printable version
  • sudo systemctl enable --now docker enable the service
  • sudo docker run -d --name web -p 8080:80 nginx:alpine background
  • sudo docker ps -a all containers and status
  • sudo docker logs --tail 20 web main process output
  • sudo docker exec -it web sh a shell inside
  • sudo docker stop web; sudo docker start web
  • sudo docker rm -f web remove, even if running
  • sudo docker port web published ports
  • sudo docker inspect -f '{{.State.Status}}' web one field
  • sudo docker volume ls volumes
  • sudo docker network ls networks
  • sudo docker stats --no-stream container CPU and memory

Overview

Docker runs programs in containers. A container is an ordinary Linux process that the kernel has given its own namespaces and cgroup (cgroups-namespaces): its own process tree, network and file system. The container takes its files and start command from an image.

How it works: the docker service (the dockerd daemon) creates and keeps containers, and the docker command sends it requests through the /var/run/docker.sock socket. Root and the docker group can write to that socket.

On the lab machine student is not in the docker group, so every command goes through sudo. Without it:

text
student@ubuntu:~$ docker ps
permission denied while trying to connect to the docker API at
unix:///var/run/docker.sock

The service is disabled in the machine image: lesson or lab preparation turns it on, by hand it is sudo systemctl enable --now docker.

Image and container

The image is the template, the container is a named instance started from it. One image can back many containers, each with its own writable layer. docker images lists images, docker ps running containers, and docker ps -a all of them, stopped ones included. The full ps -a table is wide (ID, image, command, created, status, ports, name); --format picks the columns you need:

text
student@ubuntu:~$ sudo docker ps -a --format '{{.Names}} {{.Status}}'
r4 Exited (3) 6 seconds ago
r3 Up 8 seconds
r2 Up 9 seconds

docker run is docker create plus docker start. -d leaves the container in the background, --name names it (at least two characters: for a Docker answers Invalid container name), --rm removes the container when it exits. Without --name Docker makes up a name.

Main process and status

A container lives as long as its main process — the one started by the image command or by docker run. Inside the container it has PID 1. When it exits, the container stops and the status shows its exit code.

STATUS in docker ps -a What is going on
Up 9 seconds the main process is running
Up 6 seconds (healthy) running, the healthcheck passes
Exited (3) 6 seconds ago the process exited with code 3
Exited (137) the process was killed by SIGKILL (137 = 128 + 9)
Restarting (1) 1 second ago exited with 1, Docker starts it again
Created created but never started, or the start failed

docker logs tells why the process exited — everything the main process wrote to stdout and stderr, including right before the crash:

text
student@ubuntu:~$ sudo docker logs --tail 2 web
httpd: can't change directory to '/www': No such file or directory
httpd: can't change directory to '/www': No such file or directory

docker logs -f follows live, -t adds timestamps, --since 10m shows the last 10 minutes. Logs are kept as long as the container exists.

Ports

A container listens on a port at its own address in Docker's internal network. The host addresses, localhost included, do not have that port until you publish it. Port publishing -p 8080:80 forwards host port 8080 to container port 80 — host first, container second:

text
student@ubuntu:~$ sudo docker run -d --name app -p 8081:80 app:1.1
student@ubuntu:~$ sudo docker port app
80/tcp -> 0.0.0.0:8081
80/tcp -> [::]:8081

0.0.0.0 means the port is open on every host address. For this machine only, use -p 127.0.0.1:8081:80. One host port can belong to one container or service: a second -p 8081:… gets port is already allocated.

Environment variables

-e NAME=value passes a variable to the main process, --env-file app.env passes every NAME=value line of a file. -e in the same command wins over the file:

text
student@ubuntu:~$ cat app.env
MODE=prod
LEVEL=2
student@ubuntu:~$ sudo docker run --rm --env-file app.env -e LEVEL=3 \
    busybox sh -c 'echo $MODE $LEVEL'
prod 3

Environment, ports and volumes are set when the container is created. A running container cannot change them: remove it and create it again with the new options. To see what it was created with: docker inspect -f '{{json .Config.Env}}' web.

Restart policy

A restart policy tells Docker whether to start the container again when its main process exits or when the docker service itself restarts. Set it with --restart at creation; for an existing container use docker update --restart unless-stopped web.

--restart The main process exits by itself
no (default) the container stays Exited
on-failure starts it again if the code is not 0; on-failure:3 — at most 3 times
always starts it again whatever the code
unless-stopped starts it again whatever the code

always and unless-stopped differ on a manual stop. After docker stop the container stays down under any policy. A restart of the docker service brings back always containers, and unless-stopped ones only if nobody stopped them by hand. A no container that was running during the restart stays Exited (137). This is how it looks on the machine:

text
student@ubuntu:~$ sudo docker stop r1 r2
student@ubuntu:~$ sudo systemctl restart docker
student@ubuntu:~$ sudo docker ps -a --format '{{.Names}} {{.Status}}'
r4 Exited (3) 31 seconds ago       # on-failure:3, out of retries
r3 Exited (137) 4 seconds ago      # no: the service stopped it
r2 Up 3 seconds                    # always
r1 Exited (137) 15 seconds ago     # unless-stopped, stopped by hand

With the docker service enabled (systemctl enable docker), containers come back after a reboot by the same rules. docker stop holds an always or unless-stopped container until the service restarts; docker rm is forever.

A crashing container with always ends up in a restart loop: status Restarting, RestartCount grows, and the pause between attempts roughly doubles each time. Look for the cause in docker logs.

Volumes and bind mounts

Whatever a container writes to its files lives in its layer and goes away with docker rm. Data that must survive re-creating the container is kept outside:

  • a volume — a directory managed by Docker: -v data:/var/lib/app. If the volume does not exist, docker run creates it;
  • a bind mount — a host directory: -v /srv/web:/www. A left side starting with / means a host directory.
text
student@ubuntu:~$ sudo docker volume create data1
student@ubuntu:~$ sudo docker volume inspect -f '{{.Mountpoint}}' data1
/var/lib/docker/volumes/data1/_data
student@ubuntu:~$ sudo docker run --rm -v data1:/data busybox \
    sh -c 'echo hi > /data/f'
student@ubuntu:~$ sudo docker run --rm -v data1:/data busybox \
    cat /data/f
hi
What Volume Bind mount
Where the files are /var/lib/docker/volumes/<name>/_data wherever you say
Empty on first mount gets the image's files from that path hides them, the container sees nothing
Owner and mode as in the image as on the host, numeric
Removal docker volume rm by hand, like any directory

Read-only is :ro at the end: -v /srv/web:/www:ro; a write inside ends with Read-only file system. In a bind mount the container sees host owners as numbers: a student file is 1000 1000. A process in the container running as another user hits permissions exactly as on the host.

docker volume rm will not remove a volume while any container uses it, even a stopped one.

An anonymous volume appears when a path has no volume name: -v /data or a VOLUME /data instruction in the image's dockerfile. Every docker run without -v creates a new empty volume with a hash for a name, and the previous container's data is not in it:

text
student@ubuntu:~$ sudo docker rm -f va
student@ubuntu:~$ sudo docker run -d --name vb volimg:1
student@ubuntu:~$ sudo docker volume ls
DRIVER    VOLUME NAME
local     77a8347bd7fbf9a1424251442a1d212edaab5cb8e2580b5a00b9e…
local     422da370c4cbf32ad7280434ea9bbad20f13bc25ba519945002353…

The first volume belongs to the removed va: docker rm left it, and the data sits in /var/lib/docker/volumes/77a8…/_data. Mount it again by that name: -v 77a8347b…:/data. docker rm -v and docker run --rm remove anonymous volumes together with the container.

Networks

Every container has its own network interface — one end of a veth pair, the other end plugged into a bridge on the host. Docker creates three networks:

text
student@ubuntu:~$ sudo docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
e92e175fe67b   bridge    bridge    local
ea5b0c89cc6d   host      host      local
1a37c7f6819b   none      null      local

Without --network a container lands in bridge (the docker0 bridge, 172.17.0.0/16). There containers see each other by address only — no names:

text
student@ubuntu:~$ sudo docker exec web1 ping -c1 -W1 web2
ping: bad address 'web2'

On the default network the container's /etc/resolv.conf lists the host's DNS servers. The lab machine has only the local 127.0.0.53 there, which a container cannot reach, so Docker puts in 8.8.8.8 and 8.8.4.4.

On a user-defined network names work: Docker DNS at 127.0.0.11 answers with the container name.

text
student@ubuntu:~$ sudo docker network create appnet
student@ubuntu:~$ sudo docker run -d --name app1 --network appnet \
    busybox sleep 600
student@ubuntu:~$ sudo docker run -d --name db1 --network appnet \
    busybox sleep 600
student@ubuntu:~$ sudo docker exec app1 ping -c1 -W1 db1 | head -1
PING db1 (172.18.0.3): 56 data bytes

docker network connect appnet web1 adds a running container to a second network. Only the container the host talks to needs a published port (-p); the rest talk inside the network. docker-compose creates such a network for every project by itself.

Memory and CPU limits

Without limits a container may take all the memory and all the CPUs of the machine. docker stats then shows the whole machine's memory in the LIMIT column — 458.2MiB on the lab machine. The kernel enforces limits through the container's cgroup (cgroups-namespaces):

text
student@ubuntu:~$ sudo docker run -d --name m1 --memory 64m --cpus 0.5 \
    busybox sleep 600
student@ubuntu:~$ sudo docker stats --no-stream --format \
    'table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}' m1
NAME      MEM USAGE / LIMIT   MEM %
m1        572KiB / 64MiB      0.87%
Option What it limits
--memory 64m memory of the container's processes
--memory-swap memory and swap together; without it, twice --memory
--cpus 0.5 half of one CPU

inspect keeps the limits in bytes and billionths of a CPU:

bash
sudo docker inspect -f '{{.HostConfig.Memory}}' m1      # 67108864
sudo docker inspect -f '{{.HostConfig.MemorySwap}}' m1  # 134217728
sudo docker inspect -f '{{.HostConfig.NanoCpus}}' m1    # 500000000

Swap is off on the lab machine, so --memory is all the container gets. A process that needs more is killed by the OOM killer: the container exits with code 137, and inspect shows OOMKilled: true.

text
student@ubuntu:~$ sudo docker run --name oom1 --memory 16m busybox \
    sh -c 'x=$(head -c 67108864 /dev/zero | tr "\0" a)'
student@ubuntu:~$ echo $?
137
student@ubuntu:~$ sudo docker inspect -f '{{.State.OOMKilled}}' oom1
true

Code 137 also follows a docker stop the process did not obey within 10 seconds — only OOMKilled tells them apart. Limits are an exception to "set at creation": docker update --memory 32m --memory-swap 32m m1 changes them on a running container.

Process user

By default the main process runs as the container's root, UID 0. The container and the host share one kernel, and UIDs too: in host ps such a process is root. USER in the dockerfile or --user at run time sets the user:

--user id inside the container
1000 uid=1000 gid=0(root) groups=0(root)
1000:1000 uid=1000 gid=1000 groups=1000
nobody uid=65534(nobody) gid=65534(nobody) …
web error: unable to find user web: no matching entries in passwd file

Docker looks names up in the image's /etc/passwd; any number works. --user 1000 without a group leaves group 0 — root; give the group explicitly: --user 1000:1000.

UID 1000 in the container is student on the host: in host ps a sleep 600 started with --user 1000 belongs to student. Files the process creates in a bind mount get its UID:

text
student@ubuntu:~$ ls -ln ~/u
total 0
-rw-r--r-- 1 1000 1000 0 Oct 20 15:03 by1000
-rw-r--r-- 1    0    0 0 Oct 20 15:03 byroot

byroot was created by a root container — student on the host cannot change it. A non-root process also hits permissions inside the image: touch /etc/x gives Permission denied.

docker inspect

docker inspect prints everything about a container, image, volume or network as JSON. -f with a Go template extracts one field: the dot is the JSON root, then a path of keys.

bash
sudo docker inspect -f '{{.State.Status}} {{.State.ExitCode}}' web
sudo docker inspect -f '{{.RestartCount}}' web
sudo docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' web
sudo docker inspect -f '{{json .Mounts}}' web
sudo docker inspect -f '{{json .Config.Env}}' web
sudo docker inspect -f '{{.State.Pid}}' web     # PID on the host
sudo docker inspect web | jq '.[0].State'
Field What it holds
.State.Status running, exited, restarting, created
.State.ExitCode exit code of the main process
.State.OOMKilled true — killed for going over the memory limit
.State.Health.Status healthy, unhealthy, starting
.RestartCount how many times Docker restarted the container
.HostConfig.RestartPolicy.Name the restart policy
.Mounts volumes and bind mounts: what, where, read-only or not
.Config.Env environment variables
.NetworkSettings.Networks networks and addresses

range walks a map or a list. The container's address on every network:

bash
sudo docker inspect -f \
  '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' web1

For the whole JSON, jq is handier.

Configuration

The service is configured in /etc/docker/daemon.json — JSON, no comments:

json
{
  "log-driver": "json-file",
  "log-opts": {"max-size": "10m", "max-file": "3"},
  "data-root": "/srv/docker"
}

Apply with sudo systemctl restart docker. A JSON error keeps the service from starting: check journalctl -u docker. What took effect:

bash
sudo docker info -f '{{.DockerRootDir}} {{.LoggingDriver}}'

On the lab machine that is /var/lib/docker json-file. A service restart stops every container. The restart policy decides which ones come back (see the section above). Change data-root only to an empty directory: the service does not move old images and volumes by itself.

Recipes

4

Why the container stopped

bash
sudo docker ps -a --filter name=web
sudo docker logs --tail 30 web
sudo docker inspect -f '{{.State.ExitCode}} {{.State.OOMKilled}}' web

Code 0 means the command simply finished: for a service it means it went to the background. 137 means SIGKILL: out of memory (OOMKilled true) or a docker stop the process did not answer. 1, 2 and others are errors of the program itself, with the text in the logs.

Re-create a container with new options

bash
sudo docker inspect -f '{{json .Config.Env}} {{json .Mounts}}' web
sudo docker rm -f web
sudo docker run -d --name web --restart unless-stopped \
  -p 8080:80 -v data:/data -e MODE=prod app:1.1

Data in the volume survives re-creation; data in the container layer does not.

Find a container among host processes

text
student@ubuntu:~$ sudo docker inspect -f '{{.State.Pid}}' web1
2055
student@ubuntu:~$ ps -o pid,user,cmd -p 2055
    PID USER     CMD
   2055 root     sleep 600

A command in a container from a script

docker exec -it needs a terminal. In a script or a pipeline -t fails with the input device is not a TTY — drop it: docker exec web cat /etc/hosts.

Troubleshooting

11

permission denied … docker.sock

The user is not in the docker group. Run with sudo. Group membership gives root on the machine — see the Danger zone.

Cannot connect to the Docker daemon

text
Cannot connect to the Docker daemon at unix:///var/run/docker.sock.
Is the docker daemon running?

The service is not running: sudo systemctl enable --now docker. If you stopped only docker.service, systemd warns its triggering units are still active: docker.socket — the next docker command starts the service again through the socket.

port is already allocated

Bind for 0.0.0.0:8081 failed: port is already allocated — another container or service holds the host port: sudo ss -tlnp | grep :8081 (ss). The container has already been created — remove it with docker rm and start again with another port on the left.

The container name … is already in use

A container with that name exists, maybe a stopped one (docker ps -a). Remove the old one with docker rm or pick another name.

The container exits at once with code 0

The main process finished without an error. A service that went to the background does this: httpd without -f, nginx without daemon off. The main process has to run in the foreground.

docker stop takes 10 seconds and gives 137

docker stop sends SIGTERM and waits 10 seconds. A PID 1 process without its own SIGTERM handler never gets it — sleep and busybox httpd behave this way — so Docker finishes it with SIGKILL: Exited (137). For such containers use docker stop -t 1 or docker rm -f.

container … is not running

docker exec works only in a running container. Start a stopped one with docker start; if it dies at once, read docker logs.

docker logs | grep finds nothing

docker logs returns the container output on two streams: what the process wrote to stderr goes to stderr. Program errors usually live there, and a pipe does not see them. Merge the streams: sudo docker logs web 2>&1 | grep error.

Data gone after re-creating

The data was in the container layer. Mount a volume: -v data:/path. An old volume is still there if the container had one: docker volume ls.

Containers cannot see each other by name

Both are on the default bridge network: ping: bad address. Create your own network and connect both (docker network connect).

volume is in use / has active endpoints

A container needs the volume or network, maybe a stopped one. List them with docker ps -a --filter volume=data1 or --filter network=appnet; remove the containers, then the volume or network.

Danger zone

3

The docker group is root

Whoever can write to docker.sock can start a container with -v /:/host and get the whole host disk as root. Adding a user to the docker group is the same as giving them passwordless sudo.

docker rm -f and data in the container layer

rm -f stops and removes a container at once, together with its layer. Anything the container wrote outside a volume or bind mount is gone.

docker volume prune -a and docker system prune

docker volume prune removes anonymous volumes no container needs; with -a, named ones too. A volume whose container you are re-creating right now is "needed by nobody" at that moment — its data is deleted for good. Check docker volume ls before cleaning up.

Terms

8
container
A process started from an image in its own namespaces, with its own name, file system and status in docker ps -a. It lives as long as its main process does. Also appears in: Container images, docker compose, Dockerfile, Load average: how many tasks wait for the CPU and the disk and 9 more
container main process
The process started by the image command (CMD, ENTRYPOINT) or by docker run; it has PID 1 inside the container. When it exits, the container goes to Exited with its exit code.
port publishing
Forwarding a host port into a container: -p 8080:80 sends requests for host port 8080 to container port 80. Without it the service is reachable only from the Docker network.
restart policy
The rule Docker uses to start a stopped container by itself: no, on-failure, always or unless-stopped. Set with --restart at creation or with docker update. Also appears in: docker compose
volume
A directory managed by Docker (/var/lib/docker/volumes/<name>/_data) mounted into a container. Removing the container leaves it alone: the data lives until the volume itself is removed. Also appears in: docker compose, Dockerfile, LVM logical volumes, Container images and 7 more
bind mount
A host directory or file mounted into a container at a path: -v /srv/web:/www. The container sees the same files with the same numeric owners and modes. Also appears in: docker compose, lsblk, Mounting and fstab, resize2fs / xfs_growfs
user-defined network
A network made with docker network create or by compose. Containers on it find each other by name through Docker DNS (127.0.0.11); the default bridge network has no names. Also appears in: docker compose
Docker DNS
Docker's DNS server at 127.0.0.11 on user-defined networks: it answers with the names of containers and compose services on that network. The default bridge network lacks it and uses the host's DNS servers.

See also

Loading…