Docker
Basic Commands
docker pull <image_name> will pull an image (from the Docker hub by
default). It will pull the latest version of the image unless a specific
version is specified using the syntax docker pull <name>:<tag> or
docker pull <name>@<digest>. A tag is a mutable identifier that can
point to a broad version of an image, but a digest is an immutable
identifier that points to a specific version of an image. For example,
getting the latest Ubuntu 22.04 image can be done with docker pull
ubuntu:22.04 but getting a specific version of Ubuntu 22.04 can be done
with docker pull ubuntu@sha256:<hexadecimal_hash>
docker run <image_name> will run a container from the given image.
Pass the --name <container_name> flag to name the container something
different than its default.
docker ps will list of containers including their IDs. By default, it
shows all running containers, but the -a flag can be added to show
stopped containers as well.
docker stop <container_name|container_ID> will stop a given container.
docker start <container_name|container_ID> will start a given container.
docker start <container_name|container_ID> will restart a given container.
docker rm <container_name|container_ID> will delete a given container.
docker rename <container_name|container_ID> will rename a given container.
docker images lists all images downloaded to the local system.
docker rmi <image_name|image_ID> will delete the given image from the local system storage. This is helpful for freeing up space that obsolete images are using.
docker exec [options] <container> <command> [arguments] will run
a given command in a running container. The -d flag is used to run the
command in the background (detach). The -e flag is used to set
environment variables. The -i flag is used to keep STDIN open (i.e.
“interactive”). The -t flag allocates a pseudo-TTY (useful for running
a shell). The -u flag can be used to run the command as a specific
user (e.g. -u 1000:1000 or -u root:root). The -w flag is used to
change the working directory inside the container. A very common command
to see is docker exec -it <container_name> bash to get a shell in the
running container.
Networking Containers
By default, containers are hooked up to a bridge interface which allows them to communicate with each other and the host without needing open ports on the host system.
docker network list can be used to list all the currently configured
networks. By default there are bridge, host, and none. A bridge
network is the default network which allows containers to communicate
with each other and the outside world. A host network will remove the
network isolation between the container and the host, so it will use the
host’s IP and ports. None means the container will not be connected to
the network.
Other network drivers include overlay which is used to connect
multiple separate docker daemons (useful for docker swarms), ipvlan
which gives complete control over IPv4/6 addressing including allowing
layer 2 VLAN tagging, and macvlan allows one to assign a MAC address
to a container’s virtual network interface which makes it appear to be
a physical network interface for software that may require such
a configuration.
docker network create --driver <driver_name> <network_name> can be
used to create a separate bridge network.
docker network connect <network_name> <container_name|container_ID> is
used to connect a given container to a given network. Similarly, the
docker network disconnect command will disconnect a container from
a network.
docker network rm <network_name> will remove a given network assuming
no container is connected to it. Similarly, docker network prune will
remove all unused networks.
docker network inspect <network_name> can be used to inspect the
configuration of a given network, and is useful for seeing what IP
ranges it has or whether or not IPv6 is enabled.
Making Custom Containers
Docker images are built from Dockerfiles using the docker build
command. They contain step-by-step instructions on how to build an image
starting from the base and going up. You can even use separately built
images as bases on which you can build your own images. For example, you
can start with the golang docker image and build a docker container
with your Go program on top, without having to start all the way at the
very bottom with a distribution.
Dockerfiles have a pretty simple syntax. They’re essentially just linear
sequences of lines formatted as: INSTRUCTION arguments and lines which
start with a # are comments. Single statements can be broken up across
multiple lines with a \.
A Dockerfile must begin with a FROM instruction, which describes the
base image for subsequent instructions.
The RUN instruction is used to execute commands on top of the current
image, committing the results. It can either be in the form RUN
<command> (shell form) which will run the given command in the default
shell (/bin/sh), or RUN ["executable", "param1", ... , "paramN"]
(exec form) which will run the executable with the given parameters, and
is how you can run a command using a different shell or program.
The CMD instruction specifies what the container will do when it is
ran. There can be only one of these in a Dockerfile. If there are more,
then only the final one will take effect. Similarly to RUN there are
different forms that CMD can take:
CMD ["executable","param1", ... , "paramN"]CMD ["param1", ... , "paramN"]CMD command param1 ... paramN
The first works the same as in the equivalent form of the RUN
instruction and is the preferred form of arguments to CMD. The second
will pass the parameters to the default ENTRYPOINT instruction.
Finally, the third form executes the command in the default shell,
/bin/sh. If not in shell form, no shell variable expansion or
processing will take place.
The LABEL instruction is a way to add metadata to an image and is made
up of one or more key-value pairs as arguments.
The EXPOSE instruction informs Docker that the container listens on
the given port at runtime. TCP or UDP can be specified, with TCP being
the default.
The ENV instruction sets environment variables in the form of
key-value pairs in a similar manner to how shell environment variables
look and behave. These persist for the runtime of the container, but if
they’re only needed during the build step then they can just be
specified for single instructions or by using the ARG instruction like so:
RUN HOME=/var/www cd $HOME && pwd
---
ARG HOME=/var/www
RUN cd $HOME && pwd
The ADD instruction copies new files or directories from the host into
the image in a similar manner to how the cp POSIX program works. You
can additionally modify permissions for the resulting files in the image
by using the --chown=<user>:<group> and --chmod=<perms> flags.
The COPY instruction is essentially the same as ADD, except without
handling tar files and remote URLs. It is recommended to use COPY
where the functionality of ADD is not required.
The ENTRYPOINT instruction allows you to configure a container that
will run as an executable and can be specified in both exec and shell
forms (where exec is preferred, because then the resulting executable is
able to receive signals sent by the docker daemon).
The VOLUME instruction creates a mount point in the image with the
names given as arguments and marks it as holding externally mounted
volumes. This is a way to pass data between containers and between the
host and a container.
The USER instruction sets the username (or ID) and optionally the
group to use as the default user (and group) for the following
instructions, including as the user for RUN, CMD, and ENTRYPOINT
instructions.
The WORKDIR instruction sets the working directory for any RUN,
CMD, ENTRYPOINT, COPY, and ADD instructions that follow. If the
dir doesn’t exist, it will be created. By default, arguments to this
instruction are treated as relative paths.
The ARG instruction defines a variable (with optional default value)
that users can pass at build time.
The SHELL instruction allows the default shell for the shell form of
commands to be overridden, and arguments are given in exec form.
There are quite a few more instructions and directives and such that I haven’t needed to understand yet, but a full description can be found in the Dockerfile documentation.
Docker Compose
Docker compose is how you can very easily stand up applications that
span multiple containers, without having to docker run each of them
individually. It’s also an easy way to specify a bunch of flags with
default parameters that a container should have, and is very helpful for
containers that need to expose a bunch of ports and/or have a bunch of
mounted volumes.
The default name for a docker compose file is compose.yaml, but
compose.yml, docker-compose.yaml and docker-compose.yml are also
accepted.
Check the docker compose documentation for the syntax (I have not yet needed to make a compose file).
Limiting Resource Usage
It’s very useful to be able to limit the resources such as CPU and memory that a container can take up while its running. Limits can be specified in the form of command line flags or via a docker compose file.
The -m flag can be used to limit the memory usage of a container. The
value must be a positive integer suffixed by b, k, m, or g, to
indicate magnitude. The minimum value is 6m.
The --cpus=N flag can be used to configure the amount of CPU
a container can use at a given time. For example, if your system has one
CPU and you want to limit a container to use no more than half that CPU
at any time, then you would pass the flag --cpus="0.5"
Here is the docker resource constraints documentation for a more complete reference.
Best Practices
Opt to use Docker Official Images and images published by Verified Publishers over random images found on the internet. Download and inspect the Dockerfile to make sure an image won’t be doing anything fishy before you deploy it.
When building an image, start with as small a base as possible to limit attack surface (e.g. Alpine Linux).
Consider having one, ideally statically linked executable binary per container to limit as much as possible what a given container is responsible for/can do.
Consider disabling all shells so that its extremely difficult if not impossible to get a shell in a container, should the running application become compromised.
Run docker containers under a different user than root with docker run
--user <user> <image>.
Use the Docker Bench for Security to scan a host running Docker for best practices when running in production.
Run Docker in rootless mode if your use case supports that.
Note that if you’re using a firewall like ufw, the -p or --publish
flag used to map ports on the host to the container will edit your
firewall rules directly which can have unintended consequences like your
firewall rules not actually blocking access to a certain port. A good
solution to this is to only publish to localhost on the host such as
--publish 127.0.0.1:8080:8080 so you can then use a reverse proxy in
front of the container without worrying whether or not people can access
port 8080 directly.