This is how I deploy my personal applications (e.g. this site and pubg.sh) to my VPS.
I like it because it’s simple, fast, and reliable.
Triggering
I want deploys to automatically happen on commits to master. I prefer building the new application directly on my
VPS to avoid having to transfer files.
A GitHub Action SSHes to my VPS with a restricted command key (sometimes called a single command or single purpose key) for the associated site, which triggers the execution of a script on the VPS. This key can’t do anything else on the server, which is nice in case it’s ever exposed. It’s stored as a secret in the GitHub repository. Here’s the GitHub Action:
name: Deploy
on:
push:
branches: [master]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
run: |
eval `ssh-agent -s`
ssh-add - <<< "$SSH_PRIVATE_KEY"
ssh -o StrictHostKeyChecking=no $SSH_USER@$SSH_HOST
env:
SSH_USER: ${{ secrets.DEPLOY_SSH_USER }}
SSH_KEY: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}
SSH_HOST: ${{ secrets.DEPLOY_SSH_HOST }}
The other part is at ~/.ssh/authorized_keys on the VPS:
command="/apps/deploy/azzolini.io.sh",no-port-forwarding,no-x11-forwarding,no-agent-forwarding <THE PUBLIC KEY>
Building & Deploying
/apps/deploy/azzolini.io.sh is now invoked on the VPS on all pushes to master.
This script uses Docker to build a newer image and then redeploys the container with docker compose.
#!/bin/bash
set -e
cd /apps/repos/azzolini.io
git pull
docker build . -t azzolini.io:latest
docker compose -f /apps/docker-compose.yml up --build -d azzolini.io
Fully Static Sites
Fully static sites served just by nginx usually often still need a build step.
I use Docker to build the site with the following as the last line in the Dockerfile: RUN tar -cf build.tar dist.
Then, the deploy script can do this to get the new site to nginx:
docker build . -t azzolini.io:latest
docker run --rm azzolini.io:latest cat build.tar > build.tar
tar -xf build.tar
rm -f build.tar
rm -rf /apps/nginx-sites/azzolini.io
mv dist /apps/nginx-sites/azzolini.io
Docker Compose
All of my services are managed by Docker Compose. nginx serves as a reverse proxy.
services:
nginx:
image: nginx
container_name: nginx
ports:
- 80:80
- 443:443
volumes:
- /apps/nginx/nginx.conf:/etc/nginx/nginx.conf
restart: always
azzolini.io:
image: azzolini.io:latest
container_name: azzolini.io
restart: always
ports:
- 4321:4321
nginx
The reverse proxy configuration in nginx.conf looks like this:
server {
server_name azzolini.io;
location / {
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $http_host;
proxy_pass "http://172.17.0.1:4321";
}
}
Useful Resources
- I use the certbot/certbot Docker image to generate my Let’s Encrypt certificates.
- Mozilla provides a good SSL Configuration Generator.