Making it dead-simple to deploy apps on your own K3s cluster.
Tested on Hetzner CX23/CX33 Ubuntu VPS.
- K3s — lightweight, CNCF-certified Kubernetes distribution
- Traefik — ingress controller (K3s default)
- cert-manager — automatic TLS certificates from Let's Encrypt
- Helm — single generic chart covers all app types
- Private registry — in-cluster container image storage
- Kaniko — in-cluster Docker image builds (no Docker daemon required)
After creating a fresh Hetzner VPS with Ubuntu, the guided setup.sh script brings the whole cluster up.
First, point your domain at the server. Add a wildcard DNS A record for *.<domain> pointing to the server's IP, so any subdomain you deploy resolves without adding a new record each time. cert-manager then issues (and auto-renews) an individual TLS certificate per subdomain via the HTTP-01 challenge, so this must resolve before setup runs.
Then SSH into the fresh server (as root) and run the one-liner:
curl -fsSL https://raw.githubusercontent.com/woudsma/k3s-deploy/main/install.sh | shThis installs git/curl, clones the repo to /opt/k3s-deploy, and launches setup.sh.
Prefer to clone it yourself?
cd k3s-deploy
rsync -a --exclude='.git' . root@<server-ip>:/tmp/k3s-deploy
ssh root@<server-ip>
bash /tmp/k3s-deploy/setup.shsetup.sh prompts for your domain and registry credentials, replaces the mysite.com placeholder repo-wide, and installs everything: K3s, security hardening, cert-manager + Let's Encrypt, the private registry and its pull/push secrets, the git-push deploy system, and (optionally) the Headlamp dashboard. Optional extras — security hardening, swap, Headlamp, zsh, and the login banner — are individual yes/no prompts.
When it finishes, the script prints the remaining manual steps — including copying the kubeconfig from /etc/rancher/k3s/k3s.yaml to your local ~/.kube/config (replace 127.0.0.1 with the server IP) for remote kubectl access.
See CLAUDE.md for a step-by-step breakdown of everything setup.sh does.
Each app only needs a helm-values.yaml to override the helm defaults (seen in charts/app/) — no raw Kubernetes manifests required.
my-app/
├── Dockerfile
└── helm-values.yaml # only K8s-related file needed
A minimal helm-values.yaml for a website is ~4 lines:
name: my-website
ingress:
hosts:
- my-website.mysite.comEverything else (port 80, health probe, TLS, resource limits) comes from chart defaults. See charts/app/values.yaml for all available options.
Monorepos and multi-environment repos can push to several app names from one repository — an optional .deploy/<app-name>.conf per target selects the build context, Helm values file, build args and allowed ref. See examples/monorepo/.
First deploy — just push, the hook runs helm upgrade --install and creates all resources:
git remote add deploy deploy@<server-ip>:my-app
git push deploy mainTwo ways to deploy updates:
- Git push —
git push deploy mainbuilds on the server, streams logs back (Dokku-like) - GitHub Actions — CI/CD builds image, pushes to registry, deploys via Helm
See CLAUDE.md for full setup instructions, commands, and architecture decisions.
AGENTS.md is a self-contained guide covering everything above — paste its raw URL into a prompt in any project and the agent knows how to deploy that app to your cluster without reading this repo:
https://raw.githubusercontent.com/woudsma/k3s-deploy/main/AGENTS.md
A lightweight web dashboard at headlamp.<domain> for browsing pods, viewing logs, exec-ing into containers, and checking resource status.
Optional — setup.sh asks whether to install it. To deploy it later, or to generate a login token:
kubectl apply -f monitoring/headlamp.yaml # if you skipped it during setup
kubectl create token headlamp -n headlamp --duration=8760hContinuously scans every workload's image (any language — Node, Python, Go, …) for
HIGH/CRITICAL vulnerabilities and publishes the results as VulnerabilityReport
resources. View them:
# One report per workload, with critical/high counts
kubectl get vulnerabilityreports -n default
# Summary table for all apps at once (critical/high per workload)
kubectl get vulnerabilityreports -n default -o custom-columns=\
'WORKLOAD:.metadata.labels.trivy-operator\.resource\.name,IMAGE:.report.artifact.repository,'\
'CRITICAL:.report.summary.criticalCount,HIGH:.report.summary.highCount'
# Full CVE detail for a single report
kubectl describe vulnerabilityreport -n default <name>Reports also show up in the Headlamp dashboard.
The examples/ directory has ready-to-use helm-values.yaml files for common app types — websites, APIs, WebSocket apps, databases, multi-domain setups, per-environment configs, and GitHub Actions workflows.
| Directory | Description |
|---|---|
charts/app/ |
Generic Helm chart for all app types (websites, APIs, databases) |
cert-manager/ |
ClusterIssuer for automatic Let's Encrypt SSL via cert-manager |
registry/ |
Private Docker registry (in-cluster) with htpasswd auth |
deploy/ |
Git-push deploy setup — Dokku-like git push deploy main experience |
monitoring/ |
Headlamp dashboard (Trivy Operator is installed from upstream by setup.sh) |
examples/ |
Example helm-values.yaml files for common app types |
test/ |
End-to-end harness: runs setup.sh + a real git-push deploy in a throwaway container and asserts the cluster came up |