How This Blog is Deployed | Conner's Corner
servers
knowlege-backup
meta
cd
forgejo
git
How This Blog is Deployed
July 6, 2026
I mentioned last month I had changed the underlying tech driving this blog to be primary written in golang. I may release the structure of that application at some point, but for now I thought I would document how I deploy this service to my VPS.
My VPS is hosted on hetzner, I try to host most of my external-facing things there to avoid my in home NAS from being too much of an attack surface. My previous deployment process was faily simple but not pipeline-driven.
Previous Deploys
Write new content or add a new feature
run make build
run make sync
This did a docker build which exported the contents of the static site generator’s output to a local folder, then make sync ran rsync to push the changes to a folder on my vps. My site at that time was a nginx service sitting behind my nginx proxy.
I wanted to keep a similiar process with the rewrite, but I wanted it to be backed by a pipeline, so I could write anywhere with access to my git server, and deploy with a push. I also wanted this to be much more secure and locked down than my previous action, which involved my primary user copying files to the service. This user had full sudo access to the machine, which is not ideal security practice.
After some research and experimentation, here is where things stand.
Server User Setup
I have a user deploy on my VPS. My VPS is named ext(yes this is a departure from my naming system). This user has a home directory in my service folder.
sudo useradd -M -d /var/docker-service/blog/ -s /usr/sbin/nologin deploy<br>sudo chown -R deploy:deploy /var/docker-service/blog/
Then I created a wrapper script named /usr/local/bin/blog-compose for running docker-compose commands in that directory. `
#!/bin/bash<br>set -e<br>cd /var/docker-service/blog<br>exec docker compose -f /var/docker-service/blog/docker-<br>compose.yml "$@"
Make this script executable
sudo chmod +x /usr/local/bin/blog-compose
Now you need to allow that script to be run with sudo as your user. This allows this user to perform docker commands without needing to be a member of the docker group
sudo visudo -f /etc/sudoers.d/blog<br>### contents<br>deploy ALL=(root) NOPASSWD: /usr/local/bin/blog-compose<br>deploy ALL=(root) NOPASSWD: /usr/local/bin/blog-compose *
Finally, you need to generate an ssh key and add it to /var/docker-service/blog/.ssh/authorized_key file. I added a passphrase for additional security. There are 1 million articles on this on the web, so I won’t repeat those details.
Docker Compose
I run all my services as docker containers. This is partly for encapsulation and partly because it makes adding and removing services simple. I front the containers with nginx-proxy and it’s acme-companion, which makes SSL super low maintenance. Because of this, the site is run as a Docker container. Here is the corresponding compose file.
Notes: I use scratch as my base image for running the site, this makes for a smaller image and reduces potential attack surfaces. Because of this I am mounting the root SSH keys and timezone data into the container. Without these, requests to my mealie instance fail as it cannot validate the ssh key and using a timezone to gatekeep post publishing fails as it cannot find timezone data.
services:<br>connermccall.com:<br># I run a docker registry internally for personal projects<br>image: registry.connermccall.me/personal/connermccall.com:latest<br>environment:<br>- "URL=connermccall.com"<br># Recipe retrieval requires a key<br>- "MEALIE_API_KEY=${MEALIE_API_KEY}"<br># there are the important things, this tell the proxy and companion what domains we are serving this site on.<br>- "LETSENCRYPT_EMAIL=conner@connermccall.com"<br>- "LETSENCRYPT_HOST=connermccall.com,www.connermccall.com"<br>- "VIRTUAL_HOST=connermccall.com,www.connermccall.com"<br>- "VIRTUAL_PORT=8080"<br>networks:<br>- network-nginx-proxy<br>restart: "unless-stopped"<br>volumes:<br>- "/etc/localtime:/etc/localtime:ro"<br>- "/etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro"<br>- "/usr/share/zoneinfo:/usr/share/zoneinfo:ro"<br>logging:<br>driver: syslog<br>options:<br>tag: "docker/connermccall-com"<br># I mark this as external as it is created with tofu elsewhere. The container has to be on this network for the proxy and companion to recognize it<br>networks:<br>network-nginx-proxy:<br>external: true
CI Setup
I use Forgejo as my forge. It has been rock solid for the past several years and I have a runner configured on my local NAS. This gives me more compute than my VPS provides along with plenty of disk space and access to local resources.
I will walk through the pertinent parts of my script, the full version is at the end of this post.
The first five steps are checking out the container, setting up build details, and actually building and pushing the container. Once that is done we need to update the running container on my VPS, which we do through SSH.
I chose to...