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.
- Created a Linode Account
- You can use my affiliate link to open an account and get credit. It helps support the blog! :)
- Have a Linux VM running in Linode’s Cloud with Docker Engine installed.
- Have SSH Key Auth setup on the VM.
- Have a secondary SSH key pair set up.
- Have followed the previous1 parts2 in this series.
- Have a Domain registered and an A record pointed to the public IP of your Linode VM.
- I use Google Domains with DNS handled by Cloudflare.
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 [email protected]
If the ssh login fails you may have to permit
root
login. To do this you will need to access theLish
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 runssh 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 [email protected]
You can then test to make sure your SSH key auth is working.
1
ssh -i ~/.ssh/mykey [email protected]
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 tolocalhost
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/[email protected]
# 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/[email protected]
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 thecd ~/caddy
portion of thescript:
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:
Name | Value |
---|---|
LINODEhost | public ip / dns name of your Linode VM |
LINODEuser | username for login |
LINODEkey | SSH 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
.
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!
Affiliate Links
FTC: Some links in this post are income generating.