This repository has no description
README.md

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:

  1. Build - Hugo site is built with the theme submodule (staging additionally passes -D to include draft posts)
  2. Publish - Container image built with Kaniko and pushed to the GitLab Container Registry ($CI_REGISTRY_IMAGE)
  3. 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 configuration
  • Dockerfile.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 title
  • theme - 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.