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:
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:
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:
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:
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:
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:
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.
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:
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:
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:
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.
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):
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:
sudo docker inspect -f '{{.HostConfig.Memory}}' m1
sudo docker inspect -f '{{.HostConfig.MemorySwap}}' m1
sudo docker inspect -f '{{.HostConfig.NanoCpus}}' m1
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.
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:
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.
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
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:
sudo docker inspect -f \
'{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' web1
For the whole JSON, jq is handier.