Auto-Deploy Docker Applications

Brandon Rozek

May 9, 2020

This post will combine that last three posts on Packer, Terraform, and their configuration to show an entire example of how to deploy a docker-compose application. We will specifically look at deploying a game called minetest on DigitalOcean, but these principles can be adjusted to deploy your application as well. The entire setup is documented on Github.

Shared Config

We’re going to use a shared configuration between Packer and Terraform. The template goes like this:

base_system_image = "ubuntu-20-04-x64"
region = "nyc3"
size = "512mb"
domain = "brandonrozek.com" # Replace
subdomain = "minetest"

# Secrets
do_token = "DO-TOKEN-HERE" # Replace
key_name = "SSH-NAME-ON-DO-HERE" # Replace

We’ll also need to define the types of these variables in variables.hcl

variable "do_token" {
  type = string
}

variable "base_system_image" {
  type = string
}

variable "domain" {
  type = string
}

variable "key_name" {
  type = string
}

variable "subdomain" {
  type = string
}

variable "region" {
  type = string
}

variable "size" {
  type = string
}

Packer

Create a packer directory and setup some symbolic links to the share configuration

mkdir packer && cd packer
ln -s ../config variables.auto.pkrvars.hcl
ln -s ../variables.hcl variables.pkr.hcl

Now let’s create a script named setup.sh that will run on top of our base image. This will install Docker and setup the firewall to allow SSH and Minetest traffic through.

#!/bin/bash
apt update
apt upgrade -y

apt install -y docker.io docker-compose
systemctl enable docker-compose
systemctl start docker-compose

ufw allow OpenSSH
# Add any firewall rules you need
# for your application here
ufw allow 30000/udp
ufw --force enable

The image that we’ll use for Minetest comes from linuxserverio. To configure docker-compose we’ll need a file named docker-compose.yml. Its contents will be highly similar to what is listed on their Github.

version: "2.1"
services:
  minetest:
    image: linuxserver/minetest
    container_name: minetest
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=US/Eastern
    volumes:
      - /volumes/minetest/config/.minetest:/config/.minetest
    ports:
      - 30000:30000/udp
    restart: unless-stopped

We’ll need to create a systemd script called docker-compose.service for systemd to enable docker-compose on startup.

[Unit]
Description=Docker Compose Application Service
Requires=docker.service
After=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/root
ExecStart=/usr/bin/docker-compose up -d
ExecStop=/usr/bin/docker-compose down
TimeoutStartSec=0

[Install]
WantedBy=multi-user.target

Finally we’ll need to write a packer configuration file do.pkr.hcl to create our snapshot.

source "digitalocean" "web" {
  api_token = var.do_token
  image = var.base_system_image
  region = var.region
  size = var.size
  ssh_username = "root"
  snapshot_name = "packer-docker"
}


build {
  sources = [
    "source.digitalocean.web",
  ]

  provisioner "file" {
    source = "docker-compose.yml"
    destination = "/root/docker-compose.yml"
  }
  
  provisioner "file" {
    source = "docker-compose.service"
    destination = "/etc/systemd/system/docker-compose.service"
  }
  
  provisioner "shell" {
    scripts = [ "setup.sh" ]
  }
}

To build our image we need to run packer build . in the directory with all these files.

Terraform

Like before, we need to tell terraform where to look for its configuration

mkdir terraform && cd terraform
ln -s ../config terraform.tfvars
ln -s ../variables.hcl variables.tf

To deploy, we only need to create one additional file that will tell digital ocean to create a droplet and assign a subdomain to that droplet.

provider "digitalocean" {
    token = var.do_token
}

data "digitalocean_ssh_key" laptop {
    name = var.key_name
}

data "digitalocean_droplet_snapshot" "packer_snapshot" {
    name = "packer-docker"
    most_recent = true
}

# Create a droplet
resource "digitalocean_droplet" "web" {
    name = "tf-1"
    image = data.digitalocean_droplet_snapshot.packer_snapshot.id
    region = var.region
    size = var.size
    ssh_keys = [data.digitalocean_ssh_key.laptop.id]
    backups = false
}

# Attach a subdomain
resource "digitalocean_record" "www" {
  domain = var.domain
  type   = "A"
  name   = var.subdomain
  value  = digitalocean_droplet.web.ipv4_address
}

output "ip" {
    value = digitalocean_droplet.web.ipv4_address
}

output "domain" {
    value = "${digitalocean_record.www.name}.${digitalocean_record.www.domain}"
}

To deploy run

terraform apply

To later take down the minetest server, run

terraform destroy

Conclusion

This method can be easily configured to run whichever docker services you’d like. All you have to do is edit the packer/docker.compose.yml file and packer/setup.sh to setup the firewall rules.