Self-directed learning project
Self-hosted k3s cluster on AWS with Flask/PostgreSQL and Cloudflare
A single-node Kubernetes cluster on EC2 running a public Flask app over Traefik and Cloudflare — with the API server closed to the internet and reachable only over a Tailscale mesh.
- Cloud
- Kubernetes
- Networking
01 Problem
I wanted production-shaped Kubernetes experience — ingress, stateful workloads, public exposure, secure admin access — on a single low-cost cloud instance rather than a managed control plane.
02 Approach
Deployed a single-node k3s cluster and ran a containerized Flask application backed by PostgreSQL as a StatefulSet.
Published it to the internet through the bundled Traefik ingress with Cloudflare in front for DNS and TLS, while keeping the Kubernetes API server private: the AWS security group closes port 6443 to the public and the API is reachable only over a Tailscale mesh, so kubectl works from my Windows machine without ever exposing the control plane.
I also built a separate multi-node variant with kubeadm running inside WSL2, joining the EC2 node over Tailscale.
03 Architecture
Two paths, deliberately separate: the public data plane, and the admin path that never touches the internet.
04 Stack
- k3s v1.35.4
- Ubuntu 24.04 (t3.small)
- Traefik
- klipper-lb
- flannel CNI
- PostgreSQL StatefulSet
- Docker
- Tailscale
- Cloudflare
- AWS EC2 / Security Groups
05 Result
A working public web app on infrastructure I stood up end to end.
Built as a test cluster and since decommissioned.
06 What broke, and what fixed it
-
Control-plane OOM restart loop
Diagnosed and fixed by pinning
--node-ipto a single interface. -
EXTERNAL-IPadvertisingCorrected by setting explicit
--tls-sanvalues. -
Exposure boundary
Separated public data-plane reachability from private API access.