{"id":6,"date":"2026-03-31T09:17:30","date_gmt":"2026-03-31T09:17:30","guid":{"rendered":"https:\/\/blog.lennardjohn.org\/?p=6"},"modified":"2026-03-31T09:58:34","modified_gmt":"2026-03-31T09:58:34","slug":"i-built-a-production-platform-just-to-write-a-blog","status":"publish","type":"post","link":"https:\/\/blog.lennardjohn.org\/?p=6","title":{"rendered":"I Built a Production Platform\u2026 Just to Write a Blog"},"content":{"rendered":"\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"683\" src=\"https:\/\/blog.lennardjohn.org\/wp-content\/uploads\/2026\/03\/infra-1024x683.png\" alt=\"\" class=\"wp-image-20\" srcset=\"https:\/\/blog.lennardjohn.org\/wp-content\/uploads\/2026\/03\/infra-1024x683.png 1024w, https:\/\/blog.lennardjohn.org\/wp-content\/uploads\/2026\/03\/infra-300x200.png 300w, https:\/\/blog.lennardjohn.org\/wp-content\/uploads\/2026\/03\/infra-768x512.png 768w, https:\/\/blog.lennardjohn.org\/wp-content\/uploads\/2026\/03\/infra.png 1536w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">I just wanted to write a blog.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That\u2019s it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">No scaling requirements. No users. No revenue.<br>Just a place to write.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So naturally, I built a 3-node Kubernetes cluster on bare metal, automated everything with Terraform and Ansible, added GitOps, monitoring, alerting, and exposed it to the internet using a zero-trust Cloudflare tunnel.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Completely normal.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83e\udd14 Why Over-Engineer Something So Simple?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Because the goal wasn\u2019t the blog.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The goal was to learn how real platforms are built.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Not tutorials. Not \u201chello world\u201d deployments.<br>But the actual systems that sit behind production environments.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So I treated this like a real platform:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Fully automated<\/li>\n\n\n\n<li>Reproducible<\/li>\n\n\n\n<li>Observable<\/li>\n\n\n\n<li>Secure by design<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u2699\ufe0f The One Command That Does Everything<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Everything starts with:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker compose up<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That single command:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Provisions infrastructure (Terraform \u2192 Proxmox)<\/li>\n\n\n\n<li>Configures the cluster (Ansible \u2192 Kubernetes)<\/li>\n\n\n\n<li>Deploys platform services (Ingress, monitoring, GitOps)<\/li>\n\n\n\n<li>Deploys applications (WordPress + MariaDB)<\/li>\n\n\n\n<li>Sets up public access (Cloudflare Tunnel)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">About 25 minutes later\u2026 the entire platform is live.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">No manual steps. No SSH after bootstrap.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83c\udfd7\ufe0f How It\u2019s Actually Built<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">At a high level:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Git \u2192 CI\/CD \u2192 Terraform \u2192 VMs \u2192 Ansible \u2192 Kubernetes \u2192 Argo CD \u2192 Apps<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Infrastructure (Terraform)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Creates 3 VMs on Proxmox (1 control plane, 2 workers)<\/li>\n\n\n\n<li>Configures Cloudflare tunnel + DNS<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Configuration (Ansible)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Bootstraps Kubernetes using kubeadm<\/li>\n\n\n\n<li>Installs core services (Ingress, storage, networking)<\/li>\n\n\n\n<li>Deploys monitoring and applications<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">GitOps (Argo CD)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Watches the <code>kubernetes\/<\/code> directory<\/li>\n\n\n\n<li>Automatically syncs changes to the cluster<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">After the initial build, I don\u2019t SSH into anything anymore.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Everything is managed through Git.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83d\udd01 GitOps in Action<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This is my favourite part.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I change a file in Git \u2192 push \u2192 and Argo CD updates the cluster automatically.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">No kubectl. No manual deploys.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Just Git.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That shift \u2014 from \u201crun commands\u201d to \u201cdeclare desired state\u201d \u2014 completely changes how you think about systems.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83d\udd10 Zero Trust (No Open Ports)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Nothing in my home network is exposed directly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead, everything goes through a Cloudflare Tunnel:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>User \u2192 Cloudflare Edge \u2192 Tunnel \u2192 Kubernetes Ingress \u2192 App<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>No port forwarding<\/li>\n\n\n\n<li>No public IP exposure<\/li>\n\n\n\n<li>HTTPS handled at the edge<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">It\u2019s simple, secure, and surprisingly powerful.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83d\udcca Observability (Because Things WILL Break)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">I added:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Prometheus (metrics)<\/li>\n\n\n\n<li>Grafana (dashboards)<\/li>\n\n\n\n<li>AlertManager (email alerts)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">If:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>a pod crashes<\/li>\n\n\n\n<li>a node goes down<\/li>\n\n\n\n<li>memory spikes<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">I get notified.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Because a platform you can\u2019t observe\u2026 is a platform you don\u2019t understand.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83d\udca5 What Went Wrong (Important Part)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This didn\u2019t work the first time.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Some of the issues I hit:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Terraform not returning VM IPs (guest agent problems)<\/li>\n\n\n\n<li>Ansible running before infrastructure was ready<\/li>\n\n\n\n<li>Kubernetes probes killing WordPress before it stabilised<\/li>\n\n\n\n<li>Infinite HTTPS redirect loops behind Cloudflare<\/li>\n\n\n\n<li>PVC-related deployment deadlocks<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Most of these came down to one thing:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">I assumed things would be ready\u2026 when they weren\u2019t.<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83e\udde0 Biggest Lesson<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Stop designing systems that depend on timing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Start designing systems that <strong>converge<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u201cwait 30 seconds and hope\u201d<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Do:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>retry until ready<\/li>\n\n\n\n<li>check actual state<\/li>\n\n\n\n<li>design for failure<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">That shift alone made everything more reliable.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83d\udd01 Reproducibility (This Is the Real Win)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The entire platform can be:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Destroyed<\/li>\n\n\n\n<li>Rebuilt<\/li>\n\n\n\n<li>Moved<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Using the same codebase.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That means:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>No snowflake servers<\/li>\n\n\n\n<li>No hidden config<\/li>\n\n\n\n<li>No \u201cit only works on my machine\u201d<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83c\udfaf What This Project Actually Demonstrates<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This isn\u2019t about WordPress.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It demonstrates:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Infrastructure as Code (Terraform)<\/li>\n\n\n\n<li>Configuration as Code (Ansible)<\/li>\n\n\n\n<li>GitOps (Argo CD)<\/li>\n\n\n\n<li>Zero Trust Networking (Cloudflare)<\/li>\n\n\n\n<li>Observability-first design<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">In other words:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">How modern platforms are actually built.<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83d\ude80 What\u2019s Next<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">I\u2019m planning to:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Move this to a Talos-based cluster<\/li>\n\n\n\n<li>Add multi-environment support (dev\/staging\/prod)<\/li>\n\n\n\n<li>Implement proper secrets management (SOPS or Vault)<\/li>\n\n\n\n<li>Explore multi-cluster deployments<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83d\udc4b Final Thoughts<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Yes \u2014 this is overkill for a blog.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">But that\u2019s kind of the point.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you can build something like this for a simple project\u2026<br>you can build it for something that actually matters.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">If you want to check it out:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\ud83c\udf10 <a href=\"https:\/\/lennardjohn.org\">https:\/\/lennardjohn.org<\/a><\/li>\n\n\n\n<li>\ud83d\udcbb <a href=\"https:\/\/github.com\/Lennardj\/homelab-blog\">https:\/\/github.com\/Lennardj\/homelab-blog<\/a><\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">I\u2019m Lennard \u2014 currently transitioning into DevOps \/ Platform Engineering.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you\u2019re on the same journey, feel free to connect \ud83d\udc47<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>I just wanted to write a blog. That\u2019s it. No scaling requirements. No users. No revenue.Just a place to write. So naturally, I built a 3-node Kubernetes cluster on bare metal, automated everything with Terraform and Ansible, added GitOps, monitoring, alerting, and exposed it to the internet using a zero-trust Cloudflare tunnel. Completely normal. \ud83e\udd14 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-6","post","type-post","status-publish","format-standard","hentry","category-devops"],"_links":{"self":[{"href":"https:\/\/blog.lennardjohn.org\/index.php?rest_route=\/wp\/v2\/posts\/6","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.lennardjohn.org\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.lennardjohn.org\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.lennardjohn.org\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.lennardjohn.org\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=6"}],"version-history":[{"count":2,"href":"https:\/\/blog.lennardjohn.org\/index.php?rest_route=\/wp\/v2\/posts\/6\/revisions"}],"predecessor-version":[{"id":21,"href":"https:\/\/blog.lennardjohn.org\/index.php?rest_route=\/wp\/v2\/posts\/6\/revisions\/21"}],"wp:attachment":[{"href":"https:\/\/blog.lennardjohn.org\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=6"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.lennardjohn.org\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=6"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.lennardjohn.org\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=6"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}