N. P. O'Donnell, 2020
- Containers are stored in
/var/lib/docker/containers
- Easiest to use
su
first if snooping around/var/lib/docker
- Each container has a
config.v2.json
config file
docker build [-t <tag>] <Dockerfile location>
Example:
docker build -t my-webapp:dev .
- Starts with
FROM <baseimage:tag>
eg.FROM fedora:21
FROM scratch
means start with no base image- Comments begin with
#
WORKDIR
crates andcd
's into a dir. Subsequent commands are performed in this dir. Owned by root.RUN
runs command during build. eg.RUN apt-get update
- To add files/dirs use
COPY
rather thanADD
unless you really needADD
CMD
means execute a command. eg.CMD ["/bin/bash"]
Once the image is build, a container can be created based on the image. Think of an image as a program or blueprint and container as process or instance. Containers have state, images do not.
docker create my-webapp
docker create my-webapp:dev
docker create -it ubuntu # -i = interactive, -t = allocate a pseudo TTY
docker start thirsty_pare
docker start 701d51
docker images
docker ps -a
docker attach <name or ID>
docker top <name or ID>
docker stats <name or ID>
docker exec -it <name or ID> /bin/sh
docker inspect -f '{{ .Mounts }}' <name or ID>
docker rmi <name:[version]>
docker rm <name or ID>
docker system prune -a
There are two ways to speficy your CMD
in a Dockerfile:
- Shell -- eg.
CMD node index.js
- Exec -- eg.
CMD ["node", "index.js"]
Using the shell form will cause the image's /bin/sh
to be invoked, with the CMD
as arguments. This means crucually that PID 1 will be the shell itself, and it will not forward signals.
The exec form consists of a JSON string with the command and arguments, recommended because it means one less process is running and more importantly, signals are forwarded.
Containers can be given an init process (PID 1) if the --init
option is passed to either docker create
or docker run
. This causes the init program to run as PID 1. The program specified in the CMD
directive, or custom command, runs as a child of the init process.
The init process run by docker to achieve this is tini. (tini github)
Tini mainly solves 2 problems:
-
Reaping zombies in the container - without
tini
, zombies from badly-written apps will remain in the process table, causing a resource leak.tini
regularly clears zombies from the process table. -
Signal handling - without
tini
, if a signal such asSIGINT
is sent to the app, the app will ignore the signal unless it has signal handling code. This is due to the app having the special "1" process ID which is treated differently by the kernel. Withtini
, the app gets a PID > 1 andtini
forwards signals it receives to the app, and the app behaves nomally when it receives a signal - for example it will exit if it receivesSIGINT
. This means Ctrl-C will "just work".
Tini can be added by either:
- Adding
--init
todocker build
ordocker run
- Set
ENTRYPOINT
to["/sbin/tini", "--"]
in dockerfile
More discussion on tini here
To expose a port listenting inside the container to the outside world, add -p <expose port>:<container port>
to docker create
or docker run
. For example to expose port 3000 in the container on port 80 of the host:
docker ... -p 80:3000
Stages allow multiple bases to be used in the same dockerfile. One problem this solves is reducing the size of the image by removing build/test tools that are no longer needed once the image is built. Multi-stage builds allow the app to be built against a base with all the dev tooling (for example the golang compiler for a go project), then once built, the image is re-based with a bare-bones OS (such as alpine) and the artifacts from the previous build are copied. This also improves security by reducing attack surface.
Multi-stage Dockerfiles can also be used to make development and test builds which are based off of the production build.
Good discussion on multi-stage builds here.
The AS
keyword can be used to name a stage in a multi-stage dockerfile, then the stage can be referenced as the base for another stage. This is useful for creating development or test builds which are based on the production build.
Example:
FROM alpine AS prod
CMD ["ls", "/"]
FROM prod AS dev
CMD ["ls", "-la", "/"]
In this example there are two stages: prod
and dev
. The commands to build the prod
and dev
stages respectively are:
docker build -t stages:prod --target prod .
and
docker build -t stages:dev --target dev .
To run them:
$ docker run -it stages:prod
bin etc lib mnt proc run srv tmp var
dev home media opt root sbin sys usr
and
$ docker run -it stages:dev
total 64
drwxr-xr-x 1 root root 4096 Jul 20 21:48 .
drwxr-xr-x 1 root root 4096 Jul 20 21:48 ..
-rwxr-xr-x 1 root root 0 Jul 20 21:48 .dockerenv
drwxr-xr-x 2 root root 4096 May 29 14:20 bin
...
If the project uses a compiled language such as C++, Rust, or Go, there is usually no need to have the compiler and build tools present at run time. A common pattern is to build the binaries in a build stage, replace the entire build OS with a run OS, then copy only the binaries and artifacts from the previous (build) stage, leaving behind the compiler and build tools.
The binaries, including any dynamic-linked libraries, can be copied using a COPY
directive with a --from=<stage>
parameter such as:
COPY --from=0 /go/src/github.com/alexellis/href-counter/app .
See here for more details on the COPY
directive.
Docker swarm can be used to control multiple docker hosts from one "manager" host.
First a manager host must be initalized. This will cause the docker host the command is run on to become a manager of that swarm:
docker swarm init
This command will print a docker swarm join
command incuding a token which can be run on worker nodes to join the swarm. The join command has the format:
docker swarm join --token <join token>
This command may be seen at any time on the manager host by running:
docker swarm join-token worker
Once the join command has been run on a worker node, the following command (run on the manager) will print the status of all nodes on the swarm:
docker node ls