Close

Using docker-compose and systemd

A project log for Dockerize All the Things

hard drive crash! recover, revive, and re-engineer server using docker contained services this time.

ziggurat29ziggurat29 11/15/2020 at 18:370 Comments

Summary

Here, I define a group of related services (in this case, nginx and php-fpm) using the 'docker-compose' tool.  This allows us to start (and restart) collections of related containers at once.  I also define a systemd 'unit' that will start services defined by docker-compose definitions.

Deets

First, a brief excursion:  my data drive is getting a little messy now with docker build configurations, container configuration files, and then the legacy data.  So I made some directories:

I moved my legacy data directories (formerly directly under 'srv') into 'data', and the existing container configuration file stuff under 'config'.  OK, back to the story.

I got the two containerized services working together, but I started those things up manually, and had to create some other resources (i.e. the network) beforehand as well.  There is a tool for automating this called 'docker-compose'.  Try 'which docker-compose' to see if you already have it, and if not get it installed on the host:

# Install required packages
sudo apt update
sudo apt install -y python3-pip libffi-dev
sudo apt install -y libssl-dev libxml2-dev libxslt1-dev libjpeg8-dev zlib1g-dev

# Install Docker Compose from pip (using Python3)
# This might take a while
sudo pip3 install docker-compose

So, docker-compose uses YAML to define the collection of stuff.  The containers to run are called 'services', and the body defines the various parameters that would passed on the docker command line as I have been doing up to this point.  But it's formatted in YAML!  So get used to it.

Authoring the Compose File

Now I'm going to get a little ahead of myself and tell you that the end goal is to have this docker-compose definition be hooked up as a systemd service, and so I am going to put the file in the place that makes sense for that from the start.  But don't think these files have to be in this location; you could have them in your home directory while you develop them, and maybe this is easier.  You could move them into final position later.

You can find deets on the tool at the normative location https://docs.docker.com/compose/gettingstarted/ ; I am only going to discuss the parts that are meaningful in this project.

First, create the definition in the right place:

sudo mkdir -p /etc/docker/compose/myservices

Ultimately, I am going to create a 'generic' systemd unit definition that will work with any docker-compose yaml, not just the one I am creating here.  If I wind up making more compose scripts for other logical service groups, then I would create a new directory under /etc/docker/compose with a separate name, and store its docker-compose.yml in that separate directory.  So, basically, the directory names under /etc/docker/compose will become part of the systemd service name.

But again I'm getting ahead of myself (sorry).  Onward...

Create the docker-compose definition:

/etc/docker/compose/myservices/docker-compose.yml

version: '3'
services:

  #PHP-FPM service (must be FPM for nginx)
  php:
    image: php:fpm-alpine
    container_name: php
    restart: unless-stopped
    tty: true
    #don't need to specify ports here, because nginx will access from services-network
    #ports:
    #  - "9000:9000"
    volumes:
      - /mnt/datadrive/srv/config/php/www.conf:/usr/local/etc/php-fpm.d/www.conf
      - /mnt/datadrive/srv/data/www:/srv/www
    networks:
      - services-network

  #nginx
  www:
    depends_on:
      - php
    image: nginx-certbot
    container_name: www
    restart: unless-stopped
    tty: true
    ports:
      - "80:80"
    volumes:
      - /mnt/datadrive/srv/config/nginx/default.conf:/etc/nginx/conf.d/default.conf
      - /mnt/datadrive/srv/data/www:/srv/www
    networks:
      - services-network

#Docker Networks
networks:
  services-network:
    driver: bridge

If you take a moment to read it, it should be intelligible if you were able to follow along up to this point regarding all those docker command line invocations and understand the switches I used.  The particularly interesting things here (at least I think) are:

  1. the first line specified the docker-compose format version, and it simply is a requirement that this line be present
  2. the 'services' have names, and these become the names of the containers (rather than the docker-generated random two-part names).  This is particularly useful because these will also become the container's machine name.
  3. the 'depends_on' key tells docker-compose that 'www' must be started after 'php', so I can control start order
  4. at the bottom I defined explicitly a network, named 'services-network' (can be whatever you like), using the bridge driver, and I explicitly connected www and php onto that network.  Now www can communicate with php.  Because of that, I don't need to have php publish its ports on the host machine -- no one needs to mess with those other than nginx.

There are other things that can be defined here, such as persistent docker volumes, but I am using the 'bind' method to make available storage resources on the host, so I don't have a 'volumes' section in this example.

You can get the thing running:

docker-compose -f /etc/docker/compose/myservices/docker-compose.yml up

(it's usually easier to be in the directory with the docker-compose.yml, then you can omit the -f option and simply issue 'docker-compose up')

and to bring them down:

docker-compose -f /etc/docker/compose/myservices/docker-compose.yml down

If all that is working, then I'm ready to wire it into systemd.  (If it's not working, you might consider 'docker-compose logs'.)

systemd

OK, if you did perchance create the 'docker.www' systemd service from before, it's time to replace it, so first:

sudo service docker.www stop
sudo systemctl disable docker.www
sudo rm /etc/systemd/system/docker.www.service

 Now I will make a fancier one:

/etc/systemd/system/docker-compose@.service:

[Unit]
Description=%i service with docker compose
Requires=docker.service
After=docker.service

[Service]
Type=oneshot
RemainAfterExit=true
WorkingDirectory=/etc/docker/compose/%i

# Remove old containers, images and volumes
ExecStartPre=/usr/local/bin/docker-compose down -v
ExecStartPre=/usr/local/bin/docker-compose rm -fv
ExecStartPre=-/bin/bash -c 'docker volume ls -qf "name=%i_" | xargs docker volume rm'
ExecStartPre=-/bin/bash -c 'docker network ls -qf "name=%i_" | xargs docker network rm'
ExecStartPre=-/bin/bash -c 'docker ps -aqf "name=%i_*" | xargs docker rm'

ExecStart=/usr/local/bin/docker-compose up -d --remove-orphans
ExecStop=/usr/local/bin/docker-compose down

[Install]
WantedBy=multi-user.target

This fancier one defines a general class of services that are all named 'docker-compose' followed by the particular service name, and separate by '@'.  In our case, I've created 'docker-compose@myservices'.  The 'myservices' part gets passed in as '%i', and the rest of the definition does the magicry of things in the working directory 'etc/docker/compose/%i', which has the particular docker-compose stuff for that service.

After creating that, I need to do the usual enable and first-time start:

sudo systemctl enable docker-compose@myservices
sudo systemctl start docker-compose@myservices

Now the www and php service should be up and running.  And of course you can stop it or restart it the usual systemctl ways.

All this is nifty, but in this modern era who does unencrypted HTTP anymore?  And especially since you can get TLS certificates for free from Let's Encrypt, I really should try to move towards that.

Next

Modifying the base nginx image to also include tools needed to use Let's Encrypt certificates, and renew them automatically.

Discussions