From GitHub Notes to Production Blog

Infrastructure
Table of Contents

Why This Guide Exists

I’ve been doing a lot of dev work, research and hadn’t really thought about sharing the knowledge without having to learn frontend development or maintain complex code. I knew I had a bunch of markdown GitHub notes and thought they could be used as a baseline to catch up on all my lack of documentation. Then I found Hugo.

This guide covers the entire workflow of creating a Hugo blog, containerizing it, and deploying it to Kubernetes - all without writing a single line of frontend code (html, css, js).

My Journey Roadmap

From Notes to Blog

  1. Converting local notes - Migrating content from GitHub markdown
  2. Setting up Hugo locally - Getting a working blog without frontend knowledge
  3. Containerizing with Docker - Making deployment reproducible
  4. Automating with GitHub Actions - Push code, get blog updates
  5. Production deployment on Kubernetes - Professional hosting with GitOps
  6. Roadmap
    1. Migrating content from OneNote - Converting all my onenote thoughts into useful notes, no idea yet how I will do this but ok…

Progressive Development Approach

  1. Get it working locally first - Set up Hugo, add content, and test with hugo server
  2. Build a Docker image - Containerize your working site
  3. Deploy to Kubernetes - Run the containerized site in your K8s cluster

Table of Contents

  1. Introduction to Hugo
  2. Setting Up Your Hugo Blog
  3. Writing Content
  4. Building the Docker Image
  5. GitHub Actions CI/CD
  6. Deploying to Kubernetes
  7. Production Considerations

Introduction to Hugo

Hugo is one of the most popular open-source static site generators. It’s written in Go and known for its incredible build speed and flexibility. Key benefits include:

  • Lightning Fast: Builds sites in milliseconds
  • No Dependencies: Single binary with no runtime dependencies
  • Flexible: Supports multiple content formats (Markdown, HTML, etc.)
  • Themeable: Large ecosystem of themes
  • Built-in Features: RSS feeds, taxonomies, multilingual support

Setting Up Your Hugo Blog

1. Install Hugo

For macOS installation (which I used):

# macOS using Homebrew
brew install hugo

For other operating systems, see the official Hugo installation documentation: https://gohugo.io/installation/

Important: Make sure to install the extended version of Hugo if you plan to use SCSS/Sass.

2. Create a New Site

# Create new Hugo site - this will generate the folder structure
hugo new site secondbrain
cd secondbrain

# Initialize git repository
git init

When you run hugo new site, Hugo automatically creates the folder structure that you’ll push to GitHub and use to build the Docker image.

3. Add a Theme (Using Hugo Modules)

# Initialize your site as a Hugo module
hugo mod init github.com/yourusername/myblog

# Add the hugo-blog-awesome theme as a module
hugo mod get github.com/hugo-sid/hugo-blog-awesome

# Configure the theme in hugo.toml
cat >> hugo.toml << 'EOF'
[module]
  [[module.imports]]
    path = "github.com/hugo-sid/hugo-blog-awesome"
EOF

The hugo-blog-awesome theme is a fast, clean, and responsive theme with dark mode support and excellent PageSpeed scores.

4. Project Structure

myblog/
โ”œโ”€โ”€ archetypes/       # Content templates
โ”œโ”€โ”€ assets/           # Assets to be processed by Hugo Pipes
โ”œโ”€โ”€ content/          # All content for your website
โ”œโ”€โ”€ data/            # Configuration files
โ”œโ”€โ”€ layouts/         # Templates in .html format
โ”œโ”€โ”€ public/          # Generated static site (gitignored)
โ”œโ”€โ”€ static/          # Static files (images, CSS, JS)
โ”œโ”€โ”€ themes/          # Hugo themes
โ””โ”€โ”€ hugo.toml        # Site configuration

5. Basic Configuration

Edit hugo.toml (my actual configuration):

baseURL = 'https://example.org/'
languageCode = 'en-us'
title = 'My New Hugo Site'
ignoreLogs = ['shortcode-x-getremote']

[module]
  [[module.imports]]
    path = "github.com/hugo-sid/hugo-blog-awesome"

You’ll want to update the baseURL to match your domain.

Writing Content

Creating Your First Post

# Create a new post
hugo new posts/my-first-post.md

This creates a file with front matter:

---
title: "My First Post"
date: 2025-08-07T12:00:00-04:00
draft: false
description: "This is my first blog post"
tags: ["introduction", "hugo"]
categories: ["general"]
---

## Introduction

Welcome to my blog! This is my first post using Hugo.

### Key Features

- Easy to write in Markdown
- Fast static site generation
- SEO friendly

Content Organization

content/
โ”œโ”€โ”€ _index.md         # Homepage content
โ”œโ”€โ”€ posts/           # Blog posts
โ”‚   โ”œโ”€โ”€ _index.md   # Posts section page
โ”‚   โ””โ”€โ”€ *.md        # Individual posts
โ”œโ”€โ”€ pages/          # Static pages
โ”‚   โ”œโ”€โ”€ about.md
โ”‚   โ””โ”€โ”€ projects.md
โ””โ”€โ”€ images/         # Image assets

Building and Running Locally

After creating your content:

# Build the static site
hugo

# Run Hugo server locally
hugo server

# Or run with drafts visible
hugo server -D

Visit http://localhost:1313 to see your site running locally. Hugo will automatically reload when you make changes.

Building the Docker Image

1. Create Multi-Stage Dockerfile

FROM alpine:latest AS builder

# Install system dependencies
RUN apk add --update \
    wget \
    git \
    go \
    npm \
    libc6-compat \
    libstdc++

# Install Dart Sass
RUN npm install -g sass

# Install Hugo Extended - handle multi-arch
ARG HUGO_VERSION="0.148.2"
ARG TARGETARCH
RUN HUGO_ARCH="${TARGETARCH}" && \
    if [ "${TARGETARCH}" = "arm64" ]; then HUGO_ARCH="ARM64"; fi && \
    if [ "${TARGETARCH}" = "amd64" ]; then HUGO_ARCH="64bit"; fi && \
    echo "Downloading Hugo for architecture: ${HUGO_ARCH}" && \
    wget "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_Linux-${HUGO_ARCH}.tar.gz" && \
    tar xzf hugo_extended_${HUGO_VERSION}_Linux-${HUGO_ARCH}.tar.gz && \
    chmod +x hugo && \
    mv hugo /usr/bin/hugo && \
    rm hugo_extended_${HUGO_VERSION}_Linux-${HUGO_ARCH}.tar.gz && \
    /usr/bin/hugo version

# Copy the secondbrain site
COPY secondbrain /site
WORKDIR /site

# Build the site
RUN /usr/bin/hugo

FROM nginx:alpine
# Remove default nginx static assets
RUN rm -rf /usr/share/nginx/html/*
# Copy Hugo generated files
COPY --from=builder /site/public /usr/share/nginx/html
# Copy nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

2. Create nginx.conf

The nginx configuration is needed to properly serve the static Hugo site with security headers, caching, and custom 404 handling:

server {
    listen 80;
    server_name blog.landryzetam.net;
    root /usr/share/nginx/html;
    index index.html;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "no-referrer-when-downgrade" always;

    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;

    # Cache static assets
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|ttf|svg)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Try to serve request as file, then as directory, then fall back to 404
    location / {
        try_files $uri $uri/ /404.html;
    }

    # Custom 404 page
    error_page 404 /404.html;
    location = /404.html {
        internal;
    }
}

3. Create .dockerignore

# Hugo build artifacts
secondbrain/public/
secondbrain/resources/

# Development files
secondbrain/.hugo_build.lock
secondbrain/hugo-blog-awesome.work

# Git
.git/
.gitignore

# Documentation
*.md
!secondbrain/content/**/*.md

# OS files
.DS_Store
Thumbs.db

4. Build the Image

# Build with tag
docker build -t lzetam/hugo:latest .

# Build with version tag
docker build -t lzetam/hugo:v1.0.0 .

# Build for multiple architectures
docker buildx build --platform linux/amd64,linux/arm64 \
  -t lzetam/hugo:latest .

GitHub Actions CI/CD

Docker Hub credentials are stored in GitHub Secrets, which allows the GitHub Actions workflow to automatically build and push images when changes are made.

Docker Hub Tokens

For a private repository like mine, you’ll need two tokens:

  1. GitHub Actions Token: With read and write permissions for pushing images
  2. Kubernetes Token: With read-only permissions for pulling images

Setting up GitHub Secrets

In your GitHub repository settings, add these secrets:

  • DOCKERHUB_USERNAME: Your Docker Hub username
  • DOCKERHUB_TOKEN: Your Docker Hub access token (with read/write permissions)

GitHub Actions Workflow

Create .github/workflows/hugo-build.yaml:

name: Build & Publish hugo Image

on:
  push:
    paths:
      - 'Dockerfile'
      - 'nginx.conf'
      - 'secondbrain/**'
      - '.github/workflows/hugo-build.yaml'
    branches: [main]
  workflow_dispatch:

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3
        with:
          platforms: all

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
        with:
          driver-opts: |
            image=moby/buildkit:latest

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build and push hugo
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ./Dockerfile
          platforms: linux/amd64,linux/arm64
          push: true
          no-cache: true
          pull: true
          tags: |
            ${{ secrets.DOCKERHUB_USERNAME }}/hugo:latest
            ${{ secrets.DOCKERHUB_USERNAME }}/hugo:${{ github.sha }}
          build-args: |
            BUILDKIT_INLINE_CACHE=0

This workflow:

  • Triggers on changes to Hugo-related files
  • Builds for both amd64 and arm64 architectures
  • Pushes to Docker Hub using stored credentials
  • Tags with both latest and commit SHA

Deploying to Kubernetes

Overview

The blog namespace hosts our Hugo-based static website served through Nginx. The blog is built into a Docker image and exposed to the internet via Cloudflare Tunnel.

Components

1. Hugo Blog Deployment

  • Image: lzetam/hugo:latest
  • Replicas: 1
  • Port: 80 (HTTP)
  • Resources:
    • Requests: 128Mi memory, 100m CPU
    • Limits: 256Mi memory, 500m CPU
  • Health Checks:
    • Liveness probe: HTTP GET / (30s initial delay, 10s period)
    • Readiness probe: HTTP GET / (20s initial delay, 5s period)

2. Service

  • Name: hugo-blog
  • Type: ClusterIP
  • Port: 80 โ†’ HTTP

3. Cloudflare Tunnel

  • Deployment: cloudflared
  • Replicas: 2 (for high availability)
  • Resources:
    • Requests: 128Mi memory, 250m CPU
    • Limits: 256Mi memory, 500m CPU

Secrets Management

Secrets

  1. dockerhub-registry: Docker Hub credentials for pulling private images
  2. tunnel-credentials: Cloudflare tunnel authentication credentials

Deployment Structure

Base Configuration (apps/base/blog/)

โ”œโ”€โ”€ namespace.yaml      # Namespace definition
โ”œโ”€โ”€ deployment.yaml     # Hugo blog deployment
โ”œโ”€โ”€ service.yaml        # ClusterIP service
โ”œโ”€โ”€ ingress.yaml        # Local ingress configuration
โ””โ”€โ”€ kustomization.yaml  # Kustomize configuration

Staging Overlay (apps/staging/blog/)

โ”œโ”€โ”€ cloudflare.yaml                      # Cloudflare tunnel deployment & config
โ”œโ”€โ”€ cloudflare-secret.yaml               # External secret for tunnel credentials
โ”œโ”€โ”€ dockerhub-secret.yaml                # External secret for Docker Hub
โ”œโ”€โ”€ create-docker-registry-secret-job.yaml # Job to create registry secret
โ””โ”€โ”€ kustomization.yaml                   # Kustomize overlay

Setting up External Access

1. Create Subdomain in Cloudflare

  • Add a CNAME record for your subdomain (e.g., blog.landryzetam.net)
  • Point it to your tunnel UUID

2. Create Cloudflare Tunnel

cloudflared tunnel create blog-tunnel

3. Encrypt Credentials with SOPS

SOPS (Secrets OPerationS) provides secure encryption for your secrets. In this context, we encrypt secrets before committing them to Git. When applied to the cluster via GitOps, they are automatically decrypted within the cluster by the External Secrets Operator.

# Encrypt Cloudflare tunnel credentials
sops -e tunnel-credentials.json > tunnel-credentials.enc.json

# Encrypt Docker Hub credentials
sops -e dockerhub-credentials.yaml > dockerhub-credentials.enc.yaml

Docker Hub Registry Secret

For pulling from a private repository:

apiVersion: v1
kind: Secret
metadata:
  name: dockerhub-registry
  namespace: blog
type: kubernetes.io/dockerconfigjson
data:
  .dockerconfigjson: <base64-encoded-docker-config>

Note: Use the read-only Docker Hub token for Kubernetes to follow the principle of least privilege.

GitOps Deployment

For detailed Kubernetes manifests and GitOps configuration, see the Kubernetes deployment documentation in the infrastructure repository.

Maintenance

Updating the Blog Content

The blog content is baked into the Docker image. To update:

  1. Update the Hugo site source code and commit to GitHub
  2. GitHub Actions will automatically build and push the new Docker image
  3. The deployment will be automatically updated via GitOps (FluxCD) when the new image is available

If you need to manually restart the deployment:

kubectl rollout restart deployment/hugo-blog -n blog

Conclusion

This guide provides a high-level overview of the complete workflow from creating a Hugo blog to deploying it in a production Kubernetes environment. The steps presented here are intentionally simplified to provide a clear understanding of the overall process.

The setup provides:

  • Fast Static Site: Hugo’s speed for content generation
  • Containerization: Consistent deployments with Docker
  • GitOps Ready: Infrastructure as code approach
  • High Availability: Cloudflare tunnel with multiple replicas
  • Security: Best practices for production deployments

Note that these steps are oversimplified and intended for just a high-level flow. Remember to adapt the configurations to your specific requirements and always test thoroughly before deploying to production.


๐Ÿ“š More Projects

Infrastructure

AI & Machine Learning

Health & Analytics