From GitHub Notes to Production Blog
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
- Converting local notes - Migrating content from GitHub markdown
- Setting up Hugo locally - Getting a working blog without frontend knowledge
- Containerizing with Docker - Making deployment reproducible
- Automating with GitHub Actions - Push code, get blog updates
- Production deployment on Kubernetes - Professional hosting with GitOps
- Roadmap
- 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
- Get it working locally first - Set up Hugo, add content, and test with
hugo server - Build a Docker image - Containerize your working site
- Deploy to Kubernetes - Run the containerized site in your K8s cluster
Table of Contents
- Introduction to Hugo
- Setting Up Your Hugo Blog
- Writing Content
- Building the Docker Image
- GitHub Actions CI/CD
- Deploying to Kubernetes
- 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:
- GitHub Actions Token: With read and write permissions for pushing images
- 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 usernameDOCKERHUB_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
latestand 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
- dockerhub-registry: Docker Hub credentials for pulling private images
- 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:
- Update the Hugo site source code and commit to GitHub
- GitHub Actions will automatically build and push the new Docker image
- 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.