Greg Bair's Blog #
Personal blog built with Hugo, featuring posts about C#, programming, and homelab adventures.
This work is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
Tech Stack #
- Static Site Generator: Hugo (Extended)
- Theme: Console (custom theme, managed as git submodule)
- Web Server: Caddy (containerized)
- CI/CD: GitLab CI/CD
- Deployment: Podman Quadlet self-hosted
- Container Registry: GitLab Container Registry
Local Development #
Prerequisites #
- Hugo Extended (v0.139.4+)
- Git
Setup #
# Clone the repository
git clone git@gitlab.com:gregbair/blog.git
cd blog
# Initialize theme submodule
git submodule update --init --recursive
# Start development server
hugo server -D
Visit http://localhost:1313 to see your site.
Creating New Posts #
# Create a new blog post
hugo new posts/my-new-post.md
# Create a new game page
hugo new games/my-new-game.md
New posts are created with draft = true. Set draft = false to publish.
Building #
# Build the site
hugo
# Build with minification (production)
hugo --minify --baseURL https://gregbair.blog
Built site will be in public/ directory.
Deployment #
Automated Deployment (CI/CD) #
Pushing to main automatically triggers the production pipeline; pushing to any other branch
triggers the staging pipeline instead:
- Build - Hugo site is built with the theme submodule (staging additionally passes
-Dto include draft posts) - Publish - Container image built with Kaniko and pushed to the GitLab Container Registry
(
$CI_REGISTRY_IMAGE) - Deploy - Runs directly on the self-hosted GitLab Runner on the deploy host (see below) and deploys using Podman Quadlet
Production (main) |
Staging (any other branch) | |
|---|---|---|
| URL | https://gregbair.blog | https://staging.gregbair.blog |
| Image tag | latest |
staging |
| Container / service | blog / blog.service |
blog-staging / blog-staging.service |
| Port | 8081 | 8082 |
| Drafts included | No | Yes (hugo -D) |
Staging is a single shared deployment - whichever non-main branch pushed most recently is what's
live at staging.gregbair.blog.
Manual step required outside this repo: staging.gregbair.blog needs a Cloudflare route
added (DNS record / tunnel ingress rule, matching however gregbair.blog is currently routed)
pointing at the deploy host's port 8082. This isn't managed by CI or the ansible-homelab repo.
Manual Deployment #
# Build the site
hugo --minify --baseURL https://gregbair.blog
# Build container image
docker build -t blog .
# Run locally
docker run -p 8080:8080 blog
Visit http://localhost:8080
CI/CD Configuration #
Required GitLab CI/CD Variables #
None. The publish job authenticates to the GitLab Container Registry using the built-in CI_REGISTRY_USER / CI_REGISTRY_PASSWORD job credentials. The deploy job runs directly on the deploy host via a self-hosted GitLab Runner (see below), so no SSH key or registry credentials need to be stored in GitLab at all.
Self-Hosted Runner (deploy host) #
The deploy job is pinned with tags: [deploy] and must run on a GitLab Runner registered on the deploy host itself, using the shell executor as the greg user:
# On the deploy host
curl -L "https://gitlab.com/gitlab-org/gitlab-runner/-/releases/latest/downloads/binaries/gitlab-runner-linux-amd64" -o gitlab-runner
chmod +x gitlab-runner
sudo mv gitlab-runner /usr/local/bin/
gitlab-runner register \
--url https://gitlab.com/ \
--tag-list deploy \
--executor shell \
--run-untagged=false
One-time setup on the host so the deploy job can pull without any secrets in CI:
podman login registry.gitlab.com -u <username> -p <deploy-or-personal-access-token>
podman login persists the credential to ~/.config/containers/auth.json, so it survives across runs without ever touching GitLab's variable store.
Pipeline Files #
.gitlab-ci.yml- Main CI/CD pipeline (production + staging)Dockerfile/Caddyfile- Production image definition and web server configurationDockerfile.staging/Caddyfile.staging- Staging image definition (same as production, no access restrictions - staging is publicly reachable and may include draft posts)
Container Deployment #
The blog runs as systemd service(s) on the deploy host using Podman Quadlet:
| Production | Staging | |
|---|---|---|
| Service | blog.service |
blog-staging.service |
| Container port | 8080 (published as 8081) | 8080 (published as 8082) |
| Image | $CI_REGISTRY_IMAGE:latest |
$CI_REGISTRY_IMAGE:staging |
| Quadlet file | ~/.config/containers/systemd/blog.container |
~/.config/containers/systemd/blog-staging.container |
Managing the Service (on server) #
# View status
systemctl --user status blog.service
# Restart
systemctl --user restart blog.service
# View logs
journalctl --user -u blog.service -f
# View container logs
podman logs -f blog
Substitute blog-staging for blog in any of the above to manage the staging deployment instead.
Project Structure #
.
├── archetypes/ # Content templates
├── content/ # Markdown content
│ ├── posts/ # Blog posts
│ ├── games/ # Game-related pages
│ └── _index.md # Homepage content
├── layouts/ # Custom layout overrides
│ └── partials/ # Partial templates
├── static/ # Static assets
├── themes/ # Themes (git submodule)
│ └── console/ # Console theme
├── .gitlab-ci.yml # CI/CD configuration
├── Caddyfile # Web server config
├── Dockerfile # Container image definition
└── hugo.toml # Hugo configuration
Configuration #
hugo.toml #
baseURL- Production URL (https://gregbair.blog)title- Site titletheme- Theme name (console)languageCode- Site language (en-us)
Theme Management #
The Console theme is a git submodule. To update:
cd themes/console
git pull origin main
cd ../..
git add themes/console
git commit -m "Update theme submodule"
License #
See theme repository for theme license information.
Links #
- Live Site: https://gregbair.blog
- Staging Site: https://staging.gregbair.blog
- Repository: https://gitlab.com/gregbair/blog
- Theme: https://codeberg.org/gregbair/console-hugo-theme