The Container Abstraction Problem

The standard cloud deployment stack looks like this: your app → Docker container → VM → hypervisor → physical hardware. Every layer adds latency, steals CPU cycles, and costs money. Most developers accept this as a fact of life.

It's not. Docker runs perfectly on bare metal — and when it does, it's faster, cheaper, and simpler. No hypervisor overhead. No shared CPU steal. No surprise throttling when your cloud neighbor's workload spikes.

This guide covers everything: why bare metal Docker wins, how to set it up from scratch, and actual benchmark numbers.

Bare Metal vs VM: What's Actually Different

On a cloud VM, your Docker containers are competing for resources in two places:

  1. Hypervisor overhead. The host VM itself steals 5-15% CPU just for virtualization. Intel VT-x and AMD-V reduce this, but it's never zero.
  2. CPU steal time. AWS, GCP, and DigitalOcean all oversell physical cores. In peak hours, your "4 vCPU" instance might only get 60% of that.
  3. Network virtualization. Cloud network stacks add ~0.5-2ms per hop vs bare metal's direct NIC access.

On bare metal, Docker talks directly to the Linux kernel. Your containers get 100% of the cores you paid for, guaranteed.

Real Benchmarks

We benchmarked a Node.js API server (Express, PostgreSQL) across equivalent specs: 8 vCPU / 16GB RAM.

TestRAW Bare MetalAWS EC2 (c6i.2xlarge)DigitalOcean (8 vCPU)
Requests/sec (p50)14,2009,8008,400
Latency p504.2ms6.1ms7.4ms
Latency p9918ms34ms41ms
CPU steal (avg)0%4-12%3-9%
Docker pull (1GB image)8.2s14.1s19.7s
Monthly cost$21$209$96

Benchmark: wrk2 at sustained load, 30-second windows, Frankfurt datacenter. April 2026.

Setup: Docker on Bare Metal in 15 Minutes

Here's the full setup from a fresh Ubuntu 24.04 server. No fluff.

1. Install Docker

# Remove any old versions
apt-get remove -y docker docker-engine docker.io containerd runc

# Add Docker's official GPG key
apt-get update
apt-get install -y ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg

# Add the repository
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  | tee /etc/apt/sources.list.d/docker.list > /dev/null

# Install
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# Verify
docker run hello-world

2. Configure for Production

# /etc/docker/daemon.json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 65536,
      "Soft": 65536
    }
  },
  "live-restore": true,
  "userland-proxy": false
}

live-restore: true keeps containers running during Docker daemon restarts — critical for zero-downtime deploys. userland-proxy: false uses iptables directly, cutting latency on port-forwarded connections.

3. Swap in Docker Compose

# docker-compose.yml
services:
  app:
    image: your-app:latest
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
    deploy:
      resources:
        limits:
          cpus: '4'
          memory: 8G

  nginx:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/nginx/certs:ro

4. Zero-Downtime Deploys

#!/bin/bash
# deploy.sh — rolling update with health check

docker compose pull app
docker compose up -d --no-deps --scale app=2 app
sleep 10

# Health check
if curl -sf http://localhost:3000/health; then
  docker compose up -d --no-deps --scale app=1 app
  echo "Deploy successful"
else
  docker compose rollback
  echo "Deploy failed — rolled back"
  exit 1
fi

Docker Swarm vs Kubernetes on Bare Metal

If you need orchestration across multiple bare metal nodes, you have two real options:

  • Docker Swarm. Built into Docker. Takes 5 minutes to set up. Fine for 2-10 nodes. docker swarm init and you're done.
  • K3s. Lightweight Kubernetes that actually runs on bare metal without a PhD. 512MB RAM overhead vs 4GB+ for full k8s. curl -sfL https://get.k3s.io | sh -
  • Full Kubernetes. If you have 10+ nodes and a dedicated ops team. Otherwise it's overkill — and k3s gives you 95% of it.

For most teams: Docker Compose for single-node, K3s for multi-node. Skip full Kubernetes until you're managing 50+ services.

Monitoring Your Docker Containers

# Real-time stats
docker stats

# Set up Prometheus + Grafana in 2 minutes
docker compose -f monitoring.yml up -d

# monitoring.yml
services:
  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3001:3000"
    environment:
      GF_SECURITY_ADMIN_PASSWORD: your-password

Cost Comparison: Docker on Cloud vs Bare Metal

Running 3 services (API, worker, database) with 8 vCPU / 16GB RAM each:

Provider3× 8vCPU/16GBEgress (1TB/mo)Total/month
AWS ECS$627$90$717
GCP Cloud Run$580$120$700
DigitalOcean$288$0 (5TB free)$288
RAW Bare Metal$63$0$63

RAW includes unlimited egress on all plans. AWS charges $0.09/GB outbound.

The One Thing Cloud Docker Does Better

Autoscaling. If your traffic is spiky and unpredictable (think: viral moments, cron-heavy batch jobs), cloud managed containers let you scale to zero and back. On bare metal you pay for capacity whether you use it or not.

But here's the thing: most apps don't need autoscaling. They need consistent, affordable performance. Size your bare metal server with 30% headroom and you'll be fine for years. You can always add a second node.

Try It

Deploy a Docker-ready bare metal server in under 60 seconds. Ubuntu 24.04, Docker pre-installed, SSH access immediately.

$ npx rawhq deployDeploy Free Server →