How I Deploy this Site from Self-Hosted Infrastructure
Last Updated:
For the first iteration of this site, the repository was hosted on GitHub and the actual site on Netlify. My process for deploying it to Netlify was pretty simple: I wrote a GitHub action which on pushes to master would build the site using Zola, then push it to my Netlify site using their official action.
name: Zola on Netlify
on:
push:
branches: [ master ]
jobs:
build_and_deploy:
runs-on: ubuntu-22.04
steps:
- name: Checkout master
uses: actions/checkout@v3
- name: Install and Run Zola
run: |
sudo snap install --edge zola
zola build
- name: Deploy to Netlify
uses: netlify/actions/cli@master
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_API_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
with:
args: deploy --dir=public --prod
secrets: '["NETLIFY_AUTH_TOKEN", "NETLIFY_SITE_ID"]'
The action in question.
This worked fine, but after falling deep down the self-hosting and Linux rabbit hole, I of course thought “wouldn’t this be awesome if I deployed and hosted it from my own server?”
Gitea
So now flash-forward to now, the code is hosted on a self-hosted Gitea repository, with a Gitea action which builds and pushes an image for my compiled static site. That image is then deployed as a container on my server.
name: Build Docker Image and Publish to Local Registry
run-name: Build and Publish Image
on:
push:
branches: master
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
ref: master
- name: Install Hugo
run: apt update -y && apt install hugo -y
- name: Update Submodules
run: git submodule update --init --recursive
- name: Build Hugo
run: hugo --config hugo.toml
- name: Install Docker
run: curl -fsSL https://get.docker.com | sh
- name: Setup QEMU
uses: https://github.com/docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: https://github.com/docker/setup-buildx-action@v3
- name: Build and push
uses: https://github.com/docker/build-push-action@v5
env:
ACTIONS_RUNTIME_TOKEN: '' # See https://gitea.com/gitea/act_runner/issues/119
with:
context: .
file: ./Dockerfile
platforms: |
linux/amd64
push: true
tags: |
${{ vars.REGISTRY }}/${{ vars.IMAGE }}:${{ vars.TAG }}
New action.
If you don’t know, Gitea is an open-source and self-hosted alternative to GitHub. Visually Gitea looks similar to GitHub and has a lot of the same features, including Actions.
Gitea repo for this site
The workflow syntax and Actions UI is basically identical to Github, however there are some capabilities missing when compared to GitHub Actions. But there are also a couple extra features that Gitea actions has, such as writing actions in Go.
If you self-host Gitea and want to use actions, you need to setup the Gitea act runner. Act is a cool project for running GitHub actions locally using docker, and this is the underlying tool Gitea actions uses to run actions. Please note though, these containers Act and Gitea Actions use for running jobs are not the same as the runners GitHub uses to to run jobs. GitHub actions use full VMs, not containers, and come extra tools that may missing from the containers used by Act. For example, the ubuntu-latest
tag in the action above actually corresponds to a Debian bookworm container, and it does not come with docker installed by default, hence the extra Docker installation step.
You can setup the Gitea Act Runner by following the official documentation here. I have set it up using docker. If you need a reference, here is my docker compose service for the act runner:
runner:
image: gitea/act_runner
depends_on:
- server # gitea service is called server
volumes:
- act-runner:/data
- /var/run/docker.sock:/var/run/docker.sock
- ./config.yaml:/config.yaml
secrets:
- runner_registration_token
environment:
- CONFIG_FILE: /config.yaml
- GITEA_INSTANCE_URL: ${INSTANCE_URL}
- GITEA_RUNNER_REGISTRATION_TOKEN_FILE: /run/secrets/runner_registration_token
- GITEA_RUNNER_NAME: gitea-runner-1
- GITEA_RUNNER_LABELS: GITEA_RUNNER_LABELS: ubuntu-latest:docker://node:20-bookworm,ubuntu-22.04:docker://node:20-bookworm,ubuntu-20.04:docker://node:bullseye,ubuntu-18.04:docker://node:buster,cth-ubuntu-latest:docker://catthehacker/ubuntu:act-latest
networks:
- gitea
restart: 'unless-stopped'
By default act_runner uses node:16 images based on Bullseye, I changed them to use Bookworm containers for Node 20. You should use a Node based image because a lot of the basic actions like actions/checkout
require Node and Debian does not come with Node by default. I am using node:20-bookworm
instead of the default labels because that is the version of Debian and Node I use for development and my servers.
If you are having issues with act_runner not registering after making changes to the labels or registration token, delete the volume act-runner
and then try starting the container again.
Deploying
The site is deployed as a container using docker compose. The Dockerfile used to build the image is very simple,
FROM nginx
COPY public /usr/share/nginx/html
After I run hugo
the site is built into the public
folder. I am using nginx
as the base image and copying the compiled site into the appropriate place.
Then on my server I deploy it using a docker compose file like this:
---
services:
hugo-blog:
image: ${REGISTRY}/hugo-blog
network_mode: 'container:public'
Making it Accessible
To make my site accessible to the world, I don’t actually open any ports on my home network. Instead, I have cloud VPS which acts as a proxy server for everything I host publicly.
I would like to make a part 2 with a more detailed guide on how I set this up, but basically I use a Wireguard tunnel to connect the containers on my home server to the VPS and use nginx as a reverse proxy on the VPS. I use a gluetun container as a client to my Wireguard server on the VPS, and then for any service I want to expose publicly I connect that container to gluetun by adding network_mode: 'container:public'
to the service in the compose.yaml
file.
Gluetun is a VPN client container which you can then route other container’s traffic through so they can also use the VPN connection. It supports a wide variety of VPN providers as well as custom OpenVPN and Wireguard setups. Very cool project, check it out.
After this I write an nginx config for the site I want to expose on the VPS and proxy pass to the appropriate Wireguard IP address and port that application is exposed on.
That’s all until part 2, where I will go into detail on how these services are exposed publicly, but for now I hope this gave you some ideas for how to deploy your own site from your own machine.