user@homelab:~/docs$ cat 13-this-site.md
Tento web
Jak se web sestavuje z Markdownu a nasazuje do k3s, a jak ho případně zpřístupnit z internetu
# obsah
Architektura
Markdown (site/content/*.md)
│ hugo --minify ← build stage (alpine + ověřený Hugo binár)
▼
statické HTML/CSS/JS
│ COPY do nginx-unprivileged ← runtime stage (uid 101, port 8080)
▼
image localhost/homelab-docs:<tag>
│ buildah → docker-archive → k3s ctr images import
▼
Deployment → Service → Ingress (Traefik) → http://<ip-serveru>/
│
└─ cloudflared (tunel) → https://svejklab.devStruktura projektu
homelab-docs/
├── site/ # Hugo projekt
│ ├── hugo.toml
│ ├── content/ # stránky v Markdownu
│ ├── data/status.yaml # tabulky na úvodní stránce
│ ├── layouts/ # vlastní "terminálová" šablona
│ └── assets/ # CSS a JS
├── nginx/default.conf # nginx: bezpečnostní hlavičky, /healthz
├── Dockerfile # multi-stage build
├── k8s/ # Namespace, Deployment, Service, Ingress, kustomization
├── cloudflared/ # Cloudflare Tunnel konektor (token je Secret, ne soubor)
└── deploy.sh # build + import + apply na serveruNasazení
- Projekt nahrát na server:
scp homelab-docs.zip homelab:~
ssh homelab- Na serveru rozbalit a spustit deploy:
sudo apt install -y unzip
unzip homelab-docs.zip && cd homelab-docs
chmod +x deploy.sh
./deploy.sh 0.1.0Podrobný rozbor obou kroků je níže v sekcích Co přesně dělá scp a Co přesně dělá deploy.sh.
Skript nainstaluje buildah, pokud chybí, sestaví image, naimportuje ho do containerd v k3s, nastaví tag v kustomization.yaml, provede kubectl apply -k k8s/ a počká na rollout.
- Otevřít
http://<ip-serveru>/. Pro hezčí adresu přidej na Macu do/etc/hosts:
echo "<ip-serveru> docs.homelab.lan" | sudo tee -a /etc/hostsCo přesně dělá scp
scp ~/Documents/Homelab/homelab-docs.zip homelab:~/homelab-docs.zip| Část | Význam |
|---|---|
scp | secure copy: kopírování souboru přes šifrované SSH spojení |
~/Documents/Homelab/homelab-docs.zip | odkud: soubor na Macu |
homelab: | kam: server, zkratka ze ~/.ssh/config (IP adresa, uživatel i SSH klíč) |
~/homelab-docs.zip | cesta na serveru: domovská složka uživatele |
Existující soubor na serveru scp bez ptaní přepíše. Před nahráním ověř, že posíláš nejnovější verzi:
ls -lt ~/Documents/Homelab/homelab-docs*.zip
unzip -l ~/Documents/Homelab/homelab-docs.zip | grep 14-publicCo přesně dělá deploy.sh
./deploy.sh 0.3.1Parametr je tag (verze) image. Bez parametru se vygeneruje z data a času, např. 20261028-1330.
#!/usr/bin/env bash
# Sestaví image webu přímo na serveru a nasadí ho do k3s.
# Spouštět NA SERVERU ve složce projektu: ./deploy.sh (tag = datum a čas)
# ./deploy.sh 0.2.0 (vlastní tag)
set -euo pipefail
cd "$(dirname "$(readlink -f "$0")")"
IMAGE="localhost/homelab-docs"
TAG="${1:-$(date +%Y%m%d-%H%M)}"
WORKTMP="$(mktemp -d)"
ARCHIVE="${WORKTMP}/image.tar"
trap 'sudo rm -rf "$WORKTMP"' EXIT
step() { printf '\n\033[1;32m==> %s\033[0m\n' "$*"; }
if ! command -v buildah >/dev/null; then
step "Instaluji buildah (sestavení image bez Docker démona)"
sudo apt-get update -qq && sudo apt-get install -y -qq buildah
fi
step "Build ${IMAGE}:${TAG}"
sudo buildah build --network=host --layers -t "${IMAGE}:${TAG}" -f Dockerfile .
step "Import do containerd v k3s"
sudo buildah push "${IMAGE}:${TAG}" "docker-archive:${ARCHIVE}:${IMAGE}:${TAG}"
sudo k3s ctr -n k8s.io images import "$ARCHIVE"
sudo k3s ctr -n k8s.io images ls -q | grep -F "${IMAGE}:${TAG}"
step "Nastavuji tag v k8s/kustomization.yaml"
sed -i "s/^\(\s*newTag:\).*/\1 \"${TAG}\"/" k8s/kustomization.yaml
grep newTag k8s/kustomization.yaml
step "Kontrola manifestů (server-side dry-run)"
kubectl apply -f k8s/namespace.yaml # dry-run potřebuje existující namespace
kubectl apply -k k8s/ --dry-run=server
step "kubectl apply -k k8s/"
kubectl apply -k k8s/
kubectl -n homelab-docs rollout status deploy/homelab-docs --timeout=120s
step "Hotovo"
kubectl -n homelab-docs get pods,svc,ingress -o wide
echo
echo "Web: http://<ip-serveru>/ nebo http://docs.homelab.lan/ (po úpravě /etc/hosts)"Příprava (řádky 5–15)
set -euo pipefail: pojistka. Skript se zastaví při první chybě (-e), při nedefinované proměnné (-u) i při chybě uvnitř roury|(pipefail). Po neúspěšném buildu tak nikdy nepokračuje nasazením.cd "$(dirname "$(readlink -f "$0")")": přepne se do složky skriptu. Funguje pak, i když ho spustíš odjinud.TAG="${1:-$(date …)}": první parametr, jinak datum a čas.mktemp -d+trap … EXIT: dočasná složka na archiv image, která se vždy smaže, i když skript spadne.
1. buildah (jen poprvé)
Když buildah chybí, nainstaluje ho z apt. Další běhy krok přeskočí.
2. Build image
buildah build sestaví image podle Dockerfile ve dvou fázích:
- V Alpine stáhne Hugo, ověří jeho SHA256 a vyrobí z Markdownu HTML.
- HTML zkopíruje do malého image
nginx-unprivileged, který běží bez root práv.
Přepínače:
--network=host: build použije síť serveru. Bez něj ufw zahazoval přeposílaný provoz aapknestáhl balíčky, viz Troubleshooting #16.--layers: cache mezivýsledků, takže další buildy jsou rychlejší.
3. Import do k3s
buildah a k3s mají každý své úložiště image. Image se proto zabalí do archivu (docker-archive) a naimportuje do containerd v k3s (k3s ctr images import). Poslední řádek kroku ověří, že tam image opravdu je. Když ne, grep selže a skript skončí.
Proto má Deployment imagePullPolicy: Never: image se nestahuje z registry, bere se z místního úložiště.
4. Verze v manifestu
sed přepíše newTag v k8s/kustomization.yaml. Kustomize tag při nasazení dosadí do Deploymentu, takže se manifest Deploymentu ručně neupravuje.
5. Kontrola na nečisto
kubectl apply -k k8s/ --dry-run=server: API server manifesty zkontroluje, ale nic nezmění. Ověří syntaxi, pravidla Pod Security (restricted) i existenci namespace. Proto se namespace vytvoří těsně předtím.
--dry-run=client | --dry-run=server | |
|---|---|---|
| kde se kontroluje | jen v kubectl | v API serveru |
| ověří schéma, admission (PSS), existenci namespace | ne | ano |
| typické použití | generování YAML (-o yaml) | kontrola před nasazením |
6. Nasazení a rolling update
kubectl apply -k k8s/ pošle do clusteru Namespace, Deployment, Service a Ingress. Změnil se jen tag image, takže Kubernetes spustí rolling update:
- spustí nový pod s novou verzí,
- počká, až projde readiness probe (
/healthz), - teprve pak ukončí starý pod (
maxUnavailable: 0). Web ani na chvíli nevypadne.
rollout status čeká, až se výměna dokončí. Když nový pod do 120 s nenaběhne, skript skončí chybou a starý pod běží dál.
7. Výpis stavu
kubectl get pods,svc,ingress ukáže výsledný stav.
Celý tok
Markdown ─(Hugo)─▶ HTML ─(buildah)─▶ image:tag ─(import)─▶ containerd k3s
─(kubectl apply -k)─▶ rolling update ─▶ nový pod ─▶ Traefik ─▶ cloudflared ─▶ svejklab.devKontrola a návrat k předchozí verzi
kubectl -n homelab-docs get deploy homelab-docs -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
kubectl -n homelab-docs rollout history deploy/homelab-docs
kubectl -n homelab-docs rollout undo deploy/homelab-docsrollout undo vrátí předchozí verzi. Image starších verzí zůstávají v containerd, takže návrat je okamžitý.
Proč buildah a ne Docker
k3s používá containerd, ne Docker. buildah sestaví image bez běžícího démona, takže na serveru nezabírá RAM. Hotový image se do containerd přenese jako archiv. Registry není potřeba, proto má Deployment imagePullPolicy: Never.
Úprava obsahu
Upravit .md v site/content/ a znovu spustit ./deploy.sh. Tag se bez parametru vygeneruje z data a času a Deployment udělá rolling update bez výpadku (maxUnavailable: 0).
Lokální náhled na Macu (brew install hugo):
cd homelab-docs/site && hugo serverBezpečnost podu
| Nastavení | Proč |
|---|---|
namespace pod-security…/enforce: restricted | cluster odmítne pod, který nesplňuje přísný profil |
runAsNonRoot, uid 101 | nginx neběží jako root |
readOnlyRootFilesystem + emptyDir /tmp | kontejner nemůže měnit svůj systém souborů |
capabilities: drop ALL, allowPrivilegeEscalation: false | žádná zvláštní práva jádra |
seccompProfile: RuntimeDefault | omezené systémové volání |
automountServiceAccountToken: false | pod nemá token k Kubernetes API |
| requests/limits | bez swapu je limit paměti nutnost |
readiness/liveness na /healthz | Kubernetes pozná, že web žije |
| CSP a další hlavičky v nginx | prohlížeč nespustí cizí skripty |
Přístup z internetu
Web je veřejně na https://svejklab.dev přes Cloudflare Tunnel. Postup, výběr domény a co se při tom pokazilo, je v kapitole Veřejný web.
Lokálně v domácí síti funguje dál i http://<ip-serveru>/.