This guide hardens Docker container isolation on a system running Dokploy with untrusted public templates.
| 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 |
# Ensure you have root/sudo access
sudo -v
# Check current Docker version (gVisor requires 19.03+)
docker --version# 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# 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
}
}
}
EOFIf you have existing configuration, merge the settings manually.
# 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"# 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# 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# 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 dockerBefore 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.8Some 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=runscOnly 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: runscIf 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
...# 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# 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# 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# 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 UIAfter completing all steps:
-
docker info | grep "Default Runtime"shows expected runtime -
docker run --rm alpine cat /proc/1/status | grep NoNewPrivsshowsNoNewPrivs: 1 - All Dokploy services are running (
docker ps) - Dokploy UI is accessible
- Can deploy a test application through Dokploy