- Shell 69%
- Smarty 20.5%
- Dockerfile 10.5%
|
All checks were successful
Build and publish Docker image / build (push) Successful in 1m43s
Hugo hashes CSS/JS/font/image filenames so they can be cached for 1 year with immutable. HTML pages get 5 minutes with must-revalidate since they rebuild on every push. Upstream nginx passes these headers through unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|---|---|---|
| .forgejo/workflows | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile | ||
| entrypoint.sh | ||
| hooks.json.tpl | ||
| nginx.conf.tpl | ||
| README.md | ||
| rebuild.sh | ||
| supervisord.conf | ||
cairn
A single Docker image that serves a Hugo site via nginx, automatically rebuilding on Forgejo push via webhook, with ntfy notifications.
Stack
- Hugo — static site builder
- nginx — serves the built site on port 80; proxies webhook traffic internally
- webhook — listens on port 9000, validates HMAC signatures, triggers rebuilds
- supervisord — runs nginx and webhook together in a single container
- ntfy — push notifications on build success/failure
Project Structure
cairn/
├── .env # your config (never commit this)
├── .env.example # template for .env
├── .forgejo/
│ └── workflows/
│ └── build.yml # CI: builds and publishes Docker image to Forgejo registry
├── .gitignore
├── docker-compose.yml
├── Dockerfile
├── entrypoint.sh
├── rebuild.sh
├── hooks.json.tpl
├── nginx.conf.tpl
├── supervisord.conf
└── secrets/
└── deploy_key # SSH deploy key (never commit this)
Setup
1. Configure environment
cp .env.example .env
Edit .env with your values.
2. Generate a deploy key (SSH auth)
mkdir -p ./secrets
ssh-keygen -t ed25519 -C "hugo-deploy" -f ./secrets/deploy_key -N ""
Add the contents of ./secrets/deploy_key.pub to your Forgejo repo under:
Repository → Settings → Deploy Keys (read-only access is sufficient)
3. Configure Forgejo webhook
In your Forgejo repo go to Settings → Webhooks → Add Webhook → Forgejo and set:
- URL:
http://your-server/<WEBHOOK_ENDPOINT> - Secret: the value of
WEBHOOK_SECRETfrom your.env - Trigger: Push events
The full webhook URL is logged on container start.
By default the endpoint is the SHA256 of "cairn". Set WEBHOOK_ENDPOINT_SEED in .env to use a different seed.
4. Run
Pull the pre-built image from the registry:
docker compose pull && docker compose up -d
Or build locally:
docker compose up --build
How it works
Forgejo push → nginx (:80) → webhook (:9000) → rebuild.sh → git pull + hugo build → nginx serves /output
On container start, the entrypoint will:
- Clone the repo if
/siteis empty - Run an initial Hugo build
- Generate
hooks.jsonandnginx.conffrom templates - Start nginx and webhook via supervisord
On every Forgejo push:
- nginx rate-limits (10 req/min) and proxies to webhook
- webhook verifies the HMAC-SHA256 signature
rebuild.shacquires a build lock, runsgit pullthenhugo --minify- ntfy sends a success or failure notification (with full output on failure)
CI
Pushing to main or tagging v*.*.* triggers .forgejo/workflows/build.yml, which builds and publishes the Docker image to the Forgejo container registry.
Published tags:
sha-<short-sha>— every buildlatest— on every push tomain<version>(e.g.1.2.3) — onv*.*.*tags
Old sha-* versions are pruned from the registry after each build.
Authentication
Set AUTH_METHOD in .env to either:
ssh— uses the deploy key mounted at./secrets/deploy_key(recommended)https— usesGIT_USERandGIT_TOKENfrom.env
Reverse proxy
If cairn sits behind an upstream nginx, configure a virtual host to proxy traffic through. The same vhost handles both the site and the webhook endpoint since they share port 80.
server {
listen 80;
server_name your-site.example.com;
location / {
proxy_pass http://127.0.0.1:1337;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
For HTTPS with Certbot:
server {
listen 443 ssl;
server_name your-site.example.com;
ssl_certificate /etc/letsencrypt/live/your-site.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-site.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:1337;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name your-site.example.com;
return 301 https://$host$request_uri;
}
The webhook URL to configure in Forgejo would then be https://your-site.example.com/<WEBHOOK_ENDPOINT>.
Notes
-
SSH host key verification uses TOFU (trust on first use): on container start, host and port are parsed from
GIT_REPO_URLandssh-keyscanwrites the key toknown_hosts. Bothssh://git@host:2222/pathandgit@host:pathURL formats are supported — port defaults to 22 for scp-style. The deploy key is copied from/run/secrets/deploy_keyat startup so permissions can be set correctly. The risk of TOFU is acceptable for internal networks, further mitigated by restricting container egress via Docker networking. -
The base image is
alpine:latest. If you need fully reproducible builds, pin it to a specific version (e.g.alpine:3.21) in the Dockerfile. -
Hugo is installed via
apkand will be the standard (non-extended) build. The extended variant adds SCSS/Sass compilation and WebP encoding. If your site uses either of these features it will fail to build. If not, there is no practical difference.