Deploy docker-compose using Drone CI

One of the simplest way is using secure SSH connection between Drone CI host and Application host:

  1. Create docker-compose which would work on any machine🤘
  2. Order VPS (or dedicated) host with public IP 🌐, and with ssh access and open application ports
  3. Tweak SSH settings to allow multisessional connections for docker deploys🔧
  4. Create bash script📜 which creates ssh private/public keypair inside of deployer-container, then exports DOCKER_HOST=ssh://[email protected]<host IP> and runs docker-compose up

All instructions are explained below.

We used Ubuntu 18.04 OS for my Amazon EC2 t3a.small instance (you can use any hosting). Less then 2 GB might cause memory and hanging up issues.

You need to have next TCP ports open:

If you use Amazon EC2, all ports are closed by default, but you can open ports in Security Group configuration. To do it on instance lauch dialog:

Many VPS/Dedicated hosting providers have all ports open by default.

Login by keypair

Login as ubuntu user with .pem file downloaded from AWS Console:

ssh -i ~/xx.pem [email protected]

Edit file: sudo nano /etc/ssh/sshd_config

Add lines to the end (These settings are required to properly work with docker builds):

PermitRootLogin yes
MaxSessions 500
MaxStartups 500

Run service sshd restart to apply changes✅.
Add your local public ssh key 🔑 content (cat ~/.ssh/id_rsa.pub, use ssh-keygen on your localhost if you have no such file) to the next file on remote host (add to beginning with one line break after):

sudo vim /root/.ssh/authorized_keys

By doing this you allow direct connection using your ssh key with command from your OS user:

ssh [email protected]<APP_HOST_IP>

Prepare docker host

Now on remote host Install docker:

sudo apt update && sudo apt remove docker docker-engine docker.io && sudo apt install docker.io

Prepare repository:

As an example we will show config for Django project.

/.drone.yml

kind: pipeline
name: default
type: docker
concurrency:
  limit: 1
steps:
- name: slack-begin
  image: plugins/slack
  settings:
    webhook:
      from_secret: slack_webhook
    username: Drone
    template: >
      {{repo.name}}/{{build.branch}} - Started #{{build.number}} "${DRONE_COMMIT_MESSAGE}" by {{build.author}} (<{{build.link}}|Open>)

- name: build
  environment:
    VAULT_MASTER_SSH_PRIV_KEY:
      from_secret: VAULT_MASTER_SSH_PRIV_KEY
    VAULT_MASTER_SSH_PUB_KEY:
      from_secret: VAULT_MASTER_SSH_PUB_KEY

    VAULT_MASTER_PORTAINER_PASS:
      from_secret: VAULT_MASTER_PORTAINER_PASS

    VAULT_MASTER_DB_PASS:
      from_secret: VAULT_MASTER_DB_PASS
    VAULT_MASTER_SECRET_KEY:
      from_secret: VAULT_MASTER_SECRET_KEY

  image: devforth/drone-builder
  commands:
  - cd deploy && /bin/bash build.sh

- name: slack-end
  image: plugins/slack
  settings:
    webhook:
      from_secret: slack_webhook
    username: Drone
    template: >
      {{repo.name}}/{{build.branch}} - #{{build.number}} {{uppercasefirst build.status}} after {{since build.started}} (<{{build.link}}|Open>)

This drone files uses 5 Drone Secrets. VAULT_MASTER_SSH_PRIV_KEY and VAULT_MASTER_SSH_PUB_KEY should be copied from your PC to Drone Vault.
VAULT_MASTER_PORTAINER_PASS - pass for portainer (docker admin panel), you can remove it from config if you don't need portainer.

Other secrets are defined to improve application security (better to store all values in drone valult)

/deploy/build.sh

#!/bin/bash

branch=$(git branch | grep \* | cut -d ' ' -f2)
branch=`echo $branch | sed 's/feature\///g'`
echo "Building branch $branch"

if [ -z "$VAULT_MASTER_SSH_PRIV_KEY" ]; then
    echo "Running not on Drone (localy)"
else
    mkdir -p ~/.ssh/
    echo "$VAULT_MASTER_SSH_PRIV_KEY" > ~/.ssh/id_rsa
    echo "$VAULT_MASTER_SSH_PUB_KEY" > ~/.ssh/id_rsa.pub
    chmod 600 ~/.ssh/id_rsa
    chmod 644 ~/.ssh/id_rsa.pub
    eval `ssh-agent -s`
    ssh-add ~/.ssh/id_rsa
    echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
fi


if [ "$branch" = "master" ]; then
  HOST_DOMAIN=<APP_HOST_IP>
  # add more environemnts for branches here, e.g.:
  # elif [ "$branch" = "develop" ]; then
else
  echo "No configuration for branch $branch"
  exit -1
fi

export DOCKER_HOST=ssh://[email protected]$HOST_DOMAIN   

# cleanup cache from previous builds
docker builder prune -a -f
docker container prune -f
docker rmi $(docker images -f "dangling=true" -q) || true

# build and run db image (migration will connect to db)
docker-compose -p stack-$branch -f docker-compose.yml -f assets/variables_$branch.yml up -d --build db

# build django app contanier
docker-compose -p stack-migrate-$branch -f docker-compose.yml -f assets/variables_$branch.yml build --compress django
docker-compose -p stack-migrate-$branch -f docker-compose.yml -f assets/variables_$branch.yml run --no-deps --workdir="/code/" django bash -c 'pipenv run python manage.py collectstatic --noinput'
docker-compose -p stack-migrate-$branch -f docker-compose.yml -f assets/variables_$branch.yml run --no-deps --workdir="/code/" django pipenv run python manage.py migrate

docker-compose -p stack-$branch -f docker-compose.yml -f assets/variables_$branch.yml up -d --build --remove-orphans 
docker-compose -p stack-$branch restart nginx

stack-migrate-$branch is used to reduce downtime during redeploys (migrating and collecting static actually performed without old contener stopping).

/deploy/docker-compose.yml

version: '3.5'

services:
  db:
    network_mode: host
    image: mysql:5.7
    restart: always
    command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_bin
    volumes:
      - v-db-data:/var/lib/mysql

  django:
    network_mode: host
    build: ../src/
    command: /bin/bash -c "cd /code/ && pipenv run gunicorn --worker-tmp-dir /dev/shm conf.wsgi:application --bind 0.0.0.0:8001 --workers 5 --worker-connections=1000"
    # working_dir: /root/
    restart: always
    volumes:
      - v-django:/code/serve_static

  nginx:
    network_mode: host
    build: nginx
    restart: always
    volumes:     
      - type: volume
        source: v-django
        target: /static/django
        read_only: true
        volume:
          nocopy: true

    command: /bin/bash -c "envsubst '$$CERT_NAME $$DOMAIN $$BASIC_AUTH_ENABLE' < /etc/nginx/conf.d/conf.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"

  portainer:
    image: portainer/portainer:1.24.1-alpine
    network_mode: host
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - v-portainer-data:/data


volumes:
  v-portainer-data:
    name: v-portainer-data
  v-db-data:
    name: v-db-data
  v-django:
    name: v-django

network_mode: host is recommended for better networking performance (no docker virtual DNS names resolution)

/deploy/assets/variables_master.yml

This file is used to configure variables per one environement.

version: '3.5'

services:
  db:
    environment:
      - MYSQL_ROOT_PASSWORD=${VAULT_MASTER_DB_PASS}
      - MYSQL_DATABASE=db_live

  nginx:   
    environment: 
      # - BASIC_AUTH_ENABLE=1
      - DOMAIN=yur_site.com
      - CERT_NAME=yur_site_cert

  django:
    environment:
      - ENV_NAME=live
      - DEBUG=False
      - SECRET_KEY=${VAULT_MASTER_SECRET_KEY}
      - DOMAIN=hinty.io
      - DATABASE_URL=mysql://root:${VAULT_MASTER_DB_PASS}@127.0.0.1:3306/db_live?charset=utf8mb4

  portainer:
    command: --admin-password=${VAULT_MASTER_PORTAINER_PASS}

Drone config might look like this:


#ec2 #docker #drone #ci
menu 5
Apr 28, 2020
by Ivan Borshchov

Best related

Simple way to Docker on Windows 10 home with WSL 2

Extend EC2 ext3 filesystem

How to backup SQL Database [Simple ready to use script]

Move docker machine certs from one folder to another

Docker build is slow (or docker-compose build is slow)

Other by Ivan Borshchov

Flask vs django | easy expert comparison

Resource sharing: has been blocked by cors policy [Explain like I am 5]

How to reset mysql root password on ubuntu

Using painterro with Vue.js

Change svg color in vue.js

Fast upload file to AWS S3 from scratch

Simple way to add counts to Django admin filter

Network between 2 instances of OpenVPN

Editor code syntax highlighting in EasyMDE (ex SimpleMDE)

Weighted choice in python