CI/CD pipeline for this site — GitHub Actions, GHCR and a self-hosted runner
🟢 This one isn't a case study — you're looking at its output right now. Every page on opswithzaeem.com is served by a container built, versioned and shipped by the pipelines below, running on a Kubernetes cluster I built and operate. Started Aug 30, 2026; migrated from a laptop to a three-node homelab cluster on Sep 11–12, 2026, with zero downtime.
What it does
The same commit reaches production three different ways — GitHub Actions pushing, ArgoCD pulling, and TeamCity — all ending at one set of Helm charts. Each is a genuinely different model, and the interesting part is what each one costs:
| Who starts it | Who deploys | Where the cluster credentials live | |
|---|---|---|---|
| GitHub Actions | a version tag | the pipeline, reaching into the cluster | on my self-hosted runner |
| ArgoCD | a commit that changes the desired version | an agent inside the cluster, pulling | nowhere outside the cluster |
| TeamCity | pressing Run on a build chain | the TeamCity agent | on the same lab machine |
One release path is normal. Three exist because I wanted to understand the trade-offs by operating them, not by reading about them — and because the application never has to know which one is in front of it.
The GitHub Actions path is the simplest to show:
git tag v1.4.0 && git push origin v1.4.0Three things are worth reading off that diagram. A push never ships — it runs tests and stops; shipping is always a second, deliberate act. All three paths converge on one helm upgrade, which is why the application never has to know which ran. And ArgoCD is CD only: it deploys images that already exist and builds nothing, so the commit that changes image.tag is the deployment request — which is why that workflow holds no registry credentials and no kubeconfig at all.
GitHub Actions picks up the tag, dispatches the build to a self-hosted runner (my own machine, registered as the repo's build agent), builds the site image, pushes it to GitHub Container Registry under an immutable version, and deploys it to Kubernetes with Helm. Rolling back is the previous version number, live in seconds.
The same commit can reach production three different ways — GitHub Actions pushing, ArgoCD pulling, or TeamCity — because all three end at the same Helm charts. That is the point of the design, and the rest of this page is how it works.
Architecture
Diagram source
flowchart TD
DEV["git push to main"] --> CI["CI — GitHub's hosted runner\n368 tests · lint · type-check\nruns on EVERY push, builds nothing"]
CI --> GATE{"how should this ship?"}
GATE -->|"git tag vX.Y.Z"| GHA["GitHub Actions — release\nBUILD + DEPLOY"]
GATE -->|"press Run"| TC["TeamCity — build chain\nBUILD + DEPLOY\ntags tc-N, its own namespace"]
GATE -->|"promote: commit the version"| GIT["git now declares\nwhat production should run"]
GHA --> PROBE{"version already\nin the registry?"}
PROBE -->|yes| SKIP["skip — versions are immutable"]
PROBE -->|no| RUNNER
TC --> RUNNER["ONE LAB MACHINE\nActions runner + TeamCity agent\ndocker · helm · kubectl · kubeconfig"]
RUNNER --> GHCR[("GHCR\nvX.Y.Z or tc-N\nplus a sha-commit receipt")]
GHCR --> HELM["helm upgrade — the same 6 charts\n--wait --atomic"]
GIT --> ARGO["ArgoCD — INSIDE the cluster\ncompares git to reality,\npulls on Sync. CD only: it\ndeploys images, never builds them"]
ARGO --> HELM
HELM --> K8S["Kubernetes — 3 nodes, kubeadm"]
ARGO -.->|"any change it did not make\nshows as DRIFT"| K8S
V["visitor"] --> CFE["Cloudflare edge\nTLS · cache · Always-Online"]
CFE -.->|"tunnel dialled OUTWARD\nzero open ports on the origin"| CFD["cloudflared pods ×2"]
CFD --> ING["ingress-nginx\none JSON log line per request"]
ING --> K8S
style RUNNER fill:#8b5cf6,color:#fff
style GHCR fill:#22c55e,color:#fff
style K8S fill:#f59e0b,color:#fff
style ARGO fill:#0ea5e9,color:#fff
style TC fill:#0ea5e9,color:#fffThe engineering decisions, and why
- Immutable versions, no latest. Every release gets vX.Y.Z plus a sha-<commit> receipt; a version is never rebuilt or re-pushed. latest is deliberately absent — deploys name an exact version, so the deploy command itself is the audit trail, and "what is running in production?" is answered by one line of config plus docker inspect reading the commit off the container's OCI labels.
- A version names the set, not an image. All six images build from the same commit at the same version in one run, so image-to-image version drift is impossible by construction. The workflow probes the registry first and skips versions that already exist — the local release script and CI produce interchangeable, collision-free output.
- Zero secrets in CI. The site pre-renders 189 pages of Notion-backed content at build time — but CI holds no Notion token at all. Content is recorded once into a snapshot image in GHCR; builds replay it with zero API calls, and a hole in the snapshot fails the build loudly instead of shipping thin. Auth to the registry is GitHub's per-run GITHUB_TOKEN, minted and destroyed per run — no long-lived credential exists anywhere in the pipeline.
- The registry is the interface. Everything upstream of GHCR (who builds: laptop → hosted runner → self-hosted runner, three swaps in one day) and downstream (who runs it: laptop today, VPS or Kubernetes next) changes independently, because both sides only agree on a versioned image name. The app never noticed any of it.
- Zero open ports. The origin dials out to Cloudflare through a tunnel; there is no inbound path to the machine at all. TLS, caching and Always-Online failover live at the edge.
- Self-hosted runner economics. GitHub's hosted runners are fresh 2-vCPU VMs — a cold release took 40+ minutes. The self-hosted runner keeps a warm Docker layer cache between jobs and does the full six-image set in under 15 minutes, at zero cost. CI checks stay on hosted runners so every push is tested even when the build machine is offline.
Debugged in production, same day
Three real incidents, found and fixed during bring-up — the part of CI/CD no tutorial covers:
- A swallowed rate limit became silent data corruption. A Notion 429 during content recording was caught by a graceful-degradation handler and baked seven "content unavailable" pages into three consecutive releases — invisible, because 105 of 112 pages survived the count-based guard. Fix: error handling now distinguishes setup errors (degrade with a notice) from transient errors (throw — fail the build, leave the cache empty so the next request retries). The recorder now heals itself: a rate-limited attempt fails loudly and the resumable retry fills the gap.
- DOCKER_CONFIG isolation hid more than credentials. The workflow isolates Docker's config per job so CI's ephemeral token can't overwrite the machine's real registry login — which also removed buildx, because Docker discovers CLI plugins in $DOCKER_CONFIG/cli-plugins. Fix: symlink the plugin into the isolated config — the job gets buildx without getting the credentials.
- "Works in my terminal, fails as a service" — the defining self-hosted-runner failure class: macOS folder privacy blocking a background service (exit 126), and Docker Desktop's socket living at ~/.docker/run/docker.sock rather than /var/run. Machine-specific config went into the runner's own .env, keeping the workflow file portable to the future Linux VPS runner.
Results
- Release: one git tag, ~15 minutes, unattended, six images, tested on every push beforehand
- Deploy: one config line + one command; rollback in seconds to any previous version
- Security: no registry PAT in CI, no CMS token in CI, no open ports on the origin, non-root containers
- 40+ min → under 15 min per release by owning the build machine
- Full runbooks in the repo: registry & versioning guide, from-scratch production setup, phased roadmap to VPS and Kubernetes — written so any engineer could operate it
GitHub Actions Docker buildx GHCR self-hosted runner Cloudflare Tunnel Next.js semantic versioning Notion API
The lab it runs on
Production is a mini PC in my apartment running Proxmox, carved into five machines. Nothing here is managed for me — I built the cluster with kubeadm, joined the nodes, installed the CNI, and own every failure.
Diagram source
flowchart TB
subgraph PROX["Proxmox host — one mini PC"]
direction TB
A["ansible-controller\n192.168.0.51\nGitHub runner + TeamCity agent\nhelm, kubectl, docker"]
D["mariadb\n192.168.0.52\nTeamCity's database"]
subgraph K8S["kubeadm cluster — v1.37.0, Calico, containerd 2.2.1"]
M["master1\n192.168.0.53\ncontrol plane"]
W1["worker1\n192.168.0.54\nsite, logs, metrics"]
W2["worker2\n192.168.0.55"]
end
end
A -->|"helm upgrade / kubectl"| M
K8S -->|"TeamCity server pod"| D
style A fill:#8b5cf6,color:#fff
style K8S fill:#0ea5e9,color:#fffOne machine does all the building. The GitHub Actions runner and the TeamCity agent both live on the Ansible controller, because that is where Docker, helm, kubectl and the kubeconfig are. The consequence worth stating: no cluster credential exists outside the lab. GitHub orchestrates; my hardware executes.
Three ways to production
The same commit can reach the cluster three ways. They differ in only two things: who starts it, and who holds the keys.
Diagram source
flowchart LR
C["commit on main"] --> T1["git tag vX.Y.Z"]
C --> T2["press Run\nin TeamCity"]
C --> T3["Actions: promote\n(commits image.tag)"]
T1 --> GA["GitHub Actions\nbuild + deploy"]
T2 --> TC["TeamCity\nbuild chain"]
T3 --> GIT["git says what\nproduction should run"] --> AR["ArgoCD\nin-cluster, pulls"]
GA --> H["helm upgrade\nthe same charts"]
TC --> H
AR --> H
H --> K["Kubernetes"]
style H fill:#22c55e,color:#fff
style K fill:#f59e0b,color:#fff| Triggered by | Cluster credentials live | |
|---|---|---|
| GitHub Actions | a version tag | on the lab runner |
| ArgoCD (GitOps) | a commit, then a human clicking Sync | only inside the cluster |
| TeamCity | pressing Run | in the TeamCity agent |
Push versus pull is the real distinction. Actions and TeamCity reach into the cluster and change it, then stop — between runs nothing is watching. ArgoCD runs inside the cluster and continuously compares git against reality, so a deploy made by another pipeline shows up as drift within seconds, without anything reporting it. Detection is automatic; correction is one flag (selfHeal), which I keep off so a human approves production.
I run ArgoCD on manual sync deliberately — it means the three paths never fight over the same objects. Two systems that both believe they own a Deployment is the failure mode that takes a site down, and I have the outage to prove it.
What a deploy actually does
Every pipeline ends in one script. The charts follow my employer's per-resource layout — one Helm release per concern, so helm history gives a separate audit trail for storage, secrets, config, networking and the app.
Diagram source
flowchart TD
S["deploy.sh — IMAGE_TAG=vX.Y.Z"] --> A["1 ingress-nginx · 2 pvc · 3 secrets\nwhat the app mounts must exist first"]
A --> B["4 configurations · 5 svc\nConfigMap, Service, Ingress"]
B --> C["6 deployments — site + cloudflared\n--wait --atomic"]
C --> OK["pods Ready, or the release rolls itself back"]
style OK fill:#22c55e,color:#fffThe order is a dependency order: storage and secrets exist before anything mounts them, the Service exists before the tunnel looks for it, the app comes last.
--wait --atomic is there because of a real outage. helm upgrade validates manifests, not registries — STATUS: deployed only ever meant Kubernetes accepted the object. A build once failed without pushing its image, the deploy ran anyway, and Helm reported success for a tag that did not exist. The kubelet found out minutes later. Now Helm blocks until pods are genuinely Ready and restores the previous revision if they are not.
How a request reaches the site
Diagram source
flowchart LR
V["visitor"] --> CF["Cloudflare edge\nTLS, cache, Always-Online"]
CF -.->|"outbound tunnel\nNO open ports"| CFD["cloudflared pods\n2 replicas"]
CFD --> ING["ingress-nginx\none JSON log line per request\nPrometheus histograms"]
ING --> POD["site pod\n~190 pre-rendered pages"]
POD --> VOL[("PVCs\nrendered HTML survives restarts")]
style CF fill:#f59e0b,color:#fff
style ING fill:#0ea5e9,color:#fffThere is no inbound path to the cluster at all. The ingress Service is ClusterIP — not NodePort, not LoadBalancer — so nothing is reachable from the LAN, let alone the internet. The only way in is the tunnel, and it is dialled outward from inside.
That property is also what made the migration invisible: a Cloudflare tunnel accepts multiple connectors at once, so the cluster's pods joined the same tunnel the laptop was serving, traffic shifted over, and the laptop was removed. Visitors saw nothing.
Watching it
Self-hosted Prometheus, Grafana, Loki and Fluent Bit — using the same chart structure as the rest.
Diagram source
flowchart LR
subgraph LOGS["logs"]
FB["fluent-bit forwarder\nDaemonSet, every node"] --> AGG["aggregator"]
AGG --> FILE["one directory per container\n/var/portfolio"]
AGG --> LOKI[("Loki\nqueryable")]
end
subgraph METRICS["metrics"]
NGINX["ingress-nginx"] --> PROM[("Prometheus")]
NODE["node-exporter"] --> PROM
end
LOKI --> GRAF["Grafana"]
PROM --> GRAF
style GRAF fill:#f59e0b,color:#fffThe ingress is where observability comes from, because it is the only component that sees every request — including the ones the app never renders: 404s, bot scans, cache hits. That gives per-page request counts, per-country breakdown, latency histograms and the error rate an SLO is measured against.
Fluent Bit is Filebeat's job in this stack, and Loki is Elasticsearch's — chosen because Loki indexes labels rather than full text, which is what makes it affordable on three small nodes.
Failures worth keeping
The pipeline work that taught me most was not the parts that worked.
- A readiness probe handed a third party an outage switch. The probe hit a page that renders from Notion. Notion rate-limited, the probe timed out, the kubelet pulled the pod from the Service, and nginx returned 503 for the entire site — every cached page included. A readiness probe must never depend on something you do not control. It now hits a static handler.
- A liveness probe manufactured the outage it existed to prevent. Same page, 1-second timeout, during a slow render. The kubelet killed a perfectly healthy process and threw away its warm cache, guaranteeing the next render was slow too. Liveness answers "is this wedged beyond recovery?" — it must be cheap and forgiving.
- A green pipeline while the site was down. One radio button on a build dependency said run anyway when the upstream build failed. Helm happily deployed an image that had never been pushed, and because the site deploys with Recreate, the old pod was already gone. Three faults, one outage, and the most useful thing on this page.
- CI built from a stale artifact for a week. The content snapshot is the one input no pipeline produces — mutable, hand-made, :latest. Everything else is versioned and immutable, so that is exactly where drift collected. A pipeline is only as reproducible as its least-tracked input.
How the site itself is built
The site is a Next.js application with Notion as its CMS — I write every learning note and page in Notion, and the site turns them into fast static pages. The clever part is when the work happens:
- At build time, the pipeline pre-renders every page (about 190 of them) into plain HTML inside the Docker image. Visitors never wait for the Notion API — they get pages that were finished before the container even started.
- When I edit a page in Notion, a webhook tells the site within seconds, and only that page is re-rendered and swapped in. No rebuild, no release, no downtime — writing content and shipping code are completely separate lanes.
- Rendered pages live on a Docker volume, so they survive restarts and redeploys. A page is rendered once, ever.
- Content for CI builds comes from a recorded snapshot image in the registry, so the build machine needs no Notion credentials and makes zero API calls.
Diagram source
flowchart LR
N["Notion\n(I write here)"] -->|webhook, seconds| S["Site container\nre-renders ONE page"]
N -->|recorded once| SNAP["content snapshot\nimage in GHCR"]
SNAP -->|zero API calls| B["release build\npre-renders every page"]
B --> S
V["visitor"] --> CF["Cloudflare"] --> SHow a Notion page becomes a web page
Notion is the source of truth for every word on this site — this page included. A Notion page is not a document but a tree of typed blocks, and the whole renderer is a function from that tree to React.
Diagram source
flowchart TD
API["Notion API\ngetPage + getBlockChildren\n(the only two calls this site makes)"] --> TREE["block tree\nparagraph, code, callout,\ntable, column_list, toggle …"]
TREE --> R["Blocks.tsx\none React component per block type"]
R --> SH["code blocks → Shiki\nhighlighted on the server"]
R --> MM["mermaid blocks → drawn\nin the browser"]
R --> HTML["finished HTML"]
style HTML fill:#22c55e,color:#fffOnly two API calls exist. getPage for a page's properties, getBlockChildren for its contents — called recursively, because a toggle contains blocks which contain blocks. Everything else in the site is built on those two, which is also why recording them is enough to replay an entire build offline.
Around 30 block types are handled, including the ones most Notion renderers skip: multi-column layouts, synced blocks, tables with header rows, callouts, equations, bookmarks and embeds. Code blocks are syntax-highlighted with Shiki on the server, so no highlighting library is shipped to the browser; mermaid diagrams are the exception and draw client-side, because a diagram needs layout measurement a server cannot do.
When the work happens — the part that makes it fast
Diagram source
flowchart TD
W["I edit a page in Notion"] -->|"webhook, seconds"| V["/api/revalidate\nverifies the signature"]
V --> RP["revalidatePath\nfor that ONE page"]
RP --> RE["the page re-renders\non the next request"]
RE --> DISK[("rendered HTML written\nto a PVC — survives restarts")]
REC["reconcile loop\nevery 30 minutes"] -.->|"safety net for\nmissed webhooks"| RP
style DISK fill:#f59e0b,color:#fffThree things make this work, and each exists because the naive version failed:
- A page is rendered once, ever. Next.js normally keeps rendered pages in memory, so every restart re-renders everything — which for ~190 Notion-backed pages means a stampede of API calls against a 3 req/s limit. A custom cache handler writes each rendered page to disk instead, and that directory is a PVC. Restart the pod, redeploy, reschedule it to another node: the HTML is still there.
- Editing does not rebuild. The webhook names the page that changed and only that path is purged. Writing content and shipping code are separate lanes — this paragraph reached the site in seconds without a pipeline running.
- A reconcile loop closes the gap. Webhooks get missed. A background pass walks the tree every 30 minutes and catches anything the webhook did not. It runs at 30 rather than 10 minutes for a measured reason: one Notion integration has a single 3 req/s budget, shared by the loop, on-demand renders, search-engine crawlers and any snapshot recording running on a laptop. All four at once produced nothing but 429s, and the webhook is the fast path anyway — the net only has to close eventually.
Why builds need no Notion credentials at all
The pre-render at build time reads from a recorded snapshot — every response those two API calls ever gave, captured once into a tiny image in the registry. A build replays it and makes zero API calls, which is what lets CI hold no CMS token.
Replay is strict on purpose: a missing entry is a hard error, never a silent fall back to the network. An "offline build" that quietly reaches for the internet when it is short of data is not offline, and the failure it hides is a half-empty site shipped to production — which happened once, before the rule existed.
The free tools — how every one of them works
The site ships 39 free tools for systems and DevOps engineers. The design rule behind all of them: everything computes in your browser — what you paste never leaves the page (the only two exceptions, DNS Lookup and My IP, need a server by nature and say so right on the page). Each tool exists because of a real task from real ops work.
🌐 Networking
- Subnet / VLSM Calculator — you give it a network in CIDR form (like 10.0.0.0/16) and it does the binary math to show exactly which addresses it covers, and how to slice it into smaller subnets. Every firewall rule and VPC plan starts with this arithmetic.
- My IP — shows the public address the internet sees for you, which is usually not what your machine thinks it has, because your router's NAT sits in between. Used whenever you allow-list yourself on a server.
- DNS Lookup — asks the DNS system "what does this name point to?" for any record type. Half of all outage investigations start with "does the name even resolve?" — this answers it, and shows mail (MX) and verification (TXT) records before a cutover.
- IP & Domain Owner Lookup — before you block or report an address, two questions matter: is it a real public address, and whose network is it? The tool first recognizes special ranges (private, loopback, carrier-grade NAT) and explains them; real public addresses get a registry lookup.
- MAC Lookup — the first three bytes of any MAC address are a vendor code. Paste an address from a DHCP table and the tool tells you who made the device — the fastest way to name unknown hardware on your network.
🔐 Security & Keys
- CHMOD Calculator — translates Unix file permissions between the number form (644) and the letter form (rw-r--r--) both ways, with checkboxes so you can see which permission each digit grants. SSH flatly refuses a private key with the wrong mode — this is how you know the right one.
- Password Generator — builds strong random secrets in your browser using the cryptographic randomness API, with length and character controls. Nothing is sent or stored anywhere.
- Hash Generator — computes checksums (SHA-256 and friends) of any text you paste. A hash is a fingerprint: if two files produce the same hash, they are byte-for-byte identical — how you verify a downloaded ISO or compare configs across servers.
- SSL Certificate Toolkit — a guided, numbered walkthrough of the whole certificate procedure: generate the CSR, check the issued certificate, confirm the key matches, get the chain order right. Each step catches its classic mistake before it becomes an outage.
- Scam Link Checker — reads a suspicious link's text without ever opening it, and points out the tricks scammers rely on: look-alike letters (paypa1), a real brand parked in front of an unrelated domain, characters from other alphabets, credentials smuggled before an @.
- RSA Key Generator — creates a public/private key pair in the browser for demos and test environments, no openssl needed. The page itself tells you production keys should be minted on the machine that keeps them.
☁️ DevOps & Cloud
- docker run → Compose — paste a long docker run command from some README and get the equivalent docker-compose.yml. The compose file is reviewable and lives in git; the one-liner gets lost in shell history.
- Kubernetes Resource Calculator — does the scheduler's arithmetic: pods are placed by requests (never limits), and a node's usable capacity is what's left after the OS and kubelet take their share. Enter your pod sizes and node types; it tells you pods per node and nodes needed.
- systemd Unit Generator — builds a service unit file with the mistakes pre-fixed: Restart= so the service doesn't die at 3 a.m. and stay dead, a non-root user, absolute paths that work at boot.
- nginx Reverse-Proxy Generator — generates the ten lines of nginx that sit in front of almost every web app, with the pitfalls handled: the proxy headers (so your app doesn't log every visitor as 127.0.0.1), the websocket upgrade, the HTTP→HTTPS redirect.
- Log-Line Parser Playground — write a parsing pattern and watch it run against your pasted log line on every keystroke, instead of testing blind in production. Presets for nginx logs, IPs, timestamps.
- SLA & Error Budget — turns "99.9% uptime" into what it really permits: 8.8 hours of downtime a year. Enter a target, get the error budget per day/month/year — the number on-call planning actually uses.
- Cron Parser — reads a cron expression (0 3 * * 1) back to you in plain English and shows the next run times, so a misread field doesn't fire your backup at the wrong hour.
- YAML ⇄ JSON — converts both ways. Kubernetes speaks YAML, APIs speak JSON, and this is also the quickest way to find the indentation error a YAML parser is complaining about.
- Regex Tester & Guide — regular expressions are write-only until you can watch them match. Type a pattern, paste your real log lines, and see the matches highlight live — with a guide of recipes from real ops work.
- Text Diff — compares two texts side by side the way Beyond Compare does: changed lines aligned row against row, hatched gaps where one side has extra lines, the exact changed words darkened within each line, and arrows to copy a difference from one side to the other. Both panes stay editable the whole time.
- Galera Health Check — paste the raw output of one MySQL status query and it reads the nine variables that matter for a Galera database cluster (quorum, sync state, flow control) and gives a verdict with the exact next command for each finding.
- HAProxy Config Linter — catches the load-balancer config mistakes that pass human review and then cause outages: a use_backend pointing at a backend that doesn't exist, servers without health checks that keep receiving traffic after they die. With line numbers, before you reload.
- Load Test Sizer (Gatling) — answers the first question of every load test: "how many virtual users produce my target requests per second?" It applies Little's Law (users = rate × time-in-system) and hands you a paste-ready Gatling setup.
- K8s YAML Maker — pick one of 13 Kubernetes resource kinds, fill in a form, and read the manifest being written live — including the scheduling parts nobody remembers by heart: nodeAffinity expressions, anti-affinity spreads, tolerations, topology constraints, probes and security contexts, in the exact key order kubectl uses.
🧰 Utilities
- Number Base Converter — converts between binary, octal, decimal and hex (permissions are octal, netmasks are hex, flags are binary), auto-detecting prefixes like 0x, and lays out a byte's bits with their values so you can see why the number is what it is.
- Unix Timestamp Converter — logs and JWTs record time as a big number of seconds (or milliseconds — the ambiguity is the trap). Converts both directions for correlating log lines across systems.
- UUID & ULID Generator — generates the collision-proof IDs distributed systems need. It explains the practical difference: v4 is pure random; v7 and ULID sort by creation time, which keeps database indexes fast.
- URL Encoder & Inspector — URLs can't carry spaces or special characters raw, so they get percent-encoded — and encoding bugs surface as broken callbacks. Encode, decode, and take a URL apart into its pieces.
- Data Size Converter — disks are sold in GB (powers of 1000) but Linux and Kubernetes count in GiB (powers of 1024) — a 7% gap that quietly breaks capacity math. This converts honestly between the two families.
- Base64 Encode / Decode — Base64 turns binary data into safe printable text; it's how Kubernetes stores Secrets. Encode and decode locally.
- QR & Barcode Generator — turns text or a link into a QR code, or a number into a proper Code 128 barcode — using the same set-optimization real barcode printers use, so it scans identically to commercial output.
- JSON Formatter & Viewer — format, minify and validate JSON without the data leaving the page. One missing comma breaks a consumer; this finds it.
- XML Formatter & Viewer — the same for XML, which SOAP services and plenty of telecom provisioning APIs still speak.
- JWT Decoder — a JWT login token is three Base64 parts; this decodes the header and claims locally so you can see why a login fails — expired, wrong audience, wrong issuer — without pasting your token into some random website's server.
- Markdown Editor — write on the left, see the rendered result on the right, then download or copy. READMEs, runbooks and PR descriptions are all Markdown.
- Significant Figures — shows which digits of a number are actually significant and why (1.20 promises more precision than 1.2), then rounds values to the precision you truly measured — the honesty rules of capacity math and engineering reports.
🤖 AI
- Prompt Token Counter & Trimmer — AI models bill per token and cut off at a context limit, but token counts are invisible while you write. This estimates the count live as you type (honestly labelled an estimate — exact numbers need the vendor's own tokenizer) and helps trim a prompt down to budget.
💡 A design I built, measured, and then deleted. For two weeks the pipeline built five extra images — one per tool category — so each could run as its own credential-free container. Once the cluster existed I checked what they were actually worth, and the honest answer was: nothing yet. All five were identical to the megabyte, because the build never excluded other categories' pages; the only difference was a label. Partitioning them properly would have meant five pods and a thirty-rule Ingress map that silently 404s a tool the day someone forgets to update it — to save about five megabytes. Meanwhile they cost 35 of every 50 minutes of release time.
So I removed them. The split that did pay for itself was site-versus-tools — a tools image drops the pre-rendered notes, 144 MB down to 80 — and that one is still available the day /tools needs to scale on its own. Knowing which optimisation not to make is the part that took experience to learn.