Building michaelbrant.com with Jekyll and Claude Code
A static Jekyll site, a Linux sandbox, Claude Code as the build agent. The whole pipeline.
This site is the build I’m working on right now. It runs on Jekyll, hosted on a personal AlmaLinux sandbox, deployed via Docker, with Claude Code doing most of the actual file editing.
The architecture
The site lives in three folders, each with one job.
/home/almalinux/Sites/michaelbrant.com/ is the Jekyll source — markdown content, Sass partials, Liquid layouts, the lot. This is the only folder I git-commit. It’s also the only folder Claude Code edits.
/opt/stacks/michaelbrant/ is the runtime stack — Dockerfile, docker-compose.yml, nginx.conf. The compose file mounts the source folder as the build context, runs bundle exec jekyll build inside a multi-stage container, and serves the resulting _site/ via nginx behind Traefik. This folder is intentionally separate from the source so I can change deploy config without polluting the content repo.
/home/almalinux/agent-stack/mb-jekyll-build/ is the build orchestration layer — task prompts, build reports, manifests. When I want a new feature, I drop a markdown file describing the work into the prompts folder. A watcher script picks it up, hands it to Claude Code, and the resulting changes land in the source folder. The reports folder accumulates a record of every build pass.
How Claude Code fits in
The pattern is simple and it took longer to invent than it should have.
A bash watcher script monitors /shared/queue/ for new files. When one appears, it shells out to claude --print --dangerously-skip-permissions <prompt>. Claude reads the file, executes whatever the prompt asks for (file edits, builds, Docker rebuilds, screenshots), writes a report back to the reports folder, and exits.
The “skip permissions” flag is the part that makes this fast. Without it, every file edit prompts a confirmation. With it, Claude operates as a build engineer who has root in the source folder. That’s an acceptable trade-off in a sandboxed environment with a real user reviewing the resulting commits.
What I get out of this: the entire site — design system, layouts, includes, content, deploy config — was built in roughly a hundred sequential prompts over a few days. Each prompt is a small focused chunk of work with a clear acceptance criterion. The build reports are a paper trail.
The Docker stack
The runtime image is a simple multi-stage build:
FROM ruby:3.3-alpine AS builder
WORKDIR /site
COPY Gemfile Gemfile.lock ./
RUN bundle install --jobs 4 --retry 3
COPY . .
RUN bundle exec jekyll build
FROM nginx:alpine
COPY --from=builder /site/_site /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
bundle install runs in a layer that gets cached as long as Gemfile.lock doesn’t change. The actual Jekyll build is fast — under two seconds for a site with sixty-odd posts.
The compose file routes the container behind Traefik with a Host rule, and Traefik handles TLS via Cloudflare’s API. From the outside, all you see is https://michaelbrant.com resolving to a static site that loads in under a second.
services:
michaelbrant:
build:
context: /home/almalinux/Sites/michaelbrant.com
dockerfile: /opt/stacks/michaelbrant/Dockerfile
container_name: michaelbrant
ports:
- "8083:80"
labels:
- "traefik.enable=true"
- "traefik.http.routers.michaelbrant.rule=Host(`michaelbrant.com`)"
What’s next
The WordPress migration just landed — sixty real posts and a dozen recipes from two old WP installs, parsed and converted to markdown with xml2js + turndown. Next up: real photography to replace the Unsplash placeholders, GHL form activation for newsletter and contact, and probably a deploy hook so commits to main build and ship without me running docker compose build by hand.
The whole pipeline is a single text file away from any change I want to make. That’s the part I keep coming back to.