Skip to main content
  1. Posts/

Deploying a Website with Hugo, Docker, Caddy, and GitHub Actions

·5 mins
Development Tutorial Hugo Blowfish Docker Caddy GitOps
Bibin Babuji
Author
Bibin Babuji

Why?
#

There are countless content management systems that make building and deploying a website easy, often with minimal effort. However, most of them are bloated with features I’ll never use. I wanted something simple and lightweight. Just a personal website I could host on my own VPS.

Tools Used
#

  • Hugo: The star of the show. Hugo is a fast and lightweight static site generator written in Go. It uses Markdown files to build the entire site, making it simple to create posts.

  • Docker: I prefer to use containerized services whenever possible. Here, Docker is used to run our web server in a clean and reproducible environment.

  • Caddy: An open-source web server known for its simplicity and power. Caddy automatically fetches and renews SSL certificates, making HTTPS setup effortless. We use Caddy inside a Docker container to serve our static website and act as a reverse proxy.

  • GitHub Actions: Manually building and copying site files quickly became tedious. So, I leveraged GitHub to manage my source code and set up a CI/CD pipeline using GitHub Actions. Now, every time I push to the main branch, the site is automatically built and deployed.

Let’s get started
#

Build a Hugo Website
#

We are going to build a very basic website for this demonstration. Run the below commands to create a Hugo site with the Blowfish theme.

hugo new site quickstart
cd quickstart
git init
git submodule add -b main https://github.com/nunocoracao/blowfish.git themes/blowfish
mkdir -p config/_default
rm hugo.toml
cp themes/blowfish/config/_default/*.toml config/_default/
sed -i '/^\s*#\s*theme\s*=\s*"blowfish"/s/^#\s*//' config/_default/hugo.toml
hugo server

Now open your browser and navigate to localhost:1313 to see your newly created Hugo Site.

Add Content
#

Let’s add a new page to the website.

hugo new content content/posts/example.md

This will create a markdown file in the content/posts directory. Replace the contents of the file with the below lines:

---
title: "Example Post Title"
date: 2025-07-22T21:19:29Z
draft: false
---

Lorem Ipsum

Save the file and start the Hugo server to view your progress.

Read the Blowfish documentation to understand the configuration files and to customise the website.

For example,

baseURL = 'https://example.org/'
title = 'Example Site Title'

When you are satisfied with the content, run the following command to create the static site, which will generate all the necessary files and will place it in the public directory.

hugo

Deploy Caddy with Docker
#

For this setup, I assume you’re working on a VPS with Docker installed, a public IP address, and a domain name pointing to that IP.

  • If Docker isn’t installed yet, follow the official Docker installation guide to install Docker Engine on the VPS or the machine where you are hosting the website from.
  • Most cloud providers offer free trials or always-free tiers that are great for hosting a small personal site.
  • For the domain, use a reputable registrar (I use Porkbun) and create an A record that points to the VPS’s public IP.

Login to the VPS (I’m running Debian on my VPS) and execute the below commands.

mkdir caddy
cd caddy
nano docker-compose.yml

Paste the following to the docker-compose.yml file and press ctrl + x to save the file and exit.

services:
  caddy:
    image: caddy:latest
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./conf:/etc/caddy
      - ./site:/srv
      - caddy_data:/data
      - caddy_config:/config

volumes:
  caddy_data:
  caddy_config:

Once done create the Caddyfile

nano Caddyfile

and paste the below

example.com {
        root * /srv
        file_server
        handle_errors {
                @404 {
                        expression {http.error.status_code} == 404
                }
                rewrite @404 /404.html
                file_server
        }
}

The Caddyfile contains the configuration for the web server and here we are instructing it to serve the static files from the srv directory. Note that the srv directory in the Docker container is bind-mounted to the site directory in our local filesystem. So we need to put our static site files to the site folder.

Start the container by typing

docker compose up -d

Verify that the container is running by typing

docker ps

GitOps with Github Actions
#

I assume here that you are familiar with git and that you have created the repository and configured necessary variables as well.

In order to create a continuous deployment pipeline we are going to create a workflow. To do that create the below file in your root Hugo site directory.

.github/workflows/example_flow_name.yml

Replace the contents of the file with the following.

name: Deploy Hugo site to VPS

on:
  # Runs on pushes targeting the default branch
  push:
    branches:
      - main

# Default to bash
defaults:
  run:
    shell: bash

jobs:
  # Build job
  build:
    runs-on: ubuntu-latest
    env:
      DART_SASS_VERSION: 1.89.2
      HUGO_VERSION: 0.148.0
      HUGO_ENVIRONMENT: production
      TZ: Europe/Dublin
    steps:
      - name: Install Hugo CLI
        run: |
          wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb
          sudo dpkg -i ${{ runner.temp }}/hugo.deb
      - name: Install Dart Sass
        run: |
          wget -O ${{ runner.temp }}/dart-sass.tar.gz https://github.com/sass/dart-sass/releases/download/${DART_SASS_VERSION}/dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz
          tar -xf ${{ runner.temp }}/dart-sass.tar.gz --directory ${{ runner.temp }}
          mv ${{ runner.temp }}/dart-sass/ /usr/local/bin
          echo "/usr/local/bin/dart-sass" >> $GITHUB_PATH
      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: recursive
          fetch-depth: 0
      - name: Install Node.js dependencies
        run: "[[ -f package-lock.json || -f npm-shrinkwrap.json ]] && npm ci || true"
      - name: Build with Hugo
        run: |
          hugo \
            --gc \
            --minify

  # Deployment job - working config
      - name: Deploy to server via rsync
        uses: appleboy/scp-action@v1
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          password: ${{ secrets.PASSWORD }}
          port: ${{ secrets.PORT }}
          source: ./public/*
          target: /home/user/caddy/site
          strip_components: 1
Note: Please make sure to have the host details as repository secrets. For better security, use SSH keys.

The next time you push code to your main branch, this workflow will be triggered. It will use a GitHub Runner to build the Hugo site and the static site files are then copied over to the VPS using SCP.

You can visit your repository and take a look at the Actions tab to see the progress of the workflow.

If everything went correctly, you can visit example.com and your website will be visible.

Conclusion
#

If you followed along, by this time you should have a simple website which is hosted publicly. You can configure the website as you please later on.

I hope this guide helped you. Please feel free to share your feedback.