---
title: Self-Hosted on Bare Metal
description: "Run the full Caged stack on your own hardware with Firecracker microVMs."
---

# Self-Hosted on Bare Metal

Run Caged on your own servers for maximum control, data sovereignty, and cost efficiency at scale. This recipe walks through setting up a single bare-metal node running Firecracker VMs.

## Prerequisites

- A dedicated server with:
  - Linux (Ubuntu 22.04 recommended)
  - KVM support (`/dev/kvm` present)
  - 32+ GB RAM (more = more concurrent sandboxes)
  - SSD storage
- Docker and Docker Compose installed
- A domain with DNS access

## Hardware Sizing

| Concurrent Sandboxes | RAM  | CPU | Storage | Example Server |
|---------------------|------|-----|---------|----------------|
| 10-20               | 64GB | 16c | 500GB SSD | Hetzner AX42 |
| 30-50               | 128GB | 32c | 1TB NVMe | Hetzner AX102 |
| 50-100              | 256GB | 64c | 2TB NVMe | Hetzner AX162 |

Each sandbox uses 128MB-8GB RAM + 1-8 vCPUs depending on configuration.

## Step 1: Clone and Configure

```bash
git clone https://github.com/caged-dev/caged.git
cd caged

# Copy the example environment file
cp .env.example .env
```

Edit `.env`:

```bash
# .env — Caged self-hosted configuration

# Domain (your server's domain)
CAGED_DOMAIN=caged.your-company.com
CAGED_API_URL=https://api.caged.your-company.com

# Database
POSTGRES_PASSWORD=your-secure-password-here
POSTGRES_DB=caged

# Redis
REDIS_PASSWORD=your-redis-password

# Object Storage (MinIO for self-hosted)
MINIO_ROOT_USER=caged
MINIO_ROOT_PASSWORD=your-minio-password
S3_BUCKET=caged-snapshots

# NATS
NATS_TOKEN=your-nats-token

# Secrets encryption key (generate with: openssl rand -hex 32)
ENCRYPTION_KEY=your-256-bit-hex-key

# Runtime mode
RUNTIME_MODE=firecracker   # or "docker" for testing

# Admin credentials (first user)
ADMIN_EMAIL=admin@your-company.com
ADMIN_PASSWORD=your-admin-password
```

## Step 2: Download Firecracker

```bash
# Download Firecracker binary
ARCH=$(uname -m)
FC_VERSION="1.7.0"
curl -L "https://github.com/firecracker-microvm/firecracker/releases/download/v${FC_VERSION}/firecracker-v${FC_VERSION}-${ARCH}.tgz" | tar xz

sudo mv release-v${FC_VERSION}-${ARCH}/firecracker-v${FC_VERSION}-${ARCH} /usr/local/bin/firecracker
sudo mv release-v${FC_VERSION}-${ARCH}/jailer-v${FC_VERSION}-${ARCH} /usr/local/bin/jailer

# Verify
firecracker --version
```

## Step 3: Build VM Images

```bash
# Build the base rootfs images
cd images/

# Node.js 20 image
./build-rootfs.sh node-20

# Python 3.12 image
./build-rootfs.sh python-312

# Go 1.22 image
./build-rootfs.sh go-122

# Minimal Alpine image
./build-rootfs.sh minimal
```

This produces rootfs files in `images/output/`:

```
images/output/
├── node-20.ext4
├── python-312.ext4
├── go-122.ext4
├── minimal.ext4
└── vmlinux          # Shared kernel
```

## Step 4: Start the Stack

```bash
# Start all services
docker compose up -d

# Verify everything is running
docker compose ps
```

```
NAME                 STATUS          PORTS
caged-server         Up (healthy)    0.0.0.0:8080->8080/tcp
caged-postgres       Up (healthy)    5432/tcp
caged-redis          Up (healthy)    6379/tcp
caged-clickhouse     Up (healthy)    8123/tcp
caged-minio          Up (healthy)    9000/tcp, 9001/tcp
caged-nats           Up (healthy)    4222/tcp
```

## Step 5: Configure Reverse Proxy

Use Caddy for automatic HTTPS:

```bash
# /etc/caddy/Caddyfile

api.caged.your-company.com {
    reverse_proxy localhost:8080
}

*.preview.caged.your-company.com {
    reverse_proxy localhost:8080
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }
}

caged.your-company.com {
    reverse_proxy localhost:3000   # Web dashboard
}
```

```bash
sudo systemctl restart caddy
```

## Step 6: Verify Installation

```bash
# Check API health
curl https://api.caged.your-company.com/health

# Login with admin credentials
caged login
# API URL: https://api.caged.your-company.com
# API Key: (get from dashboard or API)

# Create a test sandbox
caged run --template minimal --cpus 1 --memory 128

# Connect and verify
caged connect cage-xxxxx
# You should get a shell inside a Firecracker VM
```

## Step 7: Set Up KVM Permissions

```bash
# Ensure the caged-server user can access /dev/kvm
sudo usermod -aG kvm caged
sudo chmod 666 /dev/kvm

# Configure jailer directories
sudo mkdir -p /srv/jailer
sudo chown caged:caged /srv/jailer
```

## Docker Mode (Testing Only)

For development/testing without Firecracker:

```bash
# In .env
RUNTIME_MODE=docker

# Start the stack
docker compose up -d

# Sandboxes will be Docker containers instead of VMs
# ⚠️ NOT suitable for production — weaker isolation
```

## Updating

```bash
# Pull latest code
git pull origin main

# Rebuild and restart
docker compose build
docker compose up -d

# Run migrations
docker compose exec server /app/server migrate up
```

## Monitoring

```bash
# Check sandbox resource usage
curl https://api.caged.your-company.com/admin/metrics \
  -H "Authorization: Bearer admin_token"

# View logs
docker compose logs -f server

# ClickHouse events (all agent actions, file changes, etc.)
docker compose exec clickhouse clickhouse-client \
  --query "SELECT * FROM events ORDER BY timestamp DESC LIMIT 10"
```

## Tips

<Tip>
**Start with Docker mode**: Get the stack running with Docker first, then switch to Firecracker once you've verified everything works.
</Tip>

<Tip>
**Overcommit RAM carefully**: Firecracker VMs use balloon devices, but if all sandboxes use max memory simultaneously, you'll OOM. Plan for 70% utilization.
</Tip>

<Tip>
**Use NVMe storage**: Firecracker VM boot time is heavily influenced by disk I/O. NVMe gives sub-200ms cold starts.
</Tip>

<Tip>
**Backup PostgreSQL and MinIO**: These hold all your state. Set up daily backups with pg_dump and MinIO's mirror feature.
</Tip>

<Warning>
**Never expose Firecracker's API socket externally.** The management socket should only be accessible by the Caged server process.
</Warning>
