Skip to main content

My HUGO Setup

·1152 words·6 mins

My own website… but why?
#

I’ve always liked the idea of owning a small corner of the internet — something that’s truly mine. Not another social media profile, but a personal space where I can share my thoughts, projects, and experiments without worrying about algorithms or platforms disappearing.

I didn’t want to rely on any website builders. Instead, I wanted to learn how it actually works — from static site generation to server deployment.


Choosing Hugo
#

After trying a few different options (WordPress, various online website builder, plain HTML), I found out about Hugo. Hugo is a static site generator written in Go. It’s extremely fast (it just generates the HTML, JS, CSS and so on), simple to set up, and gives you full control over your site’s structure. There are also a lot of themes the choose from (you have to get a theme, to get hugo working).

With Hugo, I can write posts in Markdown (very easy to learn) and keep everything versioned in Git.

My workflow now looks like this:

  1. Write a new post in Markdown.
  2. Run hugo server -D locally to test the layout.
  3. Push the changes to GitHub.
  4. Pull changes from GitHub on my server.
  5. Publish to site.

Automatic Deployment — The Fun Part
#

Of course, I didn’t want to manually upload/sync files every time I updated the blog. So I built a small autodeployment system using Docker, Flask, and Nginx — completely self-hosted on my home server.

Here’s what happens under the hood:

  1. GitHub Webhook
    Whenever I push to my repository (specifically to the deploy branch), GitHub sends a webhook to my server.

  2. Webhook Listener (Flask App)
    A small Flask app (writen in python) waits for webhooks (inside Docker). It receives the webhook and automatically pulls the latest commit from GitHub using an SSH key stored securely in the container. The files are cloned into a shared Docker volume.

  3. Nginx Server
    Another container runs Nginx. It serves static files directly from the same volume. That means whenever the webhook updates the repo, the site instantly updates — no manual steps.

The simplified docker file structure looks like this:

autodeployer/
├── docker-compose.yml
├── nginx/
│   └── default.conf
├── secrets/
│   ├── deploy_key
│   └── deploy_key.pub
└── webhook/
    ├── app.py
    ├── Dockerfile
    ├── requirements.txt
    └── ssh_config

The two Docker services work together:

  • webhook-listener → pulls the newest version of my Hugo site from GitHub
  • nginx-server → serves the static files at http://localhost

Debugging Adventures
#

But of course there had to be some debugging and fixing to be done.

At first, SSH kept rejecting my deploy key with:

WARNING: UNPROTECTED PRIVATE KEY FILE!
Permissions 0777 for '/root/.ssh/id_rsa' are too open.

Turns out, Docker mounts preserve host permissions — and SSH is very picky.
The fix was to set the key to chmod 600 on the host, or copy it inside the container with the correct permissions before running git pull.

Then, Nginx complained about:

can not modify /etc/nginx/conf.d/default.conf (read-only file system?)

That one looked scary but was harmless — it just meant I had mounted my custom Nginx config as read-only. Everything still worked fine.

Finally, if Nginx starts before the webhook finishes cloning the repo, the folder is blank — leading to the infamous:

curl: (52) Empty reply from server

That was an easy fix: make a pull before the server starts!


Local Deployment Script
#

Before my server can pull the newest build via the webhook, I need to prepare and push the latest version of my website locally.
To avoid typing the same series of commands every time, I wrote a small Python automation script that takes care of everything — from building the site to pushing the deployment branch.
The process in inspired by the implementation of Networkchuck’s pipeline.

import subprocess
import sys
import getpass
import socket
from datetime import datetime

def run_command(command, success_msg=None, error_msg=None):
    try:
        print(f"\nExecute: {' '.join(command)}")
        result = subprocess.run(command, check=True, text=True, capture_output=True)
        if result.stdout:
            print(result.stdout)
        if success_msg:
            print(f"Execute Success: {success_msg}")
    except subprocess.CalledProcessError as e:
        print(f"{error_msg or 'Error at execution'}")
        print(f"Error code: {e.returncode}")
        if e.stderr:
            print("Details:", e.stderr.strip())
        sys.exit(1)
    except Exception as e:
        print(f"Unexpected error: {e}")
        sys.exit(1)


def main():
    print("Starting deployment process...\n")

    # user, hostname, date/time, message
    user = getpass.getuser()
    hostname = socket.gethostname()
    current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    commit_message = f"{user}@{hostname}: Update {current_time}"

    # 1. hugo --gc --minify
    run_command(
        ["hugo", "--gc", "--minify"],
        success_msg="Hugo build OK",
        error_msg="Hugo build FAILED"
    )

    # 2. git add .
    run_command(
        ["git", "add", "."],
        success_msg="Changes added OK",
        error_msg="Changes added FAILED"
    )

    # 3. git commit -m "{user}@{hostname}: Update {current time + date}"
    run_command(
        ["git", "commit", "-m", commit_message],
        success_msg=f"Commit OK: '{commit_message}'",
        error_msg="Commit FAILED"
    )

    # 4. git push origin master
    run_command(
        ["git", "push", "origin", "master"],
        success_msg="Push to master OK",
        error_msg="Push to master FAILED"
    )

    # 5. git subtree split --prefix public -b deploy
    run_command(
        ["git", "subtree", "split", "--prefix", "public", "-b", "deploy"],
        success_msg="Subtree split deploy OK",
        error_msg="Subtree split deploy FAILED"
    )

    # 6. git push origin deploy:deploy --force
    run_command(
        ["git", "push", "origin", "deploy:deploy", "--force"],
        success_msg="Branch deploy push OK",
        error_msg="Branch deploy push FAILED"
    )

    # 7. git branch -D deploy
    run_command(
        ["git", "branch", "-D", "deploy"],
        success_msg="Local branch deploy deleted OK",
        error_msg="Local branch deploy deleted FAILED"
    )

    print("\nDeployment successful!")


if __name__ == "__main__":
    main()

How it works
#

This script bundles all the manual deployment steps into one repeatable process:

  1. Build the sitehugo --gc --minify
  2. Add new changesgit add .
  3. Commit automatically → adds a message like
    user@hostname: Update 2001-01-01 12:45:30
  4. Push to master → keeps the source files updated
  5. Create the subtree branch → extracts only the compiled files from public/
  6. Push the deployment branch → triggers the webhook on the server
  7. Clean up → deletes the temporary deploy branch locally

Why again?
#

  • Automates seven manual commands
  • Prints success/error messages
  • I don’t have to come up with commit messages
  • My autodeployer automation handles everything else

After writing a post/site, it’s up in seconds (hopefully):

python3 deploy.py

And just because I am lazy:

alias deployblog="python3 ~/projects/blog/deploy.py"

Let the magic happen:

deployblog

The blog builds, commits, and updates itself automatically! (if nothing goes worng, but who knows…)


Learnings form this adventure
#

This small project taught me a lot:

  • Automation is truly addicting — pushing a commit and watching your site instantly update is very satisfying!
  • My first time to create my own docker container
  • Better understanding of UNIX stuff.
  • Nginx is powerful but minimal: It just works.
  • I could have done it with GitHub pages, but that would be to easy, wouldn’t it?

I’ll stop procrastinating… tomorrow.
#

Next, I’ll probably:

  • Set up a proper repo for my autodeployer container und python script.
  • Configure my web site.
  • Maybe even write a detailed documentation.

But for now — it works, it’s fast, it’s mine, and I learned a lot building it.
That’s all I wanted from this project.


Thanks for reading — if you’re thinking about building your own website, I highly recommend giving Hugo a try.

Erik Tóth
Author
Erik Tóth
Passion for technology and nature keeps my current flowing.