Skip to content

Instantly share code, notes, and snippets.

@knowsuchagency
Created December 18, 2025 18:49
Show Gist options
  • Select an option

  • Save knowsuchagency/5b8ff6f381f8d2ae806b701bb64d90c7 to your computer and use it in GitHub Desktop.

Select an option

Save knowsuchagency/5b8ff6f381f8d2ae806b701bb64d90c7 to your computer and use it in GitHub Desktop.
Docker container hardening plan for Dokploy with gVisor

Docker Container Hardening Plan for Dokploy

This guide hardens Docker container isolation on a system running Dokploy with untrusted public templates.

Overview

Step Action Risk Level
1 Backup current configuration None
2 Apply Docker daemon security defaults Low
3 Install gVisor (runsc) Medium
4 Test with gVisor as optional runtime Low
5 Set gVisor as default runtime Medium

Prerequisites

# Ensure you have root/sudo access
sudo -v

# Check current Docker version (gVisor requires 19.03+)
docker --version

Step 1: Backup Current Configuration

# Create backup directory
mkdir -p ~/docker-hardening-backup

# Backup existing daemon.json (if exists)
sudo cp /etc/docker/daemon.json ~/docker-hardening-backup/daemon.json.bak 2>/dev/null || echo "No existing daemon.json"

# Save list of running containers
docker ps --format '{{.Names}}' > ~/docker-hardening-backup/running-containers.txt

# Export current Docker info
docker info > ~/docker-hardening-backup/docker-info.txt

Step 2: Apply Docker Daemon Security Defaults

2.1 Create/Edit daemon.json

# Check if file exists and has content
cat /etc/docker/daemon.json 2>/dev/null

# If empty or doesn't exist, create new:
sudo tee /etc/docker/daemon.json << 'EOF'
{
  "no-new-privileges": true,
  "live-restore": true,
  "userland-proxy": false,
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 65536,
      "Soft": 65536
    },
    "nproc": {
      "Name": "nproc",
      "Hard": 4096,
      "Soft": 4096
    }
  }
}
EOF

If you have existing configuration, merge the settings manually.

2.2 Apply Changes

# Validate JSON syntax
sudo python3 -c "import json; json.load(open('/etc/docker/daemon.json'))"

# Restart Docker (live-restore keeps containers running)
sudo systemctl restart docker

# Verify daemon is running
sudo systemctl status docker

# Verify settings applied
docker info | grep -E "Default Runtime|Security Options"

2.3 Test

# Test that containers still start
docker run --rm alpine echo "Daemon config OK"

# Verify no-new-privileges is active
docker run --rm alpine cat /proc/1/status | grep NoNewPrivs
# Should show: NoNewPrivs: 1

Step 3: Install gVisor

3.1 Add gVisor Repository (Ubuntu/Debian)

# Install prerequisites
sudo apt-get update && sudo apt-get install -y apt-transport-https ca-certificates curl gnupg

# Add gVisor GPG key
curl -fsSL https://gvisor.dev/archive.key | sudo gpg --dearmor -o /usr/share/keyrings/gvisor-archive-keyring.gpg

# Add repository
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/gvisor-archive-keyring.gpg] https://storage.googleapis.com/gvisor/releases release main" | sudo tee /etc/apt/sources.list.d/gvisor.list > /dev/null

# Install runsc
sudo apt-get update && sudo apt-get install -y runsc

3.2 Register gVisor Runtime with Docker

# Update daemon.json to include gVisor runtime
sudo tee /etc/docker/daemon.json << 'EOF'
{
  "no-new-privileges": true,
  "live-restore": true,
  "userland-proxy": false,
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 65536,
      "Soft": 65536
    },
    "nproc": {
      "Name": "nproc",
      "Hard": 4096,
      "Soft": 4096
    }
  },
  "runtimes": {
    "runsc": {
      "path": "/usr/bin/runsc"
    }
  }
}
EOF

# Restart Docker
sudo systemctl restart docker

Step 4: Test gVisor (Optional Runtime)

Before making gVisor default, test it explicitly:

# Basic test
docker run --rm --runtime=runsc alpine echo "gVisor works!"

# Verify it's actually using gVisor (should show "runsc" in kernel version)
docker run --rm --runtime=runsc alpine uname -r

# Test a more complex workload
docker run --rm --runtime=runsc nginx:alpine nginx -t

# Test network connectivity
docker run --rm --runtime=runsc alpine ping -c 3 8.8.8.8

Known gVisor Limitations

Some workloads may not work with gVisor:

Incompatibility Examples
Raw sockets Some network tools (nmap, tcpdump)
Kernel modules Containers requiring specific kernel features
GPU access CUDA workloads
Some syscalls Specialized system calls not yet implemented

Test your specific Dokploy services:

# List your containers
docker ps --format '{{.Names}}'

# For each critical service, test with gVisor
# Example: stop container, recreate with --runtime=runsc

Step 5: Set gVisor as Default Runtime

Only proceed if Step 4 testing was successful.

# Backup current config
sudo cp /etc/docker/daemon.json ~/docker-hardening-backup/daemon.json.before-gvisor-default

# Update to use gVisor as default
sudo tee /etc/docker/daemon.json << 'EOF'
{
  "no-new-privileges": true,
  "live-restore": true,
  "userland-proxy": false,
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 65536,
      "Soft": 65536
    },
    "nproc": {
      "Name": "nproc",
      "Hard": 4096,
      "Soft": 4096
    }
  },
  "default-runtime": "runsc",
  "runtimes": {
    "runsc": {
      "path": "/usr/bin/runsc"
    }
  }
}
EOF

# Restart Docker
sudo systemctl restart docker

# Verify default runtime
docker info | grep "Default Runtime"
# Should show: Default Runtime: runsc

Running Specific Containers WITHOUT gVisor

If a container doesn't work with gVisor, override with runc:

# In docker run
docker run --runtime=runc ...

# In docker-compose.yml (for Dokploy compose deployments)
services:
  myservice:
    runtime: runc
    ...

Rollback Procedures

Rollback Step 5 (gVisor Default → Optional)

# Remove default-runtime line, keep gVisor available
sudo tee /etc/docker/daemon.json << 'EOF'
{
  "no-new-privileges": true,
  "live-restore": true,
  "userland-proxy": false,
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 65536,
      "Soft": 65536
    },
    "nproc": {
      "Name": "nproc",
      "Hard": 4096,
      "Soft": 4096
    }
  },
  "runtimes": {
    "runsc": {
      "path": "/usr/bin/runsc"
    }
  }
}
EOF

sudo systemctl restart docker

Rollback Step 3 (Remove gVisor Entirely)

# Remove gVisor from daemon.json
sudo tee /etc/docker/daemon.json << 'EOF'
{
  "no-new-privileges": true,
  "live-restore": true,
  "userland-proxy": false,
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 65536,
      "Soft": 65536
    },
    "nproc": {
      "Name": "nproc",
      "Hard": 4096,
      "Soft": 4096
    }
  }
}
EOF

sudo systemctl restart docker

# Optionally uninstall gVisor
sudo apt-get remove -y runsc
sudo rm /etc/apt/sources.list.d/gvisor.list
sudo rm /usr/share/keyrings/gvisor-archive-keyring.gpg

Rollback Step 2 (Remove All Hardening)

# Restore original daemon.json
sudo cp ~/docker-hardening-backup/daemon.json.bak /etc/docker/daemon.json 2>/dev/null

# Or if no original existed, remove it
sudo rm /etc/docker/daemon.json

sudo systemctl restart docker

Emergency: Docker Won't Start

# Check what's wrong
sudo journalctl -u docker -n 50

# Common fix: invalid JSON
sudo python3 -c "import json; json.load(open('/etc/docker/daemon.json'))"

# Nuclear option: remove config entirely
sudo rm /etc/docker/daemon.json
sudo systemctl restart docker

# Restore containers if needed
cat ~/docker-hardening-backup/running-containers.txt
# Manually restart via Dokploy UI

Verification Checklist

After completing all steps:

  • docker info | grep "Default Runtime" shows expected runtime
  • docker run --rm alpine cat /proc/1/status | grep NoNewPrivs shows NoNewPrivs: 1
  • All Dokploy services are running (docker ps)
  • Dokploy UI is accessible
  • Can deploy a test application through Dokploy

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment