
Self-Hosting
I run various services to stay independent from big corporations and international politics, but also to maintain and expand my knowledge in this field.
I run a small personal homepage and blog, which serves two main purposes: as a personal profile for self-marketing and as a place to share my projects, experiences, and learning.
Since I want my homepage to be lightweight, fast, and secure, I decided to use a static site generator. After some research, I settled on careercanvas, a project by Felipe Cordero built on top of the Hugo static site generator. Careercanvas is especially well-suited for personal resumes and developer portfolios, making it a great fit for my use case.
Careercanvas is still a work in progress and doesn’t integrate perfectly with Hugo template projects yet. For now, the recommended approach is to fork Felipe’s homepage repository: felipecordero.github.io.
I forked it privately into my self-hosted Forgejo instance, and since I made a few modifications to the theme itself, I also forked careercanvas into my own repository.
To make publishing updates to my homepage easier, I wrote a Forgejo action that runs on my private Forgejo runner. Whenever I create a new tag in the repository, the workflow kicks in and takes care of everything:
...
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Hugo CLI
run: |
wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \
&& dpkg -i ${{ runner.temp }}/hugo.deb
- name: Install Node.js dependencies
run: "[[ -f package-lock.json || -f npm-shrinkwrap.json ]] && npm ci || true"
- name: Install podman
run: |
apt update
apt install -y podman
- name: Get date
id: date_step
run: echo "TAG=$(date '+%Y-%m-%d_%H-%M-%S')" >> $GITHUB_OUTPUT
- name: Build homepage
run: |
npm install
rm -rf public
npm run build:css
npm run build
tar -czf "tomirgang-${{ steps.date_step.outputs.TAG }}.tar.gz" public/*
- name: Upload homepage artifact
uses: actions/upload-artifact@v3
with:
name: tomirgang.tar.gz
path: tomirgang-${{ steps.date_step.outputs.TAG }}.tar.gz
- name: Build container
run: |
podman build -t git.tomirgang.de/tom/tomirgang:latest .
- name: Build tag the container
run: |
podman tag git.tomirgang.de/tom/tomirgang:latest git.tomirgang.de/tom/tomirgang:${{ steps.date_step.outputs.TAG }}
podman image ls
- name: Upload latest container
run: |
podman push --creds tom:${{ secrets.HOMEPAGE_CONTAINER_TOKEN }} git.tomirgang.de/tom/tomirgang:latest docker://git.tomirgang.de/tom/tomirgang:latest
- name: Upload date-tagged container
run: |
podman push --creds tom:${{ secrets.HOMEPAGE_CONTAINER_TOKEN }} "git.tomirgang.de/tom/tomirgang:${{ steps.date_step.outputs.TAG }}" "docker://git.tomirgang.de/tom/tomirgang:${{ steps.date_step.outputs.TAG }}"
At a high level, the action does the following:
.tar.gz
archive of the generated files.latest
and as a timestamped version.This approach makes deployments simple and repeatable: the final result is a ready-to-run container image with Nginx serving the static site.
On the server, I run the container using docker-compose:
services:
homepage:
image: git.tomirgang.de/tom/tomirgang:latest
container_name: homepage
ports:
- 2210:80
restart: unless-stopped
watchtower:
image: containrrr/watchtower
container_name: watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
restart: unless-stopped
command: --interval 3600
Here’s how it works in practice:
This means publishing a change is as simple as:
What makes this setup even more fun: everything (homepage, Forgejo instance, and Forgejo runner) is hosted on the same inexpensive server — a Hetzner CX22 for just about €5/month.
It’s a neat, cost-effective, and self-contained system that gives me full control over my site, with automated builds and deployments requiring almost no manual intervention.