This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Kubernetes

Hands-on Kubernetes walk-throughs, cluster recipes, and practical application deployment patterns.

Practical Kubernetes content — installation walk-throughs, troubleshooting recipes, and notes from running clusters in the field.

1 - Retiring kube-proxy IPVS on MicroK8s: An Upgrade Readiness Runbook

A controlled pre-upgrade runbook for MicroK8s clusters that still use kube-proxy IPVS mode: inventory, migrate to iptables, verify Service traffic, and retain a tested rollback path.

A focused runbook for checking whether a MicroK8s cluster needs an IPVS migration before its next Kubernetes minor upgrade.

Scope and outcome

This is not a general CNI replacement guide and it does not change Traefik, IngressRoutes, or application manifests. It covers one question: does this cluster run kube-proxy in ipvs mode, and if so, can it move safely to iptables before the next MicroK8s minor upgrade?

Kubernetes has deprecated kube-proxy IPVS mode. The upstream project expects it to be disabled by default by v1.40 and removed by v1.43. It also notes that IPVS mode still relies on iptables for part of Kubernetes Service behaviour, so it is not a complete replacement for iptables rules.[1]

If the audit finds mode: iptables, leave the proxy mode alone and record the clean result. A migration is only needed when the active kube-proxy configuration explicitly selects ipvs.

Change boundary: run the inventory and verification commands first. Perform the mode switch only in an approved maintenance window, with a tested recovery decision. The examples use placeholders and must be adapted to the cluster under review.

Why this change deserves a Service-focused test plan

Unless a cluster has deliberately installed an alternative, every node runs kube-proxy. It watches Services and EndpointSlices, then programs the node to capture a Service clusterIP and redirect traffic to an endpoint.[2]

That makes this a node-level Service-forwarding change. A successful Traefik dashboard or a healthy Deployment does not prove that ClusterIP, cross-namespace traffic, endpoint churn, or NodePorts still work after kube-proxy reconciles rules.

1. Capture the current state

Start with a read-only evidence bundle. Save command output with the maintenance record; do not copy private addresses, tokens, or real application hostnames into a public runbook.

# Run on the MicroK8s host.
sudo microk8s status --wait-ready
sudo snap list microk8s
sudo snap info microk8s

# Confirm the active kube-proxy mode.
sudo microk8s kubectl -n kube-system get configmap kube-proxy \
  -o jsonpath='{.data.config\.conf}' | grep -E '^mode:'

# Capture the full effective config for the change record.
sudo microk8s kubectl -n kube-system get configmap kube-proxy \
  -o yaml > kube-proxy-configmap.before.yaml

# Record kube-proxy placement, logs, Services, and EndpointSlices.
sudo microk8s kubectl -n kube-system get daemonset kube-proxy -o wide
sudo microk8s kubectl -n kube-system get pods -l k8s-app=kube-proxy -o wide
sudo microk8s kubectl -n kube-system logs daemonset/kube-proxy --tail=200
sudo microk8s kubectl get svc -A -o wide
sudo microk8s kubectl get endpointslice -A

On an IPVS-based node, also capture the kernel-side evidence. These commands are diagnostic only; lack of ipvsadm does not prove the cluster is not using IPVS.

lsmod | grep '^ip_vs' || true
sudo ipvsadm -Ln 2>/dev/null || true
sudo iptables-save > iptables.before.txt
sudo nft list ruleset > nftables.before.txt 2>/dev/null || true

Inventory the dependencies that change risk

Before touching kube-proxy, identify the resources whose traffic depends on it:

# Service types and their selectors.
sudo microk8s kubectl get svc -A \
  -o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,TYPE:.spec.type,CLUSTER-IP:.spec.clusterIP,PORTS:.spec.ports[*].port'

# Workloads and the CNI configuration currently in use.
sudo microk8s kubectl get deploy,daemonset,statefulset -A
sudo microk8s kubectl -n kube-system get configmap -o name
sudo microk8s kubectl get crd

Document separately:

  • Services of type ClusterIP, NodePort, and LoadBalancer.
  • Any MetalLB, external load balancer, or NodePort consumer.
  • CNI customisations and NetworkPolicies.
  • Ingress controllers and one representative route per controller.
  • Stateful workloads, PVCs, and recovery requirements.

MicroK8s uses snap channels: patch releases stay within the installed channel, while a minor upgrade is an intentional refresh to another channel. Its documentation also warns that add-ons and the CNI are not upgraded automatically with the cluster and need independent release-note review.[3]

2. Establish recovery controls

Do not make the proxy mode change during a general application deployment. Use a maintenance window and decide in advance what restores service if verification fails.

Minimum controls:

  1. Confirm recent workload/PVC backups and a restore test appropriate to the applications involved.
  2. Save kube-proxy-configmap.before.yaml, current Service and EndpointSlice listings, and the node rule snapshots.
  3. Record the current MicroK8s channel and whether a snap refresh is held.
  4. Name an operator and a rollback decision point.
  5. Define representative Service probes before the maintenance starts.

For a single-node cluster, a practical rollback decision is: if DNS, a same-namespace ClusterIP, a cross-namespace ClusterIP, or an externally used NodePort fails after the normal reconciliation window, restore the saved kube-proxy ConfigMap and restart the DaemonSet before investigating unrelated add-ons.

3. Prove the tests before the change

Create a temporary, disposable probe namespace. It gives the runbook a repeatable DNS and Service test without depending on an application-specific image.

apiVersion: v1
kind: Namespace
metadata:
  name: proxy-migration-test
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo
  namespace: proxy-migration-test
spec:
  replicas: 2
  selector:
    matchLabels:
      app: echo
  template:
    metadata:
      labels:
        app: echo
    spec:
      containers:
        - name: echo
          image: hashicorp/http-echo:1.0
          args: ["-text=proxy-ok"]
          ports:
            - containerPort: 5678
---
apiVersion: v1
kind: Service
metadata:
  name: echo
  namespace: proxy-migration-test
spec:
  selector:
    app: echo
  ports:
    - name: http
      port: 80
      targetPort: 5678

Validate the illustrative manifest before applying it to any cluster:

sudo microk8s kubectl apply --dry-run=client -f proxy-migration-test.yaml
sudo microk8s kubectl apply --dry-run=server -f proxy-migration-test.yaml

After the approved apply, verify endpoints and in-cluster access:

sudo microk8s kubectl -n proxy-migration-test rollout status deploy/echo
sudo microk8s kubectl -n proxy-migration-test get svc,endpoints,endpointslice

sudo microk8s kubectl -n proxy-migration-test run dns-client \
  --rm -i --restart=Never --image=busybox:1.36 -- \
  sh -ec 'nslookup echo && wget -qO- http://echo'

For a cross-namespace check, run the same request from a short-lived Pod in an existing non-production namespace, targeting http://echo.proxy-migration-test.svc.cluster.local.

4. Change only the proxy mode

First inspect the stored config.conf. Do not replace the ConfigMap with a minimal hand-written one: it may contain cluster-specific settings that must remain intact.

sudo microk8s kubectl -n kube-system get configmap kube-proxy \
  -o jsonpath='{.data.config\.conf}' > kube-proxy.config.before.yaml

Edit a working copy and change only this field:

mode: iptables

Then update the existing ConfigMap from the reviewed configuration. The exact key should be confirmed from the inventory command; the common key is config.conf.

# Replace only the existing config.conf value after peer review.
sudo microk8s kubectl -n kube-system create configmap kube-proxy \
  --from-file=config.conf=./kube-proxy.config.after.yaml \
  --dry-run=client -o yaml > kube-proxy-configmap.after.yaml

# Inspect the generated object before applying it.
sudo microk8s kubectl apply --dry-run=server -f kube-proxy-configmap.after.yaml
sudo microk8s kubectl apply -f kube-proxy-configmap.after.yaml

# Ask each kube-proxy Pod to reload from the updated configuration.
sudo microk8s kubectl -n kube-system rollout restart daemonset/kube-proxy
sudo microk8s kubectl -n kube-system rollout status daemonset/kube-proxy --timeout=5m

Important: kubectl create configmap ... -o yaml produces an object definition. Before applying it, compare it with kube-proxy-configmap.before.yaml and preserve metadata and non-config.conf data required by the installed MicroK8s version. If that comparison is not clean, stop and use the platform’s supported configuration workflow instead.

5. Verify in layers

Run every check below after kube-proxy is ready. Keep the output with the maintenance record.

Control plane and proxy health

sudo microk8s kubectl -n kube-system get pods -l k8s-app=kube-proxy -o wide
sudo microk8s kubectl -n kube-system logs daemonset/kube-proxy --tail=200
sudo microk8s kubectl -n kube-system get configmap kube-proxy \
  -o jsonpath='{.data.config\.conf}' | grep -E '^mode:'

Look for ready Pods, the expected mode: iptables, and no repeated proxy initialisation or rule-programming errors.

Kubernetes DNS and ClusterIP traffic

# Same namespace: DNS plus Service forwarding.
sudo microk8s kubectl -n proxy-migration-test run same-ns \
  --rm -i --restart=Never --image=busybox:1.36 -- \
  sh -ec 'nslookup echo && wget -qO- http://echo'

# Cross namespace: fully-qualified service name.
sudo microk8s kubectl -n <probe-namespace> run cross-ns \
  --rm -i --restart=Never --image=busybox:1.36 -- \
  sh -ec 'nslookup echo.proxy-migration-test.svc.cluster.local && wget -qO- http://echo.proxy-migration-test.svc.cluster.local'

Endpoint churn, NodePort, and ingress paths

  • Scale the disposable echo Deployment from two replicas to one and back to two; confirm the EndpointSlice updates and requests keep succeeding.
  • For every NodePort consumed outside the cluster, test it from the expected LAN client and record the result.
  • Request one representative ingress hostname for every controller in use; verify the controller reaches its backing Service rather than only returning its own health page.
  • Run each application’s existing health check. A Service has no useful traffic path when it has no ready endpoints, regardless of kube-proxy mode.

Kubernetes documents that kube-proxy keeps node rules synchronized with Service and EndpointSlice state. That is why endpoint churn belongs in the verification set rather than being treated as an optional load test.[2]

6. Troubleshooting and rollback

SymptomCheck firstControlled response
kube-proxy Pods are not readyDaemonSet events and Pod logsRestore the saved ConfigMap if the mode change is implicated; do not edit the CNI first.
Service DNS works but requests failEndpointSlices, ready endpoints, and target portsConfirm the Service selector and endpoint readiness before treating it as a proxy failure.
NodePort fails but ClusterIP worksNode firewall, NodePort listener, external client pathCompare node rules and firewall policy with the pre-change snapshots.
Intermittent traffic after the switchkube-proxy logs, endpoint churn, stale node stateWait for normal reconciliation once; if the symptom persists, roll back and collect node-level evidence.
Mode remains IPVS after an editActive ConfigMap key and DaemonSet argumentsRe-read the live ConfigMap; do not assume an edited local file was applied.
CNI or add-on failures appear during the maintenanceComponent versions and release notesIsolate the failure. MicroK8s add-ons and CNI updates are separate upgrade work, not a reason to widen this change.[3][4]

Rollback

# Restore the captured object only after confirming its namespace and contents.
sudo microk8s kubectl apply -f kube-proxy-configmap.before.yaml
sudo microk8s kubectl -n kube-system rollout restart daemonset/kube-proxy
sudo microk8s kubectl -n kube-system rollout status daemonset/kube-proxy --timeout=5m

Repeat the same DNS, ClusterIP, NodePort, ingress, and application probes. Record both the failed condition and the successful rollback result before scheduling another attempt.

7. Continue with the MicroK8s minor upgrade

Only after the proxy mode is recorded as iptables and the Service checks pass should the cluster move to a new MicroK8s minor channel. Review the target channel and the release notes, then use an approved change plan to refresh MicroK8s to that channel. MicroK8s explicitly separates the snap-channel upgrade from add-on and CNI lifecycle work, so assess those components before and after the refresh rather than assuming they update automatically.[3][4]

Operator checklist

  • Captured MicroK8s channel, version, kube-proxy ConfigMap, Services, EndpointSlices, and node rule evidence.
  • Confirmed whether the active mode is ipvs or iptables.
  • Stopped with no migration required when the cluster already used iptables.
  • Reviewed workload recovery controls and rollback decision point.
  • Validated the disposable probe manifest with both client and server dry-runs.
  • Changed only kube-proxy configuration after review.
  • Verified proxy readiness, DNS, same- and cross-namespace ClusterIP traffic, endpoint churn, NodePorts, ingress, and representative application health.
  • Recorded the validated proxy mode before the MicroK8s minor upgrade.

Sources

[1] https://kubernetes.io/blog/2026/07/31/kubernetes-v1-37-sneak-peek — Kubernetes v1.37 Sneak Peek [2] https://kubernetes.io/docs/reference/networking/virtual-ips — Virtual IPs and Service Proxies [3] https://canonical.com/microk8s/docs/upgrading — Upgrading MicroK8s [4] https://canonical.com/microk8s/docs/release-notes — MicroK8s release notes

2 - MicroK8s with Traefik: Public and Private Application Deployment

Complete tutorial for setting up a MicroK8s cluster with Traefik ingress controller, exposing public apps via Cloudflare and private apps locally. Includes security hardening with IP allowlists and automatic image updates with Keel.

MicroK8s Home Lab with Traefik Ingress

Summary

This guide walks through setting up a complete MicroK8s cluster on Ubuntu with Traefik as the ingress controller. The architecture supports two types of applications: public apps exposed to the internet via Cloudflare DNS and TLS, and private apps accessible only from the local network. We’ll deploy containerized workloads running inside Kubernetes pods, proxy traffic to external hosts, and set up monitoring dashboards—all managed through declarative YAML manifests.

Key features of this setup:

  • Traefik v3 as the ingress controller with native Kubernetes CRD support
  • Cloudflare integration with origin certificates for secure public access
  • Namespace isolation between public and private workloads
  • IP allowlist middleware restricting public endpoints to Cloudflare IPs only
  • Keel for automatic container image updates
  • Kubernetes Dashboard for cluster monitoring

Architecture Overview

MicroK8s Architecture Overview
MicroK8s Architecture: Public and Private Application Deployment (Click to expand)

The architecture consists of:

  1. Internet traffic flows through Cloudflare (DNS + proxy + TLS termination)
  2. Cloudflare connects to the origin server using Cloudflare Origin Certificates
  3. Traefik receives traffic on ports 80/443 and routes based on hostname
  4. Public Namespace contains internet-facing applications
  5. Private Namespace contains local-only applications
  6. External proxying allows routing to services running on other hosts

Traffic Flow Diagram

┌─────────────────────────────────────────────────────────────────────────────────┐
│                                  INTERNET                                       │
└─────────────────────────────────────────────────────────────────────────────────┘
                                       │
                                       ▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│                               CLOUDFLARE                                        │
│  • DNS Records (app1.john.doe, app2.john.doe)                                   │
│  • SSL/TLS termination (public side)                                            │
│  • DDoS protection, WAF, caching                                                │
│  • Proxied mode (orange cloud) enabled                                          │
└─────────────────────────────────────────────────────────────────────────────────┘
                                       │
                    Cloudflare Origin Certificate (encrypted)
                                       │
                                       ▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│                         HOST (PRIVATE_IP / PUBLIC_IP)                           │
│  ┌───────────────────────────────────────────────────────────────────────────┐  │
│  │                         TRAEFIK INGRESS                                   │  │
│  │                       (traefik-system namespace)                          │  │
│  │                                                                           │  │
│  │   hostPort 80  ──► HTTP  (redirect to HTTPS / private apps)               │  │
│  │   hostPort 443 ──► HTTPS (public apps with origin cert)                   │  │
│  │   hostPort 8080 ─► Dashboard (local access only)                          │  │
│  └───────────────────────────────────────────────────────────────────────────┘  │
│            │                      │                      │                      │
│  ┌─────────┴─────────┐  ┌─────────┴────────┐  ┌──────────┴────────┐             │
│  │  PUBLIC NAMESPACE │  │  PUBLIC NAMESPACE│  │ PRIVATE NAMESPACE │             │
│  │                   │  │                  │  │                   │             │
│  │  ┌─────────────┐  │  │  ┌────────────┐  │  │  ┌─────────────┐  │             │
│  │  │  app1 POD   │  │  │  │  app2      │  │  │  │ app3 POD    │  │             │
│  │  │  (internal) │  │  │  │  EXTERNAL  │  │  │  │ (local)     │  │             │
│  │  └─────────────┘  │  │  │  PROXY ────┼──┼──┼──► EXTERNAL_IP │  │             │
│  │                   │  │  └────────────┘  │  │  └─────────────┘  │             │
│  │                   │  │                  │  │  ┌─────────────┐  │             │
│  │                   │  │                  │  │  │ app4 POD    │  │             │
│  │                   │  │                  │  │  │ (local)     │  │             │
│  │                   │  │                  │  │  └─────────────┘  │             │
│  └───────────────────┘  └──────────────────┘  └───────────────────┘             │
│                                                                                 │
│  ┌───────────────────────────────────────────────────────────────────────────┐  │
│  │                    MONITORING & MANAGEMENT                                │  │
│  │   • Kubernetes Dashboard (hostPort 9443)                                  │  │
│  │   • Keel (hostPort 9300) - automatic image updates                        │  │
│  └───────────────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────────────┘

Prerequisites

Before starting, ensure you have:

  1. Fresh Ubuntu server (20.04 LTS or newer recommended)
  2. Domain name with DNS managed by Cloudflare
  3. Network access to ports 80/443 from the internet (for public apps)
  4. Cloudflare account (free tier works)

Hardware Requirements

ResourceMinimumRecommended
CPU2 cores4+ cores
RAM4 GB8+ GB
Storage20 GB50+ GB

Step 1: Install MicroK8s

MicroK8s is a lightweight, production-grade Kubernetes distribution from Canonical. It runs as a snap package on Ubuntu.

Install MicroK8s:

# Install MicroK8s
sudo snap install microk8s --classic

# Add current user to microk8s group (avoids sudo for kubectl)
sudo usermod -aG microk8s $USER

# Apply group changes
newgrp microk8s

# Verify installation
microk8s status --wait-ready

Enable required addons:

# DNS - Required for service discovery within the cluster
microk8s enable dns

# Storage - Required for PersistentVolumeClaims
microk8s enable hostpath-storage

# Metrics Server - Required for Kubernetes Dashboard resource metrics
microk8s enable metrics-server

Verify cluster is ready:

# Check node status
microk8s kubectl get nodes

# Expected output:
# NAME            STATUS   ROLES    AGE   VERSION
# your-hostname   Ready    <none>   1m    v1.28.x

Step 2: Prepare Cloudflare Origin Certificate

Cloudflare Origin Certificates encrypt traffic between Cloudflare and your origin server. They’re free and valid for up to 15 years.

Generate Origin Certificate

  1. Log into Cloudflare Dashboard
  2. Select your domain
  3. Navigate to SSL/TLS → Origin Server
  4. Click Create Certificate
  5. Configuration:
    • Private key type: RSA (2048)
    • Hostnames: *.john.doe, john.doe (wildcard + apex)
    • Certificate Validity: 15 years
  6. Click Create
  7. Save the certificate as cert.pem
  8. Save the private key as key.pem

Configure Cloudflare SSL Mode

  1. Go to SSL/TLS → Overview
  2. Set encryption mode to Full (strict)
ModeDescriptionSecurity
OffNo encryption❌ Insecure
FlexibleHTTPS to Cloudflare, HTTP to origin⚠️ Partial
FullHTTPS everywhere, any cert on origin⚠️ Medium
Full (strict)HTTPS everywhere, valid/origin cert✅ Recommended

Configure DNS Records

For each public application, create an A record:

TypeNameContentProxy Status
Aapp1PUBLIC_IPProxied (orange cloud)
Aapp2PUBLIC_IPProxied (orange cloud)

Step 3: Project Structure

Create the following directory structure for your Kubernetes manifests:

mkdir -p ~/my_k8s/{traefik,apps/public,apps/private,scripts}
cd ~/my_k8s

Final structure:

my_k8s/
├── cert.pem                    # Cloudflare origin certificate
├── key.pem                     # Cloudflare origin private key
├── deploy.sh                   # Main deployment script
├── get-dashboard-token.sh      # Helper for K8s dashboard login
├── 00-namespaces.yaml          # Namespace definitions
│
├── traefik/
│   ├── crds.yaml               # Traefik Custom Resource Definitions
│   ├── rbac.yaml               # ServiceAccount & RBAC permissions
│   ├── deployment.yaml         # Traefik Deployment & Service
│   └── cloudflare-ipallowlist.yaml  # Security middleware
│
├── apps/
│   ├── public/                 # Internet-facing applications
│   │   ├── app1.yaml           # App running in K8s pod
│   │   └── app2-external.yaml  # Proxy to external host
│   │
│   └── private/                # Local-only applications
│       ├── app3.yaml           # Private app 1
│       ├── app4.yaml           # Private app 2
│       ├── kubernetes-dashboard.yaml
│       └── keel.yaml           # Automatic image updates
│
└── scripts/
    └── update-cloudflare-ips.sh  # Update Cloudflare IP allowlist

Step 4: Create Namespaces

Namespaces provide logical isolation between workloads. We use separate namespaces for public apps, private apps, and infrastructure components.

File: 00-namespaces.yaml

---
apiVersion: v1
kind: Namespace
metadata:
  name: publicapps
  labels:
    app.kubernetes.io/purpose: public
---
apiVersion: v1
kind: Namespace
metadata:
  name: privateapps
  labels:
    app.kubernetes.io/purpose: private
---
apiVersion: v1
kind: Namespace
metadata:
  name: traefik-system

Why namespace separation matters:

  • Security isolation: RBAC can restrict access per namespace
  • Resource organization: Easy to view/manage related resources
  • Network policies: Can enforce traffic rules between namespaces
  • Resource quotas: Limit CPU/memory per namespace

Step 5: Deploy Traefik Ingress Controller

Traefik is a modern reverse proxy and ingress controller with native Kubernetes support.

Why Traefik?

FeatureBenefit
Kubernetes CRDsIngressRoute resources for flexible routing
Auto-discoveryWatches Kubernetes API for changes
Built-in dashboardVisual monitoring without extra tools
MiddlewareRedirects, headers, authentication, rate limiting
Multiple entrypointsHTTP/HTTPS/TCP/UDP on configurable ports

Traefik Custom Resource Definitions

File: traefik/crds.yaml

The CRDs define custom Kubernetes resources that Traefik uses:

  • IngressRoute: Define routing rules (host matching, path prefixes)
  • Middleware: Transform requests (redirects, headers, auth)
  • ServersTransport: Backend connection settings (TLS verification)
  • TLSOption: TLS configuration (min version, ciphers)
# Abbreviated - full CRDs available at:
# https://doc.traefik.io/traefik/reference/dynamic-configuration/kubernetes-crd/

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: ingressroutes.traefik.io
spec:
  group: traefik.io
  names:
    kind: IngressRoute
    plural: ingressroutes
  scope: Namespaced
  versions:
    - name: v1alpha1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          # ... schema definition

Traefik RBAC Configuration

File: traefik/rbac.yaml

Traefik needs permissions to watch Kubernetes resources:

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: traefik
  namespace: traefik-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: traefik
rules:
  - apiGroups: [""]
    resources: ["services", "endpoints", "secrets"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["extensions", "networking.k8s.io"]
    resources: ["ingresses", "ingressclasses"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["traefik.io"]
    resources: ["ingressroutes", "middlewares", "tlsoptions", "serverstransports"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: traefik
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: traefik
subjects:
  - kind: ServiceAccount
    name: traefik
    namespace: traefik-system

Traefik Deployment

File: traefik/deployment.yaml

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: traefik
  namespace: traefik-system
  labels:
    app: traefik
spec:
  replicas: 1
  selector:
    matchLabels:
      app: traefik
  template:
    metadata:
      labels:
        app: traefik
    spec:
      serviceAccountName: traefik
      containers:
        - name: traefik
          image: traefik:v3.0
          args:
            - --api.insecure=true
            - --entrypoints.web.address=:80
            - --entrypoints.websecure.address=:443
            - --providers.kubernetesingress
            - --providers.kubernetescrd
            - --log.level=INFO
          ports:
            - name: web
              containerPort: 80
              hostPort: 80        # Bind directly to host port
            - name: websecure
              containerPort: 443
              hostPort: 443       # Bind directly to host port
            - name: dashboard
              containerPort: 8080
              hostPort: 8080      # Dashboard on host port
          securityContext:
            capabilities:
              drop:
                - ALL
              add:
                - NET_BIND_SERVICE
            readOnlyRootFilesystem: true
            runAsNonRoot: false
            runAsUser: 0
---
apiVersion: v1
kind: Service
metadata:
  name: traefik
  namespace: traefik-system
spec:
  type: ClusterIP
  selector:
    app: traefik
  ports:
    - name: web
      port: 80
      targetPort: 80
    - name: websecure
      port: 443
      targetPort: 443
    - name: dashboard
      port: 8080
      targetPort: 8080

Why hostPort Instead of NodePort/LoadBalancer?

ApproachProsCons
NodePortStandard K8sUses high ports (30000-32767), requires iptables redirect
LoadBalancerCloud-nativeNeeds cloud provider or MetalLB
hostPortDirect binding to 80/443Only one pod per node, requires root

For a single-node home lab, hostPort is the simplest approach—traffic goes directly to Traefik without extra routing layers.

Step 6: Deploy Public Applications

Pattern 1: App Running in Kubernetes Pod

This pattern deploys a containerized application inside Kubernetes and exposes it publicly via Cloudflare.

File: apps/public/app1.yaml

---
# PersistentVolumeClaims for data persistence
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: app1-data
  namespace: publicapps
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
---
# Deployment - runs the container
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app1
  namespace: publicapps
  labels:
    app: app1
  annotations:
    # Keel annotations for automatic updates
    keel.sh/policy: all
    keel.sh/trigger: poll
    keel.sh/pollSchedule: "@every 30m"
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app1
  template:
    metadata:
      labels:
        app: app1
    spec:
      containers:
        - name: app1
          image: yourregistry/app1:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 8000
          volumeMounts:
            - name: data
              mountPath: /app/data
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: app1-data
---
# Service - internal cluster endpoint
apiVersion: v1
kind: Service
metadata:
  name: app1
  namespace: publicapps
spec:
  selector:
    app: app1
  ports:
    - port: 8000
      targetPort: 8000
---
# IngressRoute - HTTPS routing
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: app1-https
  namespace: publicapps
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`app1.john.doe`)
      kind: Rule
      middlewares:
        - name: cloudflare-only
          namespace: traefik-system
      services:
        - name: app1
          port: 8000
  tls:
    secretName: cloudflare-origin-tls
---
# IngressRoute - HTTP redirect to HTTPS
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: app1-http-redirect
  namespace: publicapps
spec:
  entryPoints:
    - web
  routes:
    - match: Host(`app1.john.doe`)
      kind: Rule
      middlewares:
        - name: redirect-https
        - name: cloudflare-only
          namespace: traefik-system
      services:
        - name: app1
          port: 8000
---
# Middleware - HTTP to HTTPS redirect
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: redirect-https
  namespace: publicapps
spec:
  redirectScheme:
    scheme: https
    permanent: true

Pattern 2: Proxy to External Host

This pattern routes traffic through Traefik to a service running on another host (not in Kubernetes).

File: apps/public/app2-external.yaml

---
# Manual Endpoints - point to external IP
apiVersion: v1
kind: Endpoints
metadata:
  name: app2-backend
  namespace: publicapps
subsets:
  - addresses:
      - ip: EXTERNAL_HOST_IP   # Replace with actual IP (e.g., 10.0.0.6)
    ports:
      - port: 8000
---
# Headless Service - uses the manual endpoints
apiVersion: v1
kind: Service
metadata:
  name: app2-backend
  namespace: publicapps
spec:
  clusterIP: None              # Headless service
  ports:
    - port: 8000
      targetPort: 8000
---
# IngressRoute - HTTPS routing
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: app2-https
  namespace: publicapps
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`app2.john.doe`)
      kind: Rule
      middlewares:
        - name: cloudflare-only
          namespace: traefik-system
      services:
        - name: app2-backend
          port: 8000
  tls:
    secretName: cloudflare-origin-tls
---
# HTTP to HTTPS redirect
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: app2-http-redirect
  namespace: publicapps
spec:
  entryPoints:
    - web
  routes:
    - match: Host(`app2.john.doe`)
      kind: Rule
      middlewares:
        - name: redirect-https
          namespace: publicapps
        - name: cloudflare-only
          namespace: traefik-system
      services:
        - name: app2-backend
          port: 8000

How external proxying works:

  1. Endpoints resource manually defines the external IP:port
  2. Headless Service (clusterIP: None) binds to those endpoints
  3. IngressRoute routes traffic to the service
  4. Traefik forwards requests to the external host

Step 7: Deploy Private Applications

Private applications are accessible only from the local network—no Cloudflare, no TLS certificates required.

File: apps/private/app3.yaml

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app3
  namespace: privateapps
  labels:
    app: app3
  annotations:
    keel.sh/policy: minor
    keel.sh/trigger: poll
    keel.sh/pollSchedule: "@every 6h"
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app3
  template:
    metadata:
      labels:
        app: app3
    spec:
      containers:
        - name: app3
          image: excalidraw/excalidraw:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 80
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
  name: app3
  namespace: privateapps
spec:
  selector:
    app: app3
  ports:
    - port: 80
      targetPort: 80
---
# IngressRoute - Local access via HTTP
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: app3-local
  namespace: privateapps
spec:
  entryPoints:
    - web                      # HTTP only, no TLS
  routes:
    - match: Host(`app3.local`) || Host(`PRIVATE_IP`)
      kind: Rule
      services:
        - name: app3
          port: 80

Access private apps:

  • Via hostname: Add PRIVATE_IP app3.local to /etc/hosts
  • Via IP: Access http://PRIVATE_IP directly

Step 8: Security - Cloudflare IP Allowlist

To ensure public endpoints only accept traffic from Cloudflare (not direct hits to your PUBLIC_IP), we configure an IP allowlist middleware.

File: traefik/cloudflare-ipallowlist.yaml

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: cloudflare-only
  namespace: traefik-system
spec:
  ipAllowList:
    sourceRange:
      # Cloudflare IPv4 ranges
      - 173.245.48.0/20
      - 103.21.244.0/22
      - 103.22.200.0/22
      - 103.31.4.0/22
      - 141.101.64.0/18
      - 108.162.192.0/18
      - 190.93.240.0/20
      - 188.114.96.0/20
      - 197.234.240.0/22
      - 198.41.128.0/17
      - 162.158.0.0/15
      - 104.16.0.0/13
      - 104.24.0.0/14
      - 172.64.0.0/13
      - 131.0.72.0/22
      # Cloudflare IPv6 ranges
      - 2400:cb00::/32
      - 2606:4700::/32
      - 2803:f800::/32
      - 2405:b500::/32
      - 2405:8100::/32
      - 2a06:98c0::/29
      - 2c0f:f248::/32

Update script for Cloudflare IPs:

Cloudflare occasionally updates their IP ranges. Create a script to fetch the latest:

File: scripts/update-cloudflare-ips.sh

#!/bin/bash
set -e

echo "Fetching latest Cloudflare IP ranges..."
IPV4=$(curl -s https://www.cloudflare.com/ips-v4)
IPV6=$(curl -s https://www.cloudflare.com/ips-v6)

cat <<EOF > traefik/cloudflare-ipallowlist.yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: cloudflare-only
  namespace: traefik-system
spec:
  ipAllowList:
    sourceRange:
EOF

for ip in $IPV4; do echo "      - $ip" >> traefik/cloudflare-ipallowlist.yaml; done
for ip in $IPV6; do echo "      - $ip" >> traefik/cloudflare-ipallowlist.yaml; done

echo "Updated traefik/cloudflare-ipallowlist.yaml"
echo "Apply with: microk8s kubectl apply -f traefik/cloudflare-ipallowlist.yaml"

Step 9: Automatic Image Updates with Keel

Keel monitors container registries and automatically updates deployments when new images are pushed.

File: apps/private/keel.yaml

---
apiVersion: v1
kind: Namespace
metadata:
  name: keel
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: keel
  namespace: keel
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: keel
rules:
  - apiGroups: ["", "apps", "extensions", "batch"]
    resources: ["deployments", "daemonsets", "statefulsets", "pods", "services", "secrets", "configmaps"]
    verbs: ["get", "list", "watch", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: keel
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: keel
subjects:
  - kind: ServiceAccount
    name: keel
    namespace: keel
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: keel
  namespace: keel
spec:
  replicas: 1
  selector:
    matchLabels:
      app: keel
  template:
    metadata:
      labels:
        app: keel
    spec:
      serviceAccountName: keel
      containers:
        - name: keel
          image: keelhq/keel:latest
          command: ["/bin/keel"]
          env:
            - name: POLL
              value: "1"
            - name: BASIC_AUTH_USER
              value: "admin"
            - name: BASIC_AUTH_PASSWORD
              value: "admin"        # Change in production!
          ports:
            - containerPort: 9300
              hostPort: 9300        # UI accessible on host

Keel annotations for deployments:

AnnotationDescriptionExample
keel.sh/policyUpdate policyall, major, minor, patch
keel.sh/triggerHow to detect updatespoll, default
keel.sh/pollSchedulePoll frequency@every 30m, @daily

Access Keel UI: http://PRIVATE_IP:9300

Step 10: Kubernetes Dashboard

The Kubernetes Dashboard provides a web UI for cluster monitoring.

File: apps/private/kubernetes-dashboard.yaml

---
apiVersion: v1
kind: Namespace
metadata:
  name: kubernetes-dashboard
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: kubernetes-dashboard
  namespace: kubernetes-dashboard
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: kubernetes-dashboard
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin        # Full access - restrict in production!
subjects:
  - kind: ServiceAccount
    name: kubernetes-dashboard
    namespace: kubernetes-dashboard
---
apiVersion: v1
kind: Secret
metadata:
  name: kubernetes-dashboard-token
  namespace: kubernetes-dashboard
  annotations:
    kubernetes.io/service-account.name: kubernetes-dashboard
type: kubernetes.io/service-account-token
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: kubernetes-dashboard
  namespace: kubernetes-dashboard
spec:
  replicas: 1
  selector:
    matchLabels:
      app: kubernetes-dashboard
  template:
    metadata:
      labels:
        app: kubernetes-dashboard
    spec:
      serviceAccountName: kubernetes-dashboard
      containers:
        - name: kubernetes-dashboard
          image: kubernetesui/dashboard:v2.7.0
          ports:
            - containerPort: 8443
              hostPort: 9443      # Dashboard on host port 9443
          args:
            - --auto-generate-certificates
            - --namespace=kubernetes-dashboard

Get dashboard login token:

#!/bin/bash
# File: get-dashboard-token.sh

TOKEN=$(microk8s kubectl -n kubernetes-dashboard get secret \
  kubernetes-dashboard-token -o jsonpath="{.data.token}" | base64 -d)

echo "Dashboard Token:"
echo "----------------------------------------"
echo "$TOKEN"
echo "----------------------------------------"
echo ""
echo "Access: https://PRIVATE_IP:9443"

Step 11: Deployment Script

Create a single script to deploy everything:

File: deploy.sh

#!/bin/bash
set -e

cd "$(dirname "$0")"

echo "=========================================="
echo "  MicroK8s Cluster Deployment"
echo "=========================================="

# Enable addons
echo "=== Enabling addons ==="
microk8s enable dns hostpath-storage metrics-server
microk8s status --wait-ready

# Create namespaces
echo "=== Creating namespaces ==="
microk8s kubectl apply -f 00-namespaces.yaml

# Create TLS secret
echo "=== Creating TLS secret ==="
microk8s kubectl create secret tls cloudflare-origin-tls \
  --cert=cert.pem \
  --key=key.pem \
  -n publicapps \
  --dry-run=client -o yaml | microk8s kubectl apply -f -

# Deploy Traefik
echo "=== Deploying Traefik ==="
microk8s kubectl apply -f traefik/crds.yaml
microk8s kubectl apply -f traefik/rbac.yaml
microk8s kubectl apply -f traefik/deployment.yaml
microk8s kubectl apply -f traefik/cloudflare-ipallowlist.yaml
microk8s kubectl rollout status deployment/traefik -n traefik-system --timeout=120s

# Deploy apps
echo "=== Deploying public apps ==="
for app in apps/public/*.yaml; do
  [ -f "$app" ] && microk8s kubectl apply -f "$app"
done

echo "=== Deploying private apps ==="
for app in apps/private/*.yaml; do
  [ -f "$app" ] && microk8s kubectl apply -f "$app"
done

# Summary
echo ""
echo "=========================================="
echo "  Deployment Complete"
echo "=========================================="
echo ""
echo "PUBLIC APPS (via Cloudflare):"
echo "  https://app1.john.doe"
echo "  https://app2.john.doe"
echo ""
echo "PRIVATE APPS (local network):"
echo "  http://PRIVATE_IP (app3)"
echo ""
echo "DASHBOARDS:"
echo "  Kubernetes: https://PRIVATE_IP:9443"
echo "  Traefik:    http://PRIVATE_IP:8080/dashboard/"
echo "  Keel:       http://PRIVATE_IP:9300"

Make executable:

chmod +x deploy.sh get-dashboard-token.sh scripts/update-cloudflare-ips.sh

Troubleshooting

Common Issues

1. Pods stuck in ContainerCreating:

microk8s kubectl describe pod <pod-name> -n <namespace>
# Check Events section for errors

2. DNS resolution failing:

# Restart CoreDNS
microk8s kubectl rollout restart deployment/coredns -n kube-system

3. Storage issues (PVC pending):

# Check hostpath-provisioner
microk8s kubectl get pods -n kube-system | grep hostpath
microk8s kubectl rollout restart deployment/hostpath-provisioner -n kube-system

4. Traefik not routing:

# Check Traefik logs
microk8s kubectl logs deployment/traefik -n traefik-system

# Check routes
curl http://localhost:8080/api/http/routers | python3 -m json.tool

5. Certificate errors:

  • Ensure Cloudflare SSL mode is Full (strict)
  • Verify DNS record is Proxied (orange cloud)
  • Check origin cert hasn’t expired

Useful Commands

# View all pods
microk8s kubectl get pods -A

# View logs
microk8s kubectl logs deployment/<name> -n <namespace>

# Enter pod shell
microk8s kubectl exec -it <pod> -n <namespace> -- /bin/sh

# Test routing locally
curl -k -H "Host: app1.john.doe" https://localhost:443

# Reset everything
sudo microk8s reset --destroy-storage
./deploy.sh

Security Considerations

ComponentCurrent StateProduction Recommendation
K8s Dashboardcluster-adminUse RBAC with least privilege
Traefik DashboardNo authAdd BasicAuth middleware
Keel UIBasic authUse strong credentials + HTTPS
Private appsNetwork-only protectionAdd authentication

References


Note: This setup provides a solid foundation for running containerized workloads on a single node. For high availability, consider multi-node MicroK8s clusters or managed Kubernetes services.