Overview
Before writing anything else on this blog, it felt right to write about the blog itself. By the time you’re reading this post, it has already gone through the exact pipeline described below: written in Obsidian, pushed to a git repo, built into a static site, packaged into a container, and deployed to dpall.dev without me touching a server.
The goal was simple: writing should be the only manual step. Everything after “commit” should happen on its own.
The Stack
The pipeline has five moving parts:
- Obsidian: where posts are drafted and edited as plain markdown within the monorepo.
- Hugo + PaperMod: the static site generator that turns markdown into a site.
- Docker: packages the pre-built static site into a production-ready nginx image.
- OneDev: handles the orchestration — from downloading modules to building the image and triggering the deployment.
- Dokploy: receives a webhook from OneDev to pull the new image and roll it out.
Implementation
1. Writing in Obsidian
Posts live as markdown files with standard Hugo front matter directly inside the notes/vault/posts directory. This directory is part of my main homelab monorepo, which I open directly in Obsidian. A post is just a note until draft: false and a git push turn it into a published page.
2. The Hugo Build and Dockerization
Unlike many setups that do multi-stage Docker builds, I prefer to keep the build environment separated from the containerization step in OneDev.
First, a dedicated CI step runs the Hugo build using the official Hugo image:
hugo mod get
hugo --minify
This produces a public/ directory. Then, a simple Dockerfile takes that output and bakes it into a hardened nginx image with custom cache headers:
FROM nginx:1.27-alpine
COPY public/ /usr/share/nginx/html/
# Add custom nginx config for performance
COPY <<"EOF" /etc/nginx/conf.d/default.conf
server {
listen 80;
location / {
try_files $uri $uri/ =404;
}
location ~* \.(css|js|woff2|png|jpg|svg|ico)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
EOF
3. OneDev: Orchestration and Triggers
OneDev watches the notes/** path in the monorepo. When it detects a change, it runs a multi-step job:
- Checkout: Pulls the latest code.
- Hugo Build: Runs in a
hugomods/hugocontainer to generate the static files. - Build and Push: Packages the
public/folder into a Docker image and pushes it to my internal registry. - Redeploy: Sends a POST request to a Dokploy webhook to trigger the update.
- name: Publish Blog
steps:
- type: CommandStep
name: Build Hugo
image: hugomods/hugo:exts-0.146.0
commands: |
cd notes
hugo --minify
- type: BuildImageStep
name: Build Docker Image
buildPath: notes/
dockerfile: notes/Dockerfile
- type: CommandStep
name: Redeploy Dokploy
image: alpine/curl
commands: curl -f -X POST "@secret:DOKPLOY_WEBHOOK_URL@"
triggers:
- type: BranchUpdateTrigger
branches: main
paths: notes/**
4. Dokploy: Automated Rollouts
Dokploy acts as the final stage. Upon receiving the webhook from OneDev, it pulls the latest image from the registry. Traefik, running as the entry point, handles the routing based on container labels and manages the SSL termination.
To make dpall.dev reachable without opening home ports, a Cloudflare Tunnel connects the public internet to my internal Traefik instance. This setup keeps the infrastructure invisible to the public while providing a fast, secure path for readers.
Challenges & Solutions
Registry Resolution
Early on, the OneDev build agent and the Dokploy host disagreed about how to resolve the internal registry hostname. The fix was ensuring both used the same internal DNS resolver (CoreDNS in my case) consistently, rather than a mix of local /etc/hosts entries.
Webhook Reliability
Initially, I relied on Dokploy’s “Auto Deploy” feature which polls the registry. This proved to be laggy. Switching to an explicit webhook trigger in OneDev made the “commit to live” time drop from minutes to seconds.
Conclusion
The end result is a pipeline where writing and publishing are separated by a single git command.
This is also the first piece of a broader pattern: git as the source of truth, OneDev as the automation layer, and Dokploy or Colmena handling the actual deployment. In my next post, I’ll cover how the same philosophy applies to deploying NixOS configurations using Colmena.