My containers were renting rooms in a stranger's house — shared kernel, shared VPS. The fix wasn't abandoning Docker: it was running it inside a Firecracker microVM on Krova Cloud with its own kernel. Setup, compose, and honest trade-offs inside.
The moment came during a routine docker ps on the shared "container hosting" VPS where my side projects lived. The list was longer than I expected — other tenants' containers humming next to mine. Then the second thought: they're all running on the same kernel as me.
Containers were never the border. As packaging, Docker is gorgeous — same compose file from laptop to server. The problem was the landlord: my reproducible containers renting rooms in a house full of strangers, sharing the foundation. Namespaces partition the view, not the physics; a kernel bug in the box next to yours is, in the strictest sense, your bug now.
The industry's answer is real but heavy (gVisor, Kata, runtime classes, profile reviews). I didn't need a fleet. I needed a smaller house.
Docker inside a microVM
On Krova Cloud a Cube is a Firecracker microVM — own kernel, no public IP — and images ship with Docker Engine preinstalled, so the stack just moves:
krova images # shows the slugs; pick the Docker-preinstalled variant
krova cubes create home-1 \
--image ubuntu-24.04-docker \
--ssh-key "$(cat ~/.ssh/id_ed25519.pub)" \
--vcpu 2 --ram 4 --disk 40
The compose file is untouched:
# compose.yaml — identical to my laptop
services:
web:
build: ./web
ports: ["8080:8080"]
depends_on: [db]
db:
image: postgres:16
volumes: ["pgdata:/var/lib/postgresql/data"]
volumes:
pgdata:
Ship it and attach the domain (traffic enters via managed TLS ingress — the Cube itself has no public address):
krova ssh home-1 -- 'git clone git@github.com:me/stack.git && cd stack && docker compose up -d'
krova domains add home-1 --domain app.example.com --port 8080
Verify what actually changed
$ krova ssh home-1 -- docker ps --format '{{.Names}}'
stack-web-1
stack-db-1 # the list I expect. nothing else.
$ krova ssh home-1 -- uname -r
6.8.0 # a kernel nobody else is renting
$ krova ssh home-1 -- ip -brief addr
eth0 UP 10.0.x.x/24 # private NAT'd network; no public IP to scan
Three properties did the work:
- The sharing didn't disappear — the strangers did. My containers still share a kernel; now it's mine. A neighbor's kernel bug can't reach me because there is no neighbor.
- No public IP. Default-deny inbound; only ports I explicitly open are reachable (and those can be IP-allowlisted). I open none.
- Boringly cheap. Sized exactly to my stack — 2 vCPU / 4 GB / 40 GB runs about $10/mo, billed by the minute. I kept all of Docker's convenience and moved the one thing that was never Docker's job: the boundary.
The honest part, precisely
- Inside the Cube, my containers share my kernel with each other. If one of my containers gets popped, the others are in reach. Acceptable, because they're all mine — the blast radius is my estate, not a stranger's. The boundary moved to the right place, not a perfect place.
- Running other people's code? User uploads, plugins, AI agents — that's a different threat and the wrong shape; you want a microVM per workload there, not per host. Different post, different tool.
- Operational stuff stays yours. Backups of the named volume, image updates, compose hygiene — a private kernel doesn't do ops for you.
Whose kernel?
"Containers vs VMs" was never the question. It's whose kernel. Packaging and isolation got mashed together for a decade, and we defended the mash-up with config reviews instead of asking that out loud.
Keep the packaging. Change the landlord.
Top comments (0)