git push
→ Forgejo Actions builds Hugo image
→ pushes to Forgejo registry
→ updates image tag in k8s manifest
→ commits back to repo
→ Flux detects commit
→ applies Deployment
→ k3s rolls out new image
No Bitnami charts. No Flux image automation controllers. No runtime git cloning. Just a plain nginx Deployment running your pre-built static site.
Prerequisites
k3s cluster running
Flux already bootstrapped
Forgejo instance at forgejo.uclab.dev with a runner configured
Your Hugo repo at https://forgejo.uclab.dev/affragak/uclab with the Risotto theme as a git submodule
Make sure the submodule is correctly configured. Your .gitmodules should look like:
[submodule "themes/risotto"]path=themes/risotto
url = https://codeberg.org/ristomolnar/risotto.git
branch = main
Step 2 — Dockerfile
Create this at the repo root. Both stages are pinned to specific versions — update them intentionally.
# DockerfileFROMhugomods/hugo:0.157.0ASbuilderWORKDIR/site# Copy everything including the resolved submoduleCOPY . .# Build the siteRUN hugo --minify# ---FROMnginx:1.27-alpine# Remove the default nginx configRUN rm /etc/nginx/conf.d/default.conf# Copy built site and custom nginx configCOPY --from=builder /site/public /usr/share/nginx/htmlCOPY nginx.conf /etc/nginx/conf.d/default.confEXPOSE8080
Step 3 — nginx Config
# nginx.conf
server{listen8080;root/usr/share/nginx/html;indexindex.html;# Serve pre-compressed files if available
gzip_staticon;location/{try_files$uri$uri/$uri.html=404;}# Cache static assets aggressively
location~*\.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {expires1y;add_headerCache-Control"public,immutable";}# No caching for HTML
location~*\.html$ {add_headerCache-Control"no-cache";}}
The try_files $uri.html fallback lets Hugo’s ugly URLs (e.g. /about → /about.html) work without trailing slashes.
Step 4 — Forgejo Secrets
In your Forgejo repo go to Settings → Secrets and Variables → Actions and add:
Secret Name
Value
REGISTRY_USER
Your Forgejo username (e.g. affragak)
REGISTRY_TOKEN
A Forgejo access token with write:packages scope
To create the token: Forgejo → User Settings → Applications → Generate Token → check write:packages and read:packages.
GitHub Deploy Key
Since Forgejo CI needs to push to a GitHub repo, you need a deploy key with write access.
Add the public key to GitHub:
Go to github.com/affragak/pi5cluster → Settings → Deploy keys → Add deploy key → paste forgejo-ci-key.pub → check Allow write access
Add the private key to Forgejo:
Go to your Hugo repo in Forgejo → Settings → Secrets → add secret named GH_DEPLOY_KEY → paste the contents of forgejo-ci-key (the private key)
Why fetch-depth: 0? Without it, git rev-parse --short HEAD may not work correctly in shallow clones.
Why not tag latest? Kubernetes won’t re-pull a latest tag unless imagePullPolicy: Always is set, which causes an unnecessary pull on every pod restart. Using the git sha means every image is unique and immutable.