Setting Up My Hugo Blog: From Tilt.dev to… Just Hugo

Sunday afternoon continues. After documenting my Git multi-identity setup, I decided to actually set up my blog development environment on the new Nucbox K6.

This should have been straightforward. It wasn’t. But the journey from overengineered to simple was worth documenting.

The Existing Setup

My blog is a Hugo static site hosted on Netlify. The repository already had a development environment setup:

  • Docker for containerization
  • Tilt.dev for live reload during development
  • Rancher Desktop instead of Docker Desktop (because enterprise)

Here’s what the setup looked like:

Dockerfile:

FROM floryn90/hugo:0.147.0-ext-alpine
RUN git config --global --add safe.directory /src
COPY --chown=hugo . /src
RUN hugo mod tidy

docker-compose.yml:

version: "3.3"

services:
  blog:
    image: leeshan/blog
    build:
      context: .
    command: server
    ports:
      - "1313:1313"

Tiltfile:

docker_compose("docker-compose.yml")
docker_build(
    'leeshan/blog',
    context='.',
    live_update=[
        sync('./','/src/'),
    ]
)

This setup exists because I had a philosophy: “Don’t install anything on the host machine.”

As a security software engineer, I like keeping the host clean. Everything runs in containers. Dependencies are isolated. Teardown is simple - just delete the container.

For this blog, I used Chocolatey (trusted package source) to install only the essential tools:

  • Tilt.dev
  • Rancher Desktop

The blog itself would run entirely in Docker.

The Setup Process

Installing the tools was straightforward:

choco install -y tilt rancher-desktop

Tilt installed fine:

tilt version v0.35.2

Rancher Desktop needed manual startup (GUI application), but once running, Docker was available.

Then I navigated to my blog repository and ran:

tilt up

First Problem: VSCode Terminal PATH Refresh

Windows doesn’t refresh the PATH in existing terminal sessions. Even creating a new terminal session in VSCode didn’t help - VSCode itself needs to be restarted to pick up new PATH entries.

This is a classic Windows/VSCode issue. Not critical, just annoying.

Second Problem: Docker Credential Helper

Tilt started building the Docker image and immediately failed:

ERROR: ImageBuild: failed to resolve source metadata for docker.io/floryn90/hugo:0.147.0-ext-alpine:
error getting credentials - err: exec: "docker-credential-wincred": executable file not found in %PATH%

The docker-credential-wincred helper is typically included with Docker Desktop, but Rancher Desktop uses a different credential management approach.

After opening a fresh PowerShell window (outside VSCode), the credential issue resolved itself. Tilt successfully built the Hugo container.

Success… Sort Of

After about 40 seconds, the build completed:

STEP 1/2 — Building Dockerfile: [leeshan/blog]
  Step 1 - 39.48s (Building Dockerfile)
  Step 2 - 1.05s (Deploying)
  DONE IN: 40.53s

Built in 260 ms
Environment: "DEV"
Web Server is available at http://localhost:1313/

The blog was running. Tilt’s web UI (http://localhost:10350/) showed everything green:

Tilt Web UI showing successful build

The blog worked perfectly:

Blog running on localhost:1313

But as I sat there looking at the Tilt UI, a thought occurred to me.

The Realization

I originally created this Docker + Tilt setup because I didn’t want to install development tools on my host machine.

But here’s what was already installed on my host:

  • Node.js - Required for Claude Code (the AI coding assistant I’m using)
  • Go - Installed because I love Go for building cross-platform executables

And here’s what Hugo is:

  • A Go-based static site generator
  • No runtime dependencies
  • Single cross-platform executable

So I had spent time setting up:

  • Rancher Desktop (Kubernetes distribution)
  • Docker (containerization)
  • Tilt.dev (live reload orchestration)
  • A custom Dockerfile
  • A docker-compose configuration

…to run a tool that could have been installed as a single executable.

The Simplification

I created a backup branch to preserve the Tilt/Docker setup:

git checkout -b site-backup-2025-10-12-tilt-docker-compose
git push -u origin site-backup-2025-10-12-tilt-docker-compose

Then switched back to main and deleted everything:

git checkout main
rm Tiltfile docker-compose.yml Dockerfile

Installed Hugo directly via Chocolatey:

choco install hugo-extended -y

Hugo 0.151.0 installed successfully:

hugo v0.151.0-c70ab27ceb841fc9404eab5d2c985ff7595034b7+extended windows/amd64

Started the development server:

hugo server

Result:

Built in 841 ms
Environment: "development"
Web Server is available at http://localhost:1313/

Same blog. Same functionality. 841ms build vs 40+ seconds.

No Docker. No Tilt. No Kubernetes. Just Hugo.

Why The Original Setup Made Sense (At The Time)

The Tilt.dev + Docker Compose setup wasn’t entirely pointless. Here’s why it existed:

Tilt.dev as a Learning Tool

Tilt.dev is designed to bridge the gap between local Docker Compose development and Kubernetes deployments. It provides:

  • Live reload for containerized applications
  • Unified interface for managing multiple services
  • A stepping stone toward Kubernetes without the full complexity

My blog was a simple example demonstrating that Tilt works with plain docker-compose - you don’t need Kubernetes to benefit from Tilt’s developer experience improvements.

For projects with multiple services (backend, frontend, database, cache), Tilt’s live reload and orchestration are genuinely useful.

The “Don’t Install On Host” Philosophy

For languages with complex dependency management (Node.js, Python, Ruby), containerization makes sense:

  • Avoid version conflicts between projects
  • Keep the host system clean
  • Ensure consistent environments across machines

But Hugo is a single Go binary with zero runtime dependencies. The philosophy doesn’t apply here.

What This Gives You

Before:

  • Install Rancher Desktop (~500MB+)
  • Install Tilt.dev
  • Wait for Docker daemon to start
  • Run tilt up
  • Wait 40+ seconds for container build
  • Navigate Tilt web UI at http://localhost:10350/
  • Access blog at http://localhost:1313/

After:

  • Install Hugo (~90MB)
  • Run hugo server
  • Blog available at http://localhost:1313/ in under a second

Lessons Learned

Question your assumptions. I built the Docker setup when Node.js and Go weren’t installed on my machine. The requirements changed, but I didn’t revisit the architecture.

Simpler isn’t always worse. The native Hugo setup is faster, uses less memory, and requires fewer moving parts. For a single-service static site, this is objectively better.

Overengineering has value as a learning exercise. The Tilt setup taught me how Tilt works with docker-compose, which is useful knowledge for actual multi-service projects. But for production use on this blog? Unnecessary.

Keep the escape hatch. I didn’t delete the old setup - it’s preserved in a Git branch. If I ever need to demonstrate Tilt.dev or containerized Hugo for some reason, it’s still there.

The Meta Observation

I’m writing a blog post about setting up my blog development environment so I could write blog posts.

This is recursive documentation, and I’m okay with that.

Conclusion

Sometimes the right tool is the simplest one. Hugo is a static site generator designed to be a standalone binary. Running it in Docker added complexity without meaningful benefits.

The Tilt + Docker setup made sense in a different context - multiple services, complex dependencies, or demonstrating containerized workflows. For a single Hugo site on a machine that already has Go and Node.js installed? Just run Hugo.

Now I can actually write blog posts instead of configuring the environment to write them in.

Which was the entire point.


Sunday afternoon: successfully overengineered, then successfully simplified. Documentation complete.