Van eerste setup naar eerste artikelen: mijn nieuwe website in het homelab
Intro
Ondertussen ben ik al enkele jaren bezig met mijn eigen homelab. Self-hosted applicaties, los van de cloud en met allerlei handige applicaties. Vandaag is het de beurt aan een eigen website — één met ultieme flexibiliteit en geweldige performance.
Mijn doelen waren helder:
- Een snelle persoonlijke website
- Hij moet volledig self-hosted zijn
- Markdown als bron
- Eenvoudig te beheren via mijn eigen git-repository
- Geen onnodige en zware frameworks als die niet nodig zijn
Daarmee kwam ik al snel uit bij Hugo.
De architectuur
De geplande opzet is vrij eenvoudig:
Git (Forgejo)
↓
Hugo build
↓
/site/public
↓
NGINX container
↓
Traefik (TLS / routing)
↓
Internet
Belangrijk daarin is het onderscheid tussen bouwen en serveren:
- Hugo ‘bouwt’ de statische site
- NGINX serveert alleen de gegenereerde bestanden
- Traefik doet de routing en TLS
Dat betekent ook dat Hugo niet permanent hoeft te draaien. Alleen bij een build.
De mapstructuur
De uiteindelijke projectstructuur zag er in beginsel als volgt uit:
/
├─ nginx/
│ └─ default.conf
├─ site/
│ ├─ content/
│ ├─ layouts/
│ ├─ static/
│ ├─ public/
│ └─ hugo.toml
└─ docker-compose.yml
Vooral dat onderscheid tussen content/ en public/ is belangrijk:
content/bevat de Markdown-bronbestandenpublic/bevat de door Hugo gegenereerde HTML-output
Waarom Hugo?
Een van de belangrijkste doelen is dat hij héél erg snel moet zijn. Wat je bij veel CMS-sen ziet, zoals WordPress, is dat iedere pagina bij ieder verzoek opnieuw opgebouwd moet worden. Hugo doet dat vooraf, waardoor de website alleen statische bestanden hoeft te serveren.
Daarnaast was het mijn voorkeur om met Markdown te werken. Gewoon leesbare bestanden, geen ingewikkelde code, maar simpele tekstbestanden die zélfs zonder speciale viewer goed te lezen zijn.
Dat past goed bij Hugo:
- content in
.md - extreem snelle builds (14 ms)
- statische output
- lage serverload
- eenvoudig te hosten
Precies wat ik zocht.
De Docker-opzet
Ik gebruik twee services:
- een NGINX-container voor runtime
- een Hugo-container voor builds
Een vereenvoudigde compose-opzet ziet er zo uit:
services:
patrick-site:
image: nginx:alpine
restart: unless-stopped
volumes:
- /projectfolder/site/public:/usr/share/nginx/html:ro
- /projectfolder/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
hugo:
image: ghcr.io/gohugoio/hugo:latest
working_dir: /src
volumes:
- /site:/src
command: ["--source", "/src", "--destination", "/src/public", "--minify"]
restart: "no"
NGINX draait altijd en is realistisch gezien de ‘server’. Hugo draait alleen als de site gegenereerd moet worden.
Waar het misging
Zoals zo vaak was de uiteindelijke oplossing eenvoudiger dan de weg ernaartoe.
1. Mount-problemen
Een van de eerste fouten was een klassieke Docker-mountfout:
Are you trying to mount a directory onto a file
De oorzaak bleek simpel: default.conf was per ongeluk als map aangemaakt in plaats van als bestand.
Dat soort fouten zijn frustrerend, je verwacht niet helemaal dat er een map wordt aangemaakt terwijl je eigenlijk een bestand verwacht. Iets waar ik binnen mijn Stack binnen Portainer wel wat last van had.
2. Portainer relative paths
In eerste instantie draaide alles onder een Portainer stackmap in /data/compose/....
Dat werkte technisch, maar het is niet logisch:
- Webcontent staat op langzame schijven (HDD’s in plaats van SSD’s)
- De relatieve mounts in Portainer zijn voor snelle tests handig, maar voor een nette productie-opzet niet ideaal
Daarom heb ik de locatie van de site verplaatst naar mijn SSD. Dat is veel logischer en ook beter voor performance. Dat had ook het mount-probleem voorkomen, want dan had ik de mappenstructuur voorafgaand aan mijn stack kunnen maken. Mja. Lessons learned ;-)
3. Hugo die ogenschijnlijk niets deed
Toen Hugo eenmaal meedeed in de compose, leek hij eerst niets te doen.
Later bleek dat daar twee dingen speelden:
- een
profilewaardoor de service niet werd gestart - en een commandoprobleem waarbij
hugodubbel werd aangeroepen
Ik had iets in de richting van dit:
command: ["hugo", "--minify"]
Maar in de officiële Hugo-image is hugo al de entrypoint. Daardoor kreeg ik een fout in de trant van:
unknown command "hugo" for "hugo" -- logisch! je voert ook niet game.exe game.exe uit ;-)
De oplossing was dus gewoon:
command: ["--source", "/src", "--destination", "/src/public", "--minify"]
4. Rechten op het filesystem
Daarna kwam een permissions-fout:
failed to acquire a build lock: permission denied
De maprechten waren niet goed voor de user waarmee Hugo probeerde te schrijven.
Dat heb ik opgelost via de rechten op de hostmap, zodat Hugo gewoon in /src kon schrijven en dus ook public/ kon vullen.
5. Geen layouts
Toen Hugo eindelijk buildde, kwam de volgende melding:
found no layout file for "html" for kind "home"
Dat was op zich goed nieuws, want de build liep dan in elk geval al. Het probleem was alleen dat Hugo nog niet wist hoe de inhoud gerenderd moest worden.
Dus moesten er layouts bij komen, minimaal:
layouts/index.htmllayouts/_default/single.htmllayouts/_default/list.html
Pas daarna kreeg de site daadwerkelijk vorm.
6. Git via HTTPS
Ik wilde bewust geen SSH (geen portforwarding voor mij) voor deze repo, maar gewoon HTTPS.
Daarmee kwam de volgende stap: een Personal Access Token gebruiken voor Forgejo en Git op de server via HTTPS laten praten met de repo. Ook daar zaten nog wat kleine hobbels in:
- safe directory waarschuwingen
- een remote repo die al een eerste commit had
- een mergeconflict in
.gitignore
Dat is eerlijk gezegd meer mijn - op dit moment beperkte ervaring - met Git, maar op zich geen probleem. Hulde: -> https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup
Van losse setup naar eerste publicatie
Op een gegeven moment viel alles op zijn plek:
- mapstructuur logisch
- compose werkend
- Hugo buildend
- NGINX serverend
- Git gekoppeld
En toen kwam de belangrijkste stap:
Niet meer prutsen aan de infrastructuur, maar gewoon het eerste artikel schrijven.
Mijn workflow nu
De workflow is nu mooi overzichtelijk:
Markdown schrijven in VS Code
→ Committen naar Git
→ Build draaien met Hugo
→ NGINX serveert de nieuwe output
Of concreter:
- Artikel schrijven in
site/content/ - wijzigingen committen en pushen
- Hugo build uitvoeren
- Site live
Wat ik hiervan geleerd heb
Een paar dingen zou ik achteraf meteen zo doen:
- Altijd absolute paden gebruiken, het geeft de mogelijkheid de randvoorwaarden goed in te vullen voorafgaand aan je eerste run.
- Source en output vanaf het begin scheiden
- Eerder (en op juiste wijze) Git koppelen
Conclusie
Wat begon als “ik wil een snelle persoonlijke site opzetten” werd een flinke exercitie in:
- Self-hosting
- Docker
- Traefik
- Hugo
- Git
- En logisch structureren
Kortom: een leerzame en waardevolle stap voor mijn homelab.
Tot slot
Dit artikel is daarmee meteen ook de eerste echte inhoud op mijn nieuwe website. De volgende stap is verder bouwen aan:
- een verbeterde layout
- meer artikelen over dingen die ik doe met mijn homelab
- en waarschijnlijk ook wat technische notities over dingen die onderweg misgingen en hoe ik ze heb opgelost
Wordt vervolgd!