What is Docker?

Docker is software that allows other programs and OS’es to run inside a container. There are other virtualization programs that can do the same (VMWare, VirtualBox, QEMU, etc.), but Docker allows building a container using a script. This script is called a Dockerfile. In addition to this, the package docker-compose allows running multiple dockerfiles at once. This way, multiple containers can run simultaneously and work together to get a certain task done. So, Docker takes much of the pain away when configuring a system, since all system configurations are stored inside the Dockerfiles. This post is going to explain the basics of making a Dockerfile and how docker-compose can manage multiple containers.

Creating a Dockerfile

A Dockerfile is a configuration script for any given container. The Dockerfile contains all the commands needed to configure an entire container (virtual machine). This chapter explains how a Dockerfile is made. All commands one would normally insert into a system are now stored in a Dockerfile.

The syntax

Dockerfiles have the following syntax (where everything including and between the < and > is to be replaced):

# this is a comment
FROM <existing container>      # use someone else's already existing container to get started more quickly

RUN <command>                  # run a command inside the container (configure phase)

# when the container starts, run the following command, where the two dashes (--) allow arguments from the host
ENTRYPOINT ["</path/to/binary>", <optional argument 1>, --, <optional argument 3>, ...]

# when the container starts, run the following command (in case no arguments were given, this is the default)
CMD ["</path/to/binary>", <optional argument 1>, <optional argument 2>]

A real world example

In real life, a Dockerfile contains many more calls to RUN before the container is configured. In the example below, a gitea server is created within a Dockerfile:

# use alpine linux
FROM alpine:latest

# install gitea
RUN apk update
RUN apk add gitea

# run gitea
CMD /usr/bin/gitea

# expose port 3000 to the host
EXPOSE 3000

Before this Dockerfile is used to create a container, another file called docker-compose.yml is created to manage all containers.

Creating a docker-compose configuration

The syntax

A configuration for docker-compose is to be called docker-compose.yml and it has the following syntax:

version: '3'
services:
  # a container which is built using a dockerfile
  a_service:
    build: <directory where the Dockerfile is located>
    container_name: <container name goes here>
    volumes:
      - "<path/to/host/dir>:<path/to/guest/dir>"
    ports:
      - "<host port>:<guest port>"
  # create another service; uses a pre-built image from Docker Hub
  another_service:
    image: <image name from Docker Hub goes here>
    container_name: <container name goes here>
    volumes:
      - "<path/to/host/dir>:<path/to/guest/dir>"
    ports:
      - "<host port>:<guest port>"
    environment:
      - variable=value

A real world example

First, the minimal configuration is created to create a gitea server:

version: '3'
services:
  gitea:
    build: ./  # The Dockerfile is located in the same directory as this file
    container_name: gitea
    volumes:
      - "$PWD/gitea_files/var/lib/gitea:/var/lib/gitea"
      - "$PWD/gitea_files/etc/gitea:/etc/gitea"
    ports:
      - "3000:3000"

The version entry is often set to 3 or 3.x. If the given version is too old, docker-compose will start complaining. This docker-compose.yml file contains one service called gitea, with container name gitea.

Volumes are a mapped directory from guest to host, where /var/lib/gitea and /etc/gitea are folders inside the container. These two folders are mapped to $PWD/gitea_files/var/lib/gitea and $PWD/gitea_files/etc/gitea respectively. After the container has run at least one time, the container files can be viewed from the host OS by entering the gitea_files folder.

Normally, when no volumes are created, each container forgets what it was doing when it stops running. This is not very desireable. When there is a power outage for example, each container would have to be re-configured. When volumes are present in docker-compose.yml, Docker will automatically restore all files in the volumes based on the files from the host OS. This would mean that the gitea server will restore the files for /var/lib/gitea (where the git repositories are located) as well as /etc/gitea (where the configuration files are stored). All the other files in the container (like the binaries in /usr/bin) do not have to be restored, and thus do not need a volume.

Starting the container

To start the docker container, the following command is executed:

# build and start all containers
docker-compose up --build

The gitea Docker container will now be built and run. The build process will only happen once. Next time the container is run, it will start without having to be built. The --build option can therefore be left out if a container is already built.

Testing the container

If a browser is now opened and the url http://localhost:3000 is visited, the gitea setup screen will appear. This means that the container is running as expected. An image of part of the setup screen is shown below:

Gitea setup with default options

Adding another container

By default, gitea is set to use a SQLite3 SQL server. This SQL server is very lightweight, but can only handle one connection at a time. This would mean that gitea would stop working if multiple users started using it. This is not very desireable.

For this reason, another container will be created in the docker-compose.yml. This container will contain a complete MySQL server. The docker-compose.yml file will now look as follows:

version: '3'
services:
  # build gitea using a Dockerfile
  gitea:
    build: ./
    container_name: gitea
    volumes:
      - "$PWD/gitea_files/var/lib/gitea:/var/lib/gitea"
      - "$PWD/gitea_files/etc/gitea:/etc/gitea"
    ports:
      - "3000:3000"

  # add another container, containing a mysql image from docker hub
  db:
    image: mariadb
    container_name: mariadb
    volumes:
      - "$PWD/mariadb_files/var/lib/mysql:/var/lib/mysql"
    environment:
      MYSQL_ROOT_PASSWORD: example_root_password
      MYSQL_USER: example_user
      MYSQL_PASSWORD: example_password
      MYSQL_DATABASE: example_database

A docker container containing a mariadb database server is now added to the list of containers. Another volume is created to make sure that all database related files are transferable to another computer at a later time.

When the containers are now (re) started using the command

# build and start all containers
docker-compose up --build

the gitea container as well as a mariadb server is started.

Communication between containers

The cool thing about Docker containers is that they can talk to each other in a very nice way. How this works can be seen when configuring gitea. Normally, the IP address and port have to be inserted. Because the mariadb server is also a Docker container, gitea can be configured to talk to mariadb:3306. Docker will then automatically translate the mariadb part to the IP address of the mariadb container. This way, the two containers can always talk to each other. The database configuration for gitea now looks as follows:

Gitea database settings

Gitea will always know where the database is, since Docker will always provide the right IP address (also when the docker containers are restarted later). In addition to that, both the gitea and database container will always run together.

Because the Dockerfile for the gitea server is so minimal, the option Run as user is changed from gitea to root, since the gitea user does not exist within the container (programs should run as a non-root user within a docker container if it’s a production server!).

After all configurations are saved, the gitea server is operational and can be used to store projects.

Docker images

For most containers on Docker Hub, the needed docker-compose configuration is given. For some images however, only a command to start a container is given. An example of this is an unofficial nginx and php image. At the time of writing, the description contains the following command to start the container:

docker run -d -p 4488:80 --name=testapp -v $PWD:/var/www creativitykills/nginx-php-server

The options can be “converted” into a service and added to docker-compose.yml as follows:

version: '3'
services:
  nginx_php:
    image: creativitykills/nginx-php-server
    container_name: testapp
    volumes:
      - "$PWD:/var/www"
    ports:
      - "4488:80"

The docker image can now be managed by docker-compose and works exactly the same as it would without docker-compose.

More options

There are more options to Dockerfiles and docker-compose configurations. One example is setting up an nfs server container, where the container has (to have) access to some parts of the host OS. For most of these images, the needed configurations/options are given.

Migrating to another computer

Migrating to another computer is really simple: just copy the Dockerfile, docker-compose.yml and volume-directories to another computer. That’s it! No need to configure a new OS. Just install Docker, copy the files, rebuild the container(s) and it’s all done.

Conclusion

Docker is not too difficult to learn and is a great way to containerize many applications. Once a simple docker container is created using a Dockerfile and a docker-compose.yml file, many more features can be added. If there are containers that need to talk to eachother, it’s all possible! And migrating the whole setup to another computer is just copying files, instead of re-configuring an entire OS.