DEV Community

Cover image for Docker Is Fine. The Host Was the Problem.
Dhruv Malaviya
Dhruv Malaviya

Posted on

Docker Is Fine. The Host Was the Problem.

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
Enter fullscreen mode Exit fullscreen mode

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:
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Three properties did the work:

  1. 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.
  2. No public IP. Default-deny inbound; only ports I explicitly open are reachable (and those can be IP-allowlisted). I open none.
  3. 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)