Post

GitHub Actions - Automatically Deploy Docker Updates to Cloud Hosts via SSH

This is part 3 of this series so if you haven’t completed part 1 and part 2 yet, go back if you want to follow along. In this part we will be updating our Caddy pipeline to deploy updates from Actions to a VM running in Linode’s cloud. As we are using Docker to run Caddy this will be very easy. In Part 4 we will be building a binary and pushing it to the Linode VM instead, which is slightly more complicated.

Note: This is merely a guide to get started. These are the base steps to get you through the door. I’d recommend after you complete this guide that you review your security setup for your VM. As well as locking down access (disabling root login, etc). I will be covering some hardening topics later and will link them here when they are ready.

To fully complete this post you need to have a domain name registered and pointed to your Linode VM’s public IP. If you do not then you can ignore the very end of this post where it demonstrates a working web server.

The Environment

I will be running this series as if you were running on Windows 10/11 with Visual Studio Code installed. Now this series can also be followed fairly easily if you are running Linux setup or WSL. I am also assuming you have a general understanding of the command line interface. If you need more help, leave a comment below and I will reach out!

Pre-Requisites

You should have these items completed and set up before trying to follow this post.

Setting up the VM

First we need to create a new user account on our VM so Actions is not logging in as root. To do this, first SSH into your VM as root (since we haven’t setup any other users yet) and run the following commands to create a new non-root user.

1
ssh i- /path/to/private_key root@host

If the ssh login fails you may have to permit root login. To do this you will need to access the Lish console of your VM from the Linode Dashboard. See “Troubleshoot Rejected SSH Logins” section from the Linode docs site.

If you have key auth setup edit your ~/.ssh/config file and add the VM user, hostname, and the path to your private key. You can then run ssh vm to login without needing to specify the key or ip/dns name. Example setup can be found here.

1
2
# Create a new user called "actions" with a home directory (-m).
useradd -s /bin/bash -m -c "GitHub Actions Account" actions

After the command completes, create a password for the new account.

1
passwd actions

Follow the prompts to set a new password for the actions account. Once done, switch to the new user and verify the home directory was created properly.

1
su actions && cd ~ && ls -a -l

You should see some default hidden files that were created:

1
2
3
4
5
6
total 20
drwxr-xr-x 2 actions actions 4096 Jul  6 14:55 .
drwxr-xr-x 4 root    root    4096 Jul  6 14:55 ..
-rw-r--r-- 1 actions actions  220 Feb 25  2020 .bash_logout
-rw-r--r-- 1 actions actions 3771 Feb 25  2020 .bashrc
-rw-r--r-- 1 actions actions  807 Feb 25  2020 .profile

Now create the .ssh directory structure for the actions user.

1
2
mkdir .ssh
touch .ssh/authorized_keys

Set permissions on the .ssh directory.

1
2
chmod 700 .ssh
chmod 600 .ssh/authorized_keys

If you are unable to set the permissions correctly, switch back to root and set the permissions from there. Update the above commands:

1
2
chmod 700 /home/actions/.ssh
chmod 600 /home/actions/.ssh./authorized_keys

Now Add the second key pair’s public key to the .ssh/authorized_keys file. From your PC run the following commands depending on the OS:

Windows 10/11

1
type $env.USERPROFILE\.ssh\mykey.pub | ssh actions@host "cat >> .ssh/authorized_keys"

Linux

1
ssh-copy-id -i ~/.ssh/mykey.pub actions@host

You can then test to make sure your SSH key auth is working.

1
ssh -i ~/.ssh/mykey actions@host

Make sure to update the path to the public key file and the @host to the public IP/DNS name of your VM.

If it fails, check the troubleshooting section from ssh.com.

Once we have verified that our user can login via SSH, we can disable password auth for the VM. Switch back to root and edit the /etc/ssh/sshd_config file.

1
2
3
su root
# `exit` can also be used
nano /etc/ssh/sshd_config

Hit CTRL+W to enter search and type PasswordAuthentication and hit Enter. Change the value from yes to no. Hit CTRL+X then y to save and close the file. Now restart the sshd service so the changes take affect.

1
systemctl restart sshd

Creating the File Structure

Now we need to setup and create the directory structure for Caddy on the Linode VM. Run the following commands to setup the directory and file structure for Caddy.

1
2
3
4
mkdir ~/caddy
mkdir ~/caddy/data
mkdir ~/caddy/Caddyfile
mkdir ~/caddy/docker-compose.yml

Now edit the docker-compose.yml file:

1
nano ~/caddy/docker-compose.yml

Copy the following into the docker-compose file. Make sure to change <username> to your Docker Hub username. If you used GitHub’s container repo, make sure to add ghcr.io/ in front of your username, which should be your GitHub one (e.g. ghcr.io/alexandzors/caddy).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
version: '3.6'
services:
  caddy:
    restart: always
    logging:
      driver: "json-file"
      options:
        max-size: "500k"
        max-file: "1"
    image: <username>/caddy
    ports:
        - 80:80
        - 443:443
    volumes:
        - ~/caddy/config/Caddyfile:/etc/caddy/Caddyfile:ro
        - ~/caddy/config:/config
        - ~/caddy/data:/data

Save and close the file by hitting CTRL + X then y.

Now edit the Caddyfile and add the following content (same quick start from Caddy’s docs):

1
nano ~/caddy/config/Caddyfile
1
2
3
4
#Caddyfile
yourdomain.tld

respond "Hello World"

If you do not have a domain you can change the yourdomain.tld line to localhost in the Caddyfile.

If you are using Cloudflare for your DNS AND have it proxying connections, you will need to update the Caddyfile to include your CF token. Otherwise automatic SSL certificate generation will fail, which can cause you to become rate limited from Let’s Encrypt or ZeroSSL. If you are using localhost you can ignore this.

1
2
3
4
5
6
7
8
9
10
#Caddyfile
(tls) {
  tls {
    dns cloudflare <yourcftokenhere>
  {
}
yourdomain.tld {
  import tls
  respond "Hello World"
}

Save and close the file by hitting CTRL + X then y. We can leave the current SSH session open as we will be needing it later.

Updating the Actions pipeline

Now that we have the VM file structure ready to go, we can update our GitHub Actions pipeline to automatically update our docker container on image updates. Navigate to our actions repository and open the actions.yml file with your favorite text editor. Now add the following lines at the end of the file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
### This section defines the action ###

# Name of the action
name: Caddy

on:
  # Allows you to set off the action via a POST request to the GitHub workflow API
  repository_dispatch:
    types: caddy 
  # Allows you to manually run the action from inside the Actions tab of the repo
  workflow_dispatch: 

# See https://docs.github.com/en/actions/using-workflows/triggering-a-workflow for more options

### This section defines the job steps for the action ###
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      # Login to Docker Hub using repo secrets
      - name: Docker Hub Login
        id: login_docker
        if: always()
        run: |
          echo ${{ secrets.DOCKER_HUB_PW }} | docker login -u ${{ secrets.DOCKER_HUB_USR }} --password-stdin
      # Build docker image using the Dockerfile
      - name: Build Docker Image
        id: build_docker
        if: success()
        run: |
          docker build -f ./Dockerfile -t ${{ secrets.DOCKER_HUB_USR }}/caddy:latest .
      # Push docker container to Docker Hub
      - name: Push Docker Image
        id: push_docker
        if: success()
        run: |
          docker push ${{ secrets.DOCKER_HUB_USR }}/caddy:latest
      # ADD LINES BELOW THIS ONE
      # // DEPLOY TO VM
      - name: Deploy to VM
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.LINODEhost }}
          username: ${{ secrets.LINODEuser }}
          key: ${{ secrets.LINODEkey }}
          port: 22
          script:
            cd ~/caddy && docker compose down && docker compose pull && docker compose up -d

If your Caddy directory is not in /home/username/caddy, you will need to change the cd ~/caddy portion of the script: line to match your directory path. (e.g. cd /opt/docker/caddy)

Once you are done editing, save the file.

Updating Actions Secrets

With our actions pipeline file updated, we need to update our secrets to match. Navigate to your repository on GitHub.com and click on the Settings tab > Secrets > Actions, then create the following secrets:

NameValue
LINODEhostpublic ip / dns name of your Linode VM
LINODEuserusername for login
LINODEkeySSH secret key (use the second pair’s secret key not the one you used to deploy the host with.)

Updating the Repository

Now that we have the secrets created we can update our repository.

1
2
3
git add .
git commit -m "set actions to update linode vm"
git push

After the push our actions pipeline will automatically kick off. To watch the log, return to the repository on GitHub and click on the Actions tab. Under “All Workflows” click on Caddy. The newest run should show up as a yellow flashing dot. Click on the item title to view the pipeline and follow the log output. When it completes we can run a curl check on our webserver to verify its running.

1
curl https://yourdomain.tld

If you are using localhost you need to be in the ssh session of your VM to get a response.

If successful, you should get a Hello World response. If it fails, return to your SSH session and first check that Caddy is running using docker ps.

Docker 'ps' output showing Caddy running

If it is not, cd to the caddy directory and manually run the container.

1
2
cd ~/caddy
docker compose up

Running docker compose up without the -d switch runs the container in your current terminal session. It is useful when troubleshooting as you can watch the container’s log output live. When you exit (CTRL+C) the session the container will stop. Add the -d switch to run in detached mode.

If it starts up with no errors, terminate the container using CTRL+C. Then double check your action’s file commands for spelling mistakes during the container running portion under the “Deploy to VM” step. As always, if you run into issues, let me know in the comments or reach out via email and I’ll gladly walk you through it! Check out part 4 where we change up the pipeline to build and release a binary version of Caddy.

Thanks to my good friend Stefan for helping proof this post series!


FTC: Some links in this post are income generating.

This post is licensed under CC BY 4.0 by the author.