docker · ansible · automation · containers

A Summary of How I Automated My Server with Ansible, Docker, and Traefik

Something I’ve wanted for a while now is a Platform as a Service but self-hosted so that I could install apps more efficiently on my VPS; something that could alleviate some of the hassle of setting up web apps and also keep me from having to install a bunch of extra runtimes that I’m not very familiar with.

My requirements were:

  • Keep each app self-contained and not pollute the file system
  • Automatically know how publish apps to the internet on install
  • Not require me to manually ssh into the box to configure things by hand

Here’s what I came up with:

  • Ansible to configure and install apps
  • Docker containers housing all the installed apps
  • Traefik running inside of Docker handling all web traffic

Gluing everything together

I was delighted to find that most of the apps I was using already had prebuilt docker images available. For security, all the container images I used had to have at least some kind of endorsement from the developers. It’s easy to tell if an open source app supports Docker by checking the source repository for a Dockerfile.

Now, finding the Docker image isn’t the end of the process. Usually, I had to do a bit of configuring to match how I use the app, and some apps needed other services to run.

Docker’s solution for this is called Docker Compose. Docker Compose allows you to write a file describing which containers need to be started, which ports on those containers need to be exposed to the network, and where Docker should store the data that the container needs to persist.

I ended up needing to write about 2 files per app to describe how I wanted it to be setup. The first was a docker-compose.yml file containing the application configuration, and the second was an Ansible playbook (<app-name>.yml) that copied the docker-compose.yml and any other configuration files for that app to the server and then started the containers using docker-compose up. For some apps, the playbook needed to perform additional setup. Gitea, for example, needs an ssh passthrough configured to access git repositories over ssh without a special port.

My local directory structure for all these files looks something like this:

~/dev/ansible
├── docker
│   ├── gitea
│   │   ├── docker-compose.yml
│   │   ├── gitea
│   │   │   └── git
│   │   └── passthrough_script
│   ├── journey
│   │   ├── data
│   │   │   ├── config.json
│   │   │   └── themes
│   │   ├── docker-compose.yml
│   │   └── Dockerfile
│   ├── matrix-synapse
│   │   ├── docker-compose.yml
│   │   └── homeserver.yaml
│   ├── nginx
│   │   ├── docker-compose.yml
│   │   ├── nginx.conf
│   │   ├── sites-available
│   │   │   ├── static-site
│   │   │   └── another-static-site
│   │   ├── sites-enabled
│   │   │   ├── static-site
│   │   │   └── another-static-site
│   │   └── www
│   │       ├── static-site
│   │       └── another-static-site
│   ├── riot
│   │   ├── config.json
│   │   └── docker-compose.yml
│   └── traefik
│       └── docker-compose.yml
├── docker_ubuntu.yml
├── gitea.yml
├── initial_server_setup.yml
├── inventory
├── journey.yml
├── matrix-synapse.yml
├── nginx.yml
├── riot.yml
└── traefik.yml

Most of my playbooks looked like this:

---
- name: Install Journey in docker
  hosts: all
  remote_user: ansible

  tasks:
      - name: setup docker compose file
        copy:
            src: docker/journey
            dest: ~/docker

      - name: add images mountpoint
        file:
            path: ~/docker/journey/data/images
            state: directory

      - name: add database mountpoint
        file:
            path: ~/docker/journey/data/journey.db
            state: touch


      - name: Start journey
        docker_compose:
            build: yes
            project_src: docker/journey
        become: yes

and my docker-compose.yml files looked like this:

version: '3.7'
services:
        journey:
            labels:
              - "traefik.http.routers.journeyTLS.rule=Host(`thoughtfuldragon.com`, `www.thoughtfuldragon.com`)"
              - "traefik.http.routers.journeyTLS.tls.certresolver=letsencrypt"
              - "traefik.http.middlewares.httpsredirect.redirectscheme.scheme=https"
              - "traefik.http.routers.journey.rule=Host(`thoughtfuldragon.com`, `www.thoughtfuldragon.com`)"
              - "traefik.http.routers.journey.middlewares=httpsredirect"
              - "traefik.docker.network=web"
              - "traefik.enable=true"
            build: .
            image: journey
            restart: unless-stopped
            ports: 
                - 8084
            volumes:
                - ./data/journey.db:/root/content/data/journey.db
                - ./data/config.json:/root/config.json
                - ./data/images:/root/content/images
                - ./data/themes:/root/content/themes
            networks:
                - web
networks:
    web:
        external: true

Those lines under labels tell Traefik how to route requests to the service. Some important things to note on how I setup Traefik:

  • Traefik needs to share a Docker network with the containers it routes to. In my setup this is the web network.
  • Each Docker Compose file defines the domain and/or path that this app will be served on using Routers.
  • Some of my apps use Middlewares to redirect to https or require authentication.
  • I have configured Traefik to only expose containers which have it enabled. This improves security.
  • Traefik can’t create DNS entries, so I set those up manually.
  • I setup Traefik to get TLS certificates from Let’s Encrypt.

The Docker daemon notifies Traefik when a container starts. Traefik then reads the labels on the container and configures itself to serve requests to it.

Once I had written a docker-compose.yml and Ansible playbook for an app, I ran

ansible-playbook <app-name>.yml -i inventory -l production

and after a few seconds I could access it in my browser and view it in the Traefik dashboard.

Except if I messed up somewhere

Usually it took several goes before I got everything right. I have a home server that I use as a testbed for my Docker containers. Once I’ve gotten stuff setup there, I generally know how to write the playbook, but sometimes that needs a few iterations as well.

When I finally decided I knew enough to put this system in production, I needed two full days to get everything setup. Most of that time was writing the Docker Compose and playbook files for the apps that I hadn’t gotten to during my preparations.

Conclusion

Ansible, Docker, and Traefik each automate a portion of the work needed to deploy apps. In the end, I still needed to spend time working out how to configure each application. Using these tools didn’t eliminate the need to install and configure the applications, but they did smooth out the process.

Resources

Official Documentation

https://docs.traefik.io/
https://docs.ansible.com/ansible/latest/index.html
https://docs.docker.com/

DigitalOcean Ansible Tutorials

https://www.digitalocean.com/community/tutorials/configuration-management-101-writing-ansible-playbooks

https://www.digitalocean.com/community/tutorials/how-to-use-ansible-to-automate-initial-server-setup-on-ubuntu-18-04

https://www.digitalocean.com/community/tutorials/how-to-use-ansible-to-install-and-set-up-docker-on-ubuntu-18-04

Published:
comments powered by Disqus