Skip to main content

Setting Up CI/CD for My Hugo Site with GitHub Actions, Docker, and a Self-Hosted Harbor Registry

·688 words·4 mins
Projects Hugo Infrastructure Blogs DevOps

A guide on how I automated Hugo site deployment to my VPS using GitHub Actions, Docker, and Harbor.

Pipeline Diagram

✨ Overview
#

In this post, I’ll walk you through how I configured a complete deployment pipeline for my Hugo website (noorraihan.com) using:

  • GitHub Actions to build and push the Docker image
  • Self-hosted Harbor as a private image registry
  • Docker for containerization
  • NGINX for reverse proxy
  • VPS (Virtual Private Server) as the host

πŸ› οΈ Prerequisites
#

  • A running VPS with root access
  • Docker and Docker Compose installed on VPS
  • Self-hosted Harbor registry
  • Domain name (e.g., noorraihan.com)
  • GitHub repo with Hugo source code
  • GitHub Secrets configured

πŸ“‚ Project Structure
#

Here’s a simple project layout:

.
β”œβ”€β”€ .github/
β”‚   └── workflows/
β”‚       └── deploy.yml
β”œβ”€β”€ Dockerfile
β”œβ”€β”€ config.toml
└── content/

βš™οΈ Step 1: Create a Dockerfile for Hugo
#

# Stage 1
FROM alpine:latest AS build

# Install the Hugo go app.
RUN apk add --update hugo
RUN apk add --update git

WORKDIR /opt/HugoApp

# Copy Hugo config into the container Workdir.
COPY . .

RUN git init
RUN git submodule update --init --recursive

# Run Hugo in the Workdir to generate HTML.
RUN hugo 

# Stage 2
FROM nginx:1.25-alpine

# Set workdir to the NGINX default dir.
WORKDIR /usr/share/nginx/html

# Copy HTML from previous build into the Workdir.
COPY --from=build /opt/HugoApp/public .

# Expose port 80
EXPOSE 80/tcp

βš™οΈ Step 2: Setup GitHub Action Workflow
#

In .github/workflows/deploy.yml:

name: Docker

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

on:
  push:
    branches:
      - '**'

jobs:
  build:

    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      id-token: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0

      - name: Log into registry
        id: registry-login
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} ${{ secrets.DOCKER_REGISTRY }}          

      - name: Build and push Docker image
        id: build-and-push
        run: |
          docker build . -t "${{ secrets.DOCKER_REGISTRY }}/yourproject/noorraihan-blog:${{ github.ref_name }}"
          docker push "${{ secrets.DOCKER_REGISTRY }}/yourproject/iamgename:${{ github.ref_name }}"          

πŸ” GitHub Secrets to Set
#

  • DOCKER_REGISTRY = your-vps-domain.com:port
  • DOCKER_USERNAME = admin
  • DOCKER_PASSWORD = your_password

πŸ“¦ Step 3: Setup a Webhook Deployer
#

You can refer the step on how to deploy or run my webhook deployer from my repository or you can create your own webhook listener

Follow Webhook_Deployer installation guide for full configuration: https://github.com/NoorRaihan/webhook_deployer

Follow Harbor installation guide for full configuration: https://goharbor.io/docs

Once the webhook is configured, setup the webhook endpoint in the harbor UI.

alt text
#

🐳 Step 4: Deploy Harbor Image on VPS
#

Create a bash script to be executed by the webhook deployer.

#!/bin/bash


#read -p "Enter image tag (e.g., v1.0.0): " IMAGE_TAG

IMAGE_TAG="$1"

# Validate input
if [ -z "$IMAGE_TAG" ]; then
    echo "Error: Image tag is required."
    exit 1
fi

IMAGE="yourregistry/yourporject/myhugosite:$IMAGE_TAG"
CONTAINER_NAME="hugo-site"

# Check if container is running
if [ "$(docker ps -q -f name=^/${CONTAINER_NAME}$)" ]; then
	  echo "Stopping running container $CONTAINER_NAME..."
	    docker stop $CONTAINER_NAME
fi

# Check if container exists (running or stopped) and remove it
if [ "$(docker ps -aq -f name=^/${CONTAINER_NAME}$)" ]; then
	  echo "Removing existing container $CONTAINER_NAME..."
	    docker rm $CONTAINER_NAME
fi

# Pull the latest image
echo "Pulling latest image $IMAGE..."
docker pull $IMAGE

# Run the container (customize options as needed)
echo "Starting new container $CONTAINER_NAME..."
docker run -d -p8080:80 --name $CONTAINER_NAME $IMAGE

Alternative way is you can:

  • Manually pull and run the image on the VPS:

    docker pull yourregistry/my-hugo-site:latest
    docker run -d --name hugo-site -p 8080:80 my-hugo-site:latest
    
  • Or set up a watcher script.


🌐 Step 5: Configure NGINX as Reverse Proxy
#

Example NGINX config on VPS (/etc/nginx/sites-available/hugo):

server {
    listen 80;
    server_name mydomain.com www.mydomain.com;

    location / {
        proxy_pass http://localhost:8080;
    }
}

Then enable the site and restart:

ln -s /etc/nginx/sites-available/hugo /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

βœ… Final Result
#

Now whenever you push to your main branch:

alt text

  1. GitHub Action builds your Hugo site
  2. Docker image is pushed to your private Harbor registry
  3. Image can be pulled and deployed on your VPS
  4. NGINX serves the site to the web