paritybit.ca

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:

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.