kubernetes-k3s
SKILL.md
Kubernetes K3s Patterns
Deploy and manage applications on lightweight K3s clusters optimized for single-node VPS deployments with Traefik ingress and cert-manager.
When to Use This Skill
- Installing and configuring K3s on a VPS
- Setting up Traefik IngressRoute for subdomain routing
- Configuring cert-manager with Let's Encrypt for TLS
- Creating Kustomize base + overlay configurations
- Managing resource limits on constrained hardware (4GB RAM)
- Deploying containerized apps to K3s
K3s Installation and Configuration
Single-Node Installation
# Install K3s with default Traefik and HostPort binding
curl -sfL https://get.k3s.io | sh -s - \
--write-kubeconfig-mode 644
# Verify installation
sudo k3s kubectl get nodes
sudo k3s kubectl get pods -A
# Copy kubeconfig for local kubectl
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config
Kubeconfig for Remote Access
# On local machine: copy kubeconfig and update server address
scp user@server:/etc/rancher/k3s/k3s.yaml ~/.kube/k3s-config
sed -i 's/127.0.0.1/YOUR_SERVER_IP/' ~/.kube/k3s-config
export KUBECONFIG=~/.kube/k3s-config
Embedded Components
K3s ships with:
- Traefik - Ingress controller (v2.x)
- CoreDNS - Cluster DNS
- Flannel - CNI networking
- Local Path Provisioner - Default storage class
- Metrics Server - Resource metrics
K3s Upgrade
# Upgrade K3s
curl -sfL https://get.k3s.io | sh -
# Or use system-upgrade-controller for automated upgrades
kubectl apply -f https://github.com/rancher/system-upgrade-controller/releases/latest/download/system-upgrade-controller.yaml
Traefik Ingress
HostPort Configuration (No Load Balancer)
Avoid Hetzner Load Balancer costs (~10€/mo) by using HostPort:
# traefik-config.yaml (HelmChartConfig)
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: traefik
namespace: kube-system
spec:
valuesContent: |-
ports:
web:
hostPort: 80
websecure:
hostPort: 443
# Enable access logs
logs:
access:
enabled: true
# Dashboard (disable in production)
ingressRoute:
dashboard:
enabled: false
IngressRoute CRDs
# Basic IngressRoute
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: easyfactu-api
namespace: easyfactu
spec:
entryPoints:
- websecure
routes:
- match: Host(`api.easyfactu.es`)
kind: Rule
services:
- name: easyfactu-api
port: 8000
tls:
certResolver: letsencrypt
Wildcard Subdomain Routing
# Route all *.app.easyfactu.es to the frontend
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: easyfactu-web-wildcard
namespace: easyfactu
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`{subdomain:[a-z]+}.app.easyfactu.es`)
kind: Rule
services:
- name: easyfactu-web
port: 3000
tls:
certResolver: letsencrypt
domains:
- main: "app.easyfactu.es"
sans:
- "*.app.easyfactu.es"
Middleware
# Rate limiting middleware
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: rate-limit
namespace: easyfactu
spec:
rateLimit:
average: 100
burst: 50
period: 1m
---
# HTTPS redirect middleware
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: https-redirect
namespace: easyfactu
spec:
redirectScheme:
scheme: https
permanent: true
---
# Security headers middleware
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: security-headers
namespace: easyfactu
spec:
headers:
frameDeny: true
contentTypeNosniff: true
browserXssFilter: true
stsSeconds: 31536000
stsIncludeSubdomains: true
---
# Apply middleware to IngressRoute
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: easyfactu-api
namespace: easyfactu
spec:
entryPoints:
- websecure
routes:
- match: Host(`api.easyfactu.es`)
kind: Rule
middlewares:
- name: rate-limit
- name: security-headers
services:
- name: easyfactu-api
port: 8000
tls:
certResolver: letsencrypt
Cert-Manager
Installation
# Install cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml
# Verify
kubectl get pods -n cert-manager
ClusterIssuer with Let's Encrypt
# Production issuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@easyfactu.es
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: traefik
---
# Staging issuer (for testing, avoids rate limits)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: admin@easyfactu.es
privateKeySecretRef:
name: letsencrypt-staging
solvers:
- http01:
ingress:
class: traefik
Certificate Resource
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: easyfactu-tls
namespace: easyfactu
spec:
secretName: easyfactu-tls-secret
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- api.easyfactu.es
- app.easyfactu.es
- "*.app.easyfactu.es"
Kustomize Patterns
Base Configuration
infra/k8s/
├── base/
│ ├── kustomization.yaml
│ ├── namespace.yaml
│ ├── deployment.yaml
│ ├── service.yaml
│ └── ingress.yaml
└── overlays/
├── dev/
│ ├── kustomization.yaml
│ └── patches/
│ └── resource-limits.yaml
├── staging/
│ ├── kustomization.yaml
│ └── patches/
└── prod/
├── kustomization.yaml
└── patches/
├── resource-limits.yaml
└── replicas.yaml
Base Kustomization
# infra/k8s/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- deployment.yaml
- service.yaml
- ingress.yaml
commonLabels:
app.kubernetes.io/managed-by: kustomize
Base Deployment
# infra/k8s/base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: easyfactu-api
spec:
replicas: 1
selector:
matchLabels:
app: easyfactu-api
template:
metadata:
labels:
app: easyfactu-api
spec:
containers:
- name: api
image: ghcr.io/franciscosanchezn/easyfactu-api:latest
ports:
- containerPort: 8000
envFrom:
- secretRef:
name: easyfactu-api-secrets
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
imagePullSecrets:
- name: ghcr-credentials
Production Overlay
# infra/k8s/overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namespace: easyfactu
images:
- name: ghcr.io/franciscosanchezn/easyfactu-api
newTag: v1.2.3 # Pin to specific version
patches:
- path: patches/resource-limits.yaml
# infra/k8s/overlays/prod/patches/resource-limits.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: easyfactu-api
spec:
template:
spec:
containers:
- name: api
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "512Mi"
cpu: "1000m"
Dev Overlay
# infra/k8s/overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namespace: easyfactu-dev
namePrefix: dev-
configMapGenerator:
- name: easyfactu-api-config
literals:
- DEBUG=true
- LOG_LEVEL=debug
secretGenerator:
- name: easyfactu-api-secrets
envs:
- .env.dev
Deploy with Kustomize
# Preview the generated manifests
kubectl kustomize infra/k8s/overlays/prod
# Apply to cluster
kubectl apply -k infra/k8s/overlays/prod
# Dry-run validation
kubectl apply -k infra/k8s/overlays/prod --dry-run=client
kubectl apply -k infra/k8s/overlays/prod --dry-run=server
Resource Management (4GB RAM VPS)
Resource Budget
For a Hetzner CX22 (2 vCPU, 4GB RAM):
| Component | Memory Request | Memory Limit | CPU Request | CPU Limit |
|---|---|---|---|---|
| K3s system | ~600 MB | - | - | - |
| Traefik | 64 MB | 128 MB | 50m | 200m |
| cert-manager | 64 MB | 128 MB | 50m | 200m |
| CoreDNS | 32 MB | 64 MB | 50m | 100m |
| App 1 (API) | 128 MB | 256 MB | 100m | 500m |
| App 2 (Web) | 64 MB | 128 MB | 50m | 200m |
| Available | ~2.5 GB | - | - | - |
Namespace Organization
# namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: easyfactu
labels:
project: easyfactu
environment: prod
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: easyfactu-quota
namespace: easyfactu
spec:
hard:
requests.cpu: "1"
requests.memory: 1Gi
limits.cpu: "2"
limits.memory: 2Gi
pods: "10"
Deployment Patterns
Rolling Update
apiVersion: apps/v1
kind: Deployment
metadata:
name: easyfactu-api
spec:
replicas: 1
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
template:
spec:
containers:
- name: api
image: ghcr.io/franciscosanchezn/easyfactu-api:v1.2.3
Container Registry Authentication (GHCR)
# Create GHCR pull secret
kubectl create secret docker-registry ghcr-credentials \
--namespace=easyfactu \
--docker-server=ghcr.io \
--docker-username=franciscosanchezn \
--docker-password=$GITHUB_TOKEN
Health Checks
containers:
- name: api
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
startupProbe:
httpGet:
path: /health
port: 8000
failureThreshold: 30
periodSeconds: 2
Troubleshooting
Common Commands
# Check pod status
kubectl get pods -n easyfactu -o wide
# View pod logs
kubectl logs -n easyfactu deployment/easyfactu-api -f
# Describe pod for events
kubectl describe pod -n easyfactu <pod-name>
# Check resource usage
kubectl top pods -n easyfactu
kubectl top nodes
# Check Traefik logs
kubectl logs -n kube-system -l app.kubernetes.io/name=traefik -f
# Check cert-manager
kubectl get certificates -A
kubectl describe certificate -n easyfactu easyfactu-tls
# K3s system logs
journalctl -u k3s -f
Common Issues
| Issue | Cause | Fix |
|---|---|---|
Pod stuck in Pending |
Insufficient resources | Check resource quotas, reduce requests |
Pod CrashLoopBackOff |
Application error | Check kubectl logs, verify env vars |
| Certificate not issued | DNS not pointing to server | Verify DNS A record, check cert-manager logs |
| 503 from Traefik | Service not ready | Check readiness probe, verify service selector |
Dockerfile Best Practices
# Multi-stage build for Python FastAPI
FROM python:3.12-slim AS builder
WORKDIR /app
# Install UV
COPY /uv /usr/local/bin/uv
# Copy dependency files
COPY pyproject.toml uv.lock ./
# Install dependencies
RUN uv sync --frozen --no-dev
# Production stage
FROM python:3.12-slim AS runtime
WORKDIR /app
# Copy installed dependencies
COPY /app/.venv /app/.venv
# Copy application code
COPY src/ ./src/
# Set PATH to use venv
ENV PATH="/app/.venv/bin:$PATH"
# Run as non-root
RUN useradd --create-home appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "easyfactu_api.main:app", "--host", "0.0.0.0", "--port", "8000"]
Cost Reference
| Resource | Cost | Notes |
|---|---|---|
| Hetzner CX22 | ~5€/mo | 2 vCPU, 4GB RAM, 40GB SSD |
| Supabase Free | $0 | 500MB DB, 50K MAU auth |
| GHCR | $0 | Free with GitHub |
| Let's Encrypt | $0 | Free SSL certificates |
| Hetzner DNS | $0 | Free with Hetzner account |
| Total | ~5€/mo |
When Costs May Increase
- Adding a second VPS node (~5€/mo each)
- Upgrading to Supabase Pro ($25/mo) for more storage
- Adding Hetzner Load Balancer (~10€/mo) — avoid with HostPort
- Adding block storage for persistent volumes (~0.05€/GB/mo)
Operational Commands
# Terraform
cd infra/terraform/environments/prod
terraform init
terraform plan -out=tfplan
terraform apply tfplan
# K3s / Kubernetes
sudo k3s kubectl get pods -A # All pods
sudo k3s kubectl logs -f deploy/api -n ns # Follow logs
sudo k3s kubectl apply -k infra/k8s/overlays/prod # Apply Kustomize
sudo k3s kubectl rollout restart deploy/api -n ns # Restart
# Docker / GHCR
docker build -t ghcr.io/franciscosanchezn/app:tag .
docker push ghcr.io/franciscosanchezn/app:tag
# K9s (terminal UI)
k9s --kubeconfig /etc/rancher/k3s/k3s.yaml
Guidelines
- Use HostPort for Traefik to avoid Load Balancer costs
- Set resource requests and limits on all containers
- Use Kustomize overlays for environment-specific configs
- Pin image tags in production (never use
latest) - Configure health checks on all deployments
- Use namespaces for project isolation
- Use letsencrypt-staging for testing to avoid rate limits
- Monitor resource usage to right-size the VPS
- Keep K3s updated for security patches
- Use multi-stage Docker builds for small images
- Target ~5€/mo total infrastructure cost
Weekly Installs
1
Repository
franciscosanche…factu-esFirst Seen
14 days ago
Security Audits
Installed on
mcpjam1
claude-code1
junie1
windsurf1
zencoder1
crush1