Skip to main content
Back to Blog
engineeringminecraftkubernetesopen-sourcefounding-story

Why we built CraftCtrl: The story of $300/month Minecraft server bills

Jon
5 min read

How a friend's runaway hosting bill turned into a Kubernetes operator, a lazymc integration, and an open-source platform for cost-efficient Minecraft server management.

In early 2024, a friend of mine sent me a screenshot of his cloud hosting bill. He was running three Minecraft servers for different friend groups — a vanilla survival world, a modded playthrough using a popular 1.20 pack, and a creative server for building. The bill was $290. I asked him how many hours per day these servers were actually being played. He said maybe six or seven hours combined across all three.

He was paying for 72 server-hours per day and using about six.

I told him to just shut them down when nobody was playing. He said he tried that — he'd turn them off at midnight and wake up to messages from people in different time zones who couldn't connect. He'd tried cron jobs. Someone always wanted to play at the wrong time.

That conversation started CraftCtrl.

The obvious solution that doesn't quite work

The first thing I tried was simple: write a script that polls the Minecraft RCON interface for player count, and stop the server process when it hits zero. Restart it on a cron job every 30 minutes so it's available in case someone wants to join.

This works, technically. But the experience is terrible. Players check the server list, see it's offline, give up. Or they wait 30 minutes for the cron to run. The problem isn't stopping the server — it's having something smart enough to wake it up exactly when someone wants to connect.

That's when I found lazymc, a Rust project by Tim Visee. lazymc is a TCP proxy that sits in front of your Minecraft server. When the JVM is stopped, lazymc keeps listening on port 25565. When a client sends a server list ping, lazymc intercepts it and returns a real-looking status response — "Server starting, please wait." When the client sends a login packet, lazymc triggers the server to start and bridges the connection once it's ready.

From the player's perspective: the server shows up in the list, they click connect, they wait 10-15 seconds (roughly how long a Minecraft server takes to fully load), and they're in. No admin intervention. No 30-minute wait.

This was the insight that made CraftCtrl worth building.

From a shell script to a Kubernetes operator

Once I had lazymc working, I started wiring it into something more manageable. My friend needed to run three servers. I had five of my own across different groups. Manual lazymc configuration files and shell scripts weren't going to scale.

The natural home for this kind of workload is Kubernetes. I was already running a k3s cluster on a pair of Hetzner nodes for other projects. The cost was a fraction of what you'd pay on AWS or GCP for equivalent specs — a meaningful advantage when your users are mostly individuals trying to avoid $300/month bills.

The Kubernetes operator pattern made sense for Minecraft servers specifically because:

  • Servers have desired state (running, stopped, version, memory allocation) and actual state (pod status, player count, backup completion)
  • Controllers naturally handle reconciliation loops — if a pod crashes, the operator restarts it
  • Persistent volumes give you world data that outlives the container, which is table stakes for Minecraft
  • Namespace-per-tenant gives you isolation essentially for free

I wrote the first version of the operator using controller-runtime, the same library that powers most production Kubernetes operators. Each Minecraft server is a MinecraftServer Custom Resource Definition:

apiVersion: craftctrl.io/v1alpha1
kind: MinecraftServer
metadata:
  name: survival
spec:
  type: paper
  version: "1.21.4"
  memory: 4Gi
  lazymc:
    enabled: true
    idleTimeout: 300

The operator reconciles this against real Kubernetes resources. lazymc runs as a sidecar container alongside the Minecraft server container. When the idle timeout fires, the operator stops the Minecraft JVM while leaving the lazymc sidecar running. The PVC holding the world data is untouched. When a player connects, the operator restarts the JVM and lazymc bridges the session.

Resource profile when sleeping: 64 Mi RAM, 0.02 CPU cores. Resource profile when players are online: whatever you configured. For a server sitting idle 18 hours per day, that's a 90%+ reduction in resource consumption during idle hours — which works out to roughly 60-80% of the total monthly cost depending on your play patterns.

Why ConnectRPC, and why it matters

The first version of the API was plain REST with Gin. It worked but it had the problems REST always has: inconsistent error shapes, no type safety across client and server, hand-written clients in every language.

I rewrote the API layer around ConnectRPC with protobuf as the source of truth. The proto files in backend/proto/craftctrl/ define every service and message shape. Generated code handles serialization, the Go server handler stubs, and the TypeScript and Go client libraries.

The practical benefit: when I add a new field to a server resource in the proto, it propagates to the dashboard, the Go SDK, and the Python SDK without any manual updates. The browser dashboard uses gRPC-web via Connect's transport. Log streaming uses server-side streaming RPCs instead of polling.

I probably wouldn't have chosen this architecture for a simpler project. But CraftCtrl's API surface is large — servers, backups, plugins, configurations, scheduled tasks, analytics, whitelists, RCON, templates — and keeping that consistent across multiple clients without generated code would become a maintenance problem quickly.

Multi-tenancy from the start

One of the earlier architectural decisions I'm glad I made: treating multi-tenancy as a first-class concern from day one rather than bolting it on later.

Each user in CraftCtrl corresponds to a tenant. Each tenant's servers live in a dedicated Kubernetes namespace. The namespace name encodes the tenant ID: craftctrl-{tenantID[:8]}-{serverName}. Namespace-scoped RBAC means even if two tenants share cluster nodes, they cannot access each other's Kubernetes resources. Tenant quotas — max servers, max RAM, storage limits — are enforced at the API layer before any Kubernetes resources are created.

Building this in early meant the API was always designed around tenant context, which makes adding a billing layer much cleaner. When you subscribe to the Pro plan, your quota record updates and you can create more servers. When you cancel, the quota drops and you can't create new ones (existing servers keep running).

Open source philosophy

CraftCtrl is Apache 2.0 licensed. The entire platform — operator, API, frontend, Helm chart — is on GitHub and free to self-host.

The reason for this is practical more than ideological: the target audience for self-hosted CraftCtrl is technically sophisticated. They'll look at the source before trusting their world data to it. Proprietary software with "trust us" assurances doesn't work well for this audience.

The managed SaaS exists because most people don't want to operate a Kubernetes cluster. That's legitimate — cluster operations are a real skill, and Hetzner infra plus monitoring plus cert-manager plus DNS management is a non-trivial stack to maintain. The SaaS wraps the same open-source software. If you run it yourself, you get the same features with no artificial limitations.

Where it is now

The core feature set is complete: server CRUD, automatic sleep/wake, backups to S3, plugin and mod management, configuration editing, real-time logs, scheduled tasks, analytics, whitelist management, RCON console. Python and Go SDKs. Multi-tenant isolation.

The commercial layer is in progress: Clerk for authentication, Stripe for billing, per-tenant rate limiting, production Kubernetes on Hetzner. Launching soon.

The thing I'm most pleased with is the sleep/wake implementation. My friend's three-server setup now costs about $35/month, down from $290. Players haven't complained about the startup time. Most of them didn't even notice anything changed.

That's the right outcome.


CraftCtrl is open source on GitHub. Managed hosting starts at $9.99/month at craftctrl.io. Docs at docs.craftctrl.io.