Taco Steemers

A personal blog.

Notes on docker

Docker is a container runtime. A container is a way to organise dependencies and run code that stays "contained"; what happens inside the container doesn't interfere with the data and software on the computer it runs on. Docker has a background service and a client application that we use to interact with the service.

One example of how we might use it is to create a docker image that contains everything we need to generate a website like the one you are looking at, created with the Pelican static site generator. An advantage of working that way would be that we don't need to install Pelican-specific dependencies on all the computers we want to use, we only need to have Docker installed. A downside would be that it makes the process of building our website slower. The whole process of building becomes different, and we need to do additional work to be able to see the output and save the output.

Basic Docker concepts

Some important terms are "Dockerfile", "image" and "container". The container is the collection of code that we are running. The docker service is executing the container code for us. The image is the state in which the container starts out. The Dockerfile is a file that we create, with instructions on what the image needs to contain.

Example Dockerfile

Below is an example Dockerfile for building a Pelican website. The file is called "Dockerfile" and is placed in the project's root directory. The lines starting with # are comments.

# We use the latest version of the Alpine Linux base image: https://hub.docker.com/_/alpine/
FROM alpine:latest
# Indicate which directory to use when running commands.
WORKDIR /app
# Copy the src directory on the host computer to the working directory inside the container.
# This only happens once, when building the container.
# In this example the src directory contains the source files for our website. 
COPY src .
# RUN is for commands as part of building the image.
# Here we adding tools to make it possible to use Pelican.
RUN apk add --no-cache python3 py3-pip make
RUN python3 -m pip install pelican[markdown]
# The compile script that was copied from the src directory needs to be executable.
RUN chmod +x compile.sh
# CMD is for running commands after the container has launched.
CMD ["python3", "-m", "http.server", "8000"]

The last line is asking to start the Python development server after the container starts. That makes it easy for us to see which files are created inside the container working directory, and we can use that to see the effect of any changes we make in that directory.

Interacting with the docker service and docker client

We can use the docker client to ask the docker service to "build" and "run" an image, in which case we will get a running container based on the image.

Build a docker image based on the contents of this directory:

docker build -t imagename .

Run a docker container based on the docker image, give it a name, and map the host port 8000 to the same container port:

docker run -d -p 8000:8000 --name containername imagename

We can use the docker client to stop, restart or delete a container. We can also the docker client to ask the docker server to execute commands, inside the container. We would need to do that if we want to use a container to generate a website. If we delete a container any data inside the container will be lost forever. That also means any program output. If we want to keep the data we need to do additional work. In other words, docker is a stateless system by default.

Here are some commands for checking which containers are running, stopping and removing containers:

docker ps
docker stop containername
docker rm containername

Using the example docker file

Let's see how we can use the docker file example listed above. In this example we use the project name "diystaticsites". When running the below commands it is assumed that the src directory and the src/compile.sh example file actually exist in the directory that we run the commands in. The src/compile.sh file does not need any content for this example.

  • If the docker file is named "Dockerfile": docker build -t diystaticsites .
  • If we have a different file name we need to pass that with -f: docker build -f diystaticsites.dockerfile .
  • To create a new container called diystaticsites using the diystaticsites image: docker run -d -p 8000:8000 --name diystaticsites diystaticsites By passing the argument -p 8000:8000 we map the host port 8000, listed on the left side, to the container port 8000, listed on the right side.

  • If we want to stop our container and throw it away, including all the data inside it, we can combine all these commands in one script:

    docker stop diystaticsites docker rm diystaticsites docker build -t diystaticsites . docker run -d -p 8000:8000 --name diystaticsites diystaticsites

Interacting with the container

We can open an interactive shell to the diystaticsites container:

docker exec -it diystaticsites sh

If we want we can use the terminal to create a file, and browse localhost:8000 on our host computer to see that the file has indeed appeared in the container's working directory.

We can execute the compile.sh script that was copied to the image when we built the image:

docker exec diystaticsites /app/compile.sh

If we want to use that output anywhere else, such as on our live website, we will need to copy it out. Here is an example command for copying the compile.sh file from inside the container to the tmp directory on the host:

docker cp diystaticsites:/app/compile.sh /tmp/compile.sh

When copying a directory from the container we only indicate where we want to place the directory, without including the directory name. For example, here we copy the /app/output directory from the container to our local working directory, including the contents of the directory.

docker cp diystaticsites:/app/output .