Docker 102

🗓️
•
🔄
•
⏳ 6 min

We’ll go over some every-day commands and files you’ll use as part of your development workflow with docker.

Let’s start by tying together the concepts from the previous post, with the ones we are about to see:

One builds and image (which might share a volume with the host machine) based on the definition found in a Dockerfile, runs it in a container and optionally composes multiple images together.

To make sense of this, let’s take a closer look.

Dockerfile

A file that defines a docker Image, a blueprint of sorts. It will look something like this:

dockerfile
FROM alpine
RUN apk update
RUN apk add nginx
RUN echo Image created!

It contains a series of commands of the format INSTRUCTION arguments.

Keep in mind that every line is a new layer in the Image. So the order does matter.

Common Instructions

FROM

Sets the Base Image for subsequent instructions. In its most basic form, you’ll see here what OS the Image is based on (Alpine Linux in our example).

A valid Dockerfile must start with a FROM instruction. Most commonly, this will be done by pulling an already existing image from the public repos.

RUN

Executes a command within the Container.

Create a directory? RUN mkdir. Update your system? RUN apk update. Install a dependency? RUN apk add dependency.

Plain and simple.

CMD

Default command to execute when running an image, ‘what the image does’.

dockerfile
CMD ["echo", "This will be printed to the host system!"]

Only one is allowed per Dockerfile and whatever command we append to the docker run command will override this instruction. We’ll take a closer look at the run command further down.

ENV

Sets environment variables, quite like you would in your .bashrc or .zshrc.

Useful if you need information to be set at build time, for later modification or reference at run time.

COPY

Copy files or directories from the host to the Container.

It works pretty much as you would expect:

dockerfile
COPY /source/host/path/afile /destination/container/path/

ADD

Pretty much like COPY, with the remarkable difference that ADD can also unpack tarballs and fetch files from remote URLs.

So you could say

dockerfile
ADD https://cool-github-repo.git /destination/container/path/

Pretty handy, but if you don’t need the added functionality, prefer COPY.

VOLUME

Creates a sort of shared directory between host and Container.

So an instruction like:

dockerfile
VOLUME ["/opt"]

Would make the Container’s /opt directory accessible from the host. In fact, it will actually ‘mount the volume’ somewhere under the host’s /var/lib/docker/volumes/ directory.

Docker Build

Used to build an Image, use -f to specify the Image path (optional if it’s located in the cwd) and -t to give it a name.

Those are options you can (but don’t have to) pass. It does however need to get a context as parameter.

A build’s context is the set of files located outside the Container (local path or URL) that it will be able to refer to at build time.

This is more or less like the COPY instruction we saw before, with the difference being that the COPY command makes the hosts files available at run time, while the context makes them available only at build time.

Example

A build command usually looks something like this:

sh
docker build -t my-docker-image -f src/Dockerfile .

Which is to say: ‘Build an Image called my-docker-image based on the file src/Dockerfile with . (or cwd) as its build context’.

Docker Run

Tells Docker to execute the image as defined in the Dockerfile. If a command (or script) is appended, it will override the CMD instruction (if set). Its more or less like spinning up a VM.

It must take an Image as parameter, although its options make it possible to override nearly all the commands specified in the Dockerfile. This allows for a lot of flexibility.

Common options

-it

It allocates a ‘pseudo-tty’ and keeps STDIN open during execution. Useful if you want to be able to interact with your Container through command line.

—rm

Removes the Container from the hosts file system after execution.

-u

Changes the user and group (both of which are root by default) for the specific execution. Useful if your docker Image outputs files to the hosts file system (which can get a bit unwieldy if done so as root).

One neat thing you can do is make the docker Image run as the current host user (the one executing the command). You would do so by passing "$(id -u "$USER"):$(id -g "$USER")" as the parameter for -u.

—volume

Allows you to bind or mount directories from the hosts file system to the Container, or from one Container to another.

Takes an argument of the structure host-source:container-destination (container-destination must be an absolute path).

Example

sh
docker run -it --rm --volume "$PWD":/data -u "$(id -u "$USER"):$(id -g "$USER")" my-docker-image useful-script.sh

Run the Image tagged as my-docker-image in a Container and execute useful-script.sh at startup. Keep STDIN open with a ‘pseudo-tty’ while running and remove the Container when done.

Also, mount the cwd ($PWD) of the host into the /data directory in the Container, and operate as the current host user (and group) instead of root.

Docker Compose

Utility for managing the build and run of one or more Images, and the relations (or dependencies) between them.

You might be able to achieve similar results by just running the Images separately from the command line. This is however a really easy and convenient way of building complex systems of interconnected and/or interdependent Containers in a reproducible manner.

So just like you would build an Image from a Dockerfile, you can compose a bunch of Services from a docker-compose.yml like:

yaml
services:
my-cool-app:
build:
context: ${PWD}
dockerfile: ./Dockerfile
command: python app.py
ports:
- "5000:5000"
mysql:
image: mysql
datadog:
image: datadog
volumes:
- /var/run/docker.sock:/var/run/docker.sock

As you might be able to tell by the general structure of the file, it is quite literally a sequence of ‘Dockerfile-like’ instructions enclosed within a sequence of ‘Services’ (which for simplicity we’ll consider equivalent to Containers).

How it works

  1. Define your app Image with a Dockerfile (and point to it from docker-compose.yml).
  2. Define the Images for the rest of the Containers (Services) you need in docker-compose.yml.
  3. Run docker compose up to start and run the Containers (docker compose down will stop and remove the ongoing processes gracefully).

You can pass it the -d flag to detach the process from the terminal and, just like with the build command, you can use -f to tell it where the docker-compose.yml is located.


Other posts you might like