containerd vs Docker
Docker is built on top of containerd. When you run docker run, Docker's CLI talks to the Docker daemon, which talks to containerd, which talks to runc. By running containerd directly, you eliminate the Docker daemon entirely.
| Feature | containerd | Docker |
|---|---|---|
| Architecture | Minimal runtime (no daemon bloat) | Client-server (dockerd daemon) |
| Memory footprint | Lower (no dockerd process) | Higher (~50–100 MB for dockerd) |
| Container starts | Faster (fewer abstraction layers) | Slightly slower |
| Attack surface | Smaller (fewer running daemons) | Larger |
| Kubernetes | Native default runtime | Requires dockershim (deprecated) |
| CLI | nerdctl (Docker-compatible) or ctr | Native docker CLI |
Why RamNode for containerd
- KVM virtualization with full kernel access for container operations
- Starting at $4/month — 1 GB RAM works for lightweight workloads
- $500 annual credit for new accounts
- Generous bandwidth — image pulls won't be bottlenecked
Prerequisites
- A RamNode VPS running Ubuntu 24.04 LTS (1 GB for lightweight, 2 GB+ for production)
- Root or sudo access
- A domain name (optional, for exposing services)
- Basic Linux command-line familiarity
Prepare Your RamNode VPS
sudo apt update && sudo apt upgrade -y
sudo apt install -y \
ca-certificates \
curl \
gnupg \
lsb-release \
apt-transport-https \
software-properties-commonEnable Required Kernel Modules
containerd relies on specific kernel modules for networking and storage:
cat <<EOF | sudo tee /etc/modules-load.d/containerd.conf
overlay
br_netfilter
EOF
sudo modprobe overlay
sudo modprobe br_netfilterConfigure Kernel Networking Parameters
cat <<EOF | sudo tee /etc/sysctl.d/99-containerd.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
sudo sysctl --systemsysctl net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-ip6tables net.ipv4.ip_forwardAll three values should return 1.
Install containerd
Option A: Docker's Official Repository (Recommended)
Docker maintains the most up-to-date containerd packages:
# Add Docker's GPG key
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo 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" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install containerd
sudo apt update
sudo apt install -y containerd.ioInstall runc
runc is the low-level OCI runtime that containerd uses to create containers:
RUNC_VERSION="1.2.4"
curl -LO https://github.com/opencontainers/runc/releases/download/v${RUNC_VERSION}/runc.amd64
sudo install -m 755 runc.amd64 /usr/local/sbin/runcInstall CNI Plugins
Container Networking Interface plugins handle network setup for containers:
CNI_VERSION="1.6.1"
curl -LO https://github.com/containernetworking/plugins/releases/download/v${CNI_VERSION}/cni-plugins-linux-amd64-v${CNI_VERSION}.tgz
sudo mkdir -p /opt/cni/bin
sudo tar Cxzvf /opt/cni/bin cni-plugins-linux-amd64-v${CNI_VERSION}.tgzConfigure containerd
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml > /dev/nullEnable SystemdCgroup
Critical for proper resource management — without it, you'll encounter stability issues:
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.tomlKey Config Settings
# Under [plugins."io.containerd.grpc.v1.cri"]
# Set the sandbox (pause) image
sandbox_image = "registry.k8s.io/pause:3.10"
# Under [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true
# Under [plugins."io.containerd.grpc.v1.cri".registry]
# Configure mirror endpoints if needed for faster pullsApply Configuration
sudo systemctl restart containerd
sudo systemctl status containerd
# Verify installation
containerd --version
runc --versionYou should see active (running) in the status output.
Install nerdctl & BuildKit
ctr is containerd's native CLI but minimal. nerdctl provides a Docker-compatible interface that feels natural if you're coming from Docker.
NERDCTL_VERSION="2.0.3"
curl -LO https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-${NERDCTL_VERSION}-linux-amd64.tar.gz
sudo tar Cxzvf /usr/local/bin nerdctl-${NERDCTL_VERSION}-linux-amd64.tar.gzInstall BuildKit (for Building Images)
containerd doesn't include a built-in image builder — BuildKit handles that:
BUILDKIT_VERSION="0.18.2"
curl -LO https://github.com/moby/buildkit/releases/download/v${BUILDKIT_VERSION}/buildkit-v${BUILDKIT_VERSION}.linux-amd64.tar.gz
sudo tar Cxzvf /usr/local buildkit-v${BUILDKIT_VERSION}.linux-amd64.tar.gzcat <<EOF | sudo tee /etc/systemd/system/buildkit.service
[Unit]
Description=BuildKit
Documentation=https://github.com/moby/buildkit
After=containerd.service
[Service]
Type=notify
ExecStart=/usr/local/bin/buildkitd --oci-worker=false --containerd-worker=true
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now buildkitVerify nerdctl Works
sudo nerdctl run --rm hello-worldYou should see the familiar "Hello from Docker!" message — but running through containerd directly.
Configure Networking with CNI
Create a default bridge network configuration:
sudo mkdir -p /etc/cni/net.d
cat <<EOF | sudo tee /etc/cni/net.d/10-containerd-bridge.conflist
{
"cniVersion": "1.0.0",
"name": "containerd-bridge",
"plugins": [
{
"type": "bridge",
"bridge": "cni0",
"isGateway": true,
"ipMasq": true,
"promiscMode": true,
"ipam": {
"type": "host-local",
"ranges": [
[{
"subnet": "10.88.0.0/16"
}]
],
"routes": [
{ "dst": "0.0.0.0/0" }
]
}
},
{
"type": "firewall"
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
}
]
}
EOFThis gives containers a private 10.88.0.0/16 subnet with NAT for outbound traffic and port mapping for inbound access.
Rootless containerd (Optional)
Running containers as a non-root user improves security — if a container is compromised, the attacker doesn't land as root on the host.
sudo apt install -y uidmap dbus-user-sessionConfigure User Namespaces
# Replace 'deploy' with your non-root username
USERNAME="deploy"
sudo useradd -m -s /bin/bash $USERNAME 2>/dev/null || true
# Allocate subordinate UID/GID ranges
sudo usermod --add-subuids 100000-165535 $USERNAME
sudo usermod --add-subgids 100000-165535 $USERNAMEInstall Rootless containerd
su - deploy
containerd-rootless-setuptool.sh install📌 Note
If the setup tool isn't available, configure it manually by setting up a user-level systemd service for containerd.
Resource Management & Limits
On a VPS, resource management matters. containerd uses cgroups v2 (default on Ubuntu 24.04) for enforcement.
# Run a container with a 256MB memory limit
sudo nerdctl run -d --name myapp --memory 256m --memory-swap 512m nginx:alpine
# Limit to 0.5 CPU cores
sudo nerdctl run -d --name myapp --cpus 0.5 nginx:alpineMonitor Resource Usage
# View real-time container stats
sudo nerdctl stats
# Inspect a specific container's resource consumption
sudo nerdctl inspect myapp | grep -A 20 "Resources"Recommended Limits by Plan
| RamNode Plan | RAM | Max Containers | Per-Container Limit |
|---|---|---|---|
| 1 GB VPS | 1 GB | 1–2 lightweight | 128–256 MB each |
| 2 GB VPS | 2 GB | 3–4 lightweight | 256–512 MB each |
| 4 GB VPS | 4 GB | 6–8 mixed | 512 MB–1 GB each |
| 8 GB+ VPS | 8+ GB | 12+ | As needed |
Log Management
Containers can generate substantial logs. On a VPS, disk space is finite — configure log rotation early.
cat <<EOF | sudo tee /etc/logrotate.d/containerd
/var/log/containerd/*.log {
daily
missingok
rotate 7
compress
delaycompress
notifempty
create 0640 root root
}
EOFLimit journald Logs
# Edit /etc/systemd/journald.conf
[Journal]
SystemMaxUse=200M
MaxFileSec=7daysudo systemctl restart systemd-journaldSecurity Hardening
AppArmor Profiles
Ubuntu 24.04 ships with AppArmor enabled. Verify containerd uses it:
sudo aa-status | grep containerdSeccomp Profiles
containerd applies a default seccomp profile that blocks dangerous syscalls:
sudo nerdctl inspect myapp | grep -i seccompFirewall Configuration with UFW
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 80/tcp # HTTP
sudo ufw allow 443/tcp # HTTPS
sudo ufw enableRestrict containerd Socket
sudo chmod 660 /run/containerd/containerd.sock
sudo chown root:root /run/containerd/containerd.sockCommon Operations Quick Reference
nerdctl (Docker-Compatible CLI)
# Pull an image
sudo nerdctl pull nginx:alpine
# Run a container (detached, with port mapping)
sudo nerdctl run -d --name webserver -p 80:80 nginx:alpine
# List running containers
sudo nerdctl ps
# List all containers (including stopped)
sudo nerdctl ps -a
# View container logs
sudo nerdctl logs webserver
sudo nerdctl logs -f webserver # Follow/stream logs
# Execute a command inside a running container
sudo nerdctl exec -it webserver /bin/sh
# Stop and remove a container
sudo nerdctl stop webserver
sudo nerdctl rm webserver
# Build an image from a Dockerfile
sudo nerdctl build -t myapp:latest .
# List images
sudo nerdctl images
# Remove unused images
sudo nerdctl image prune -a
# Docker Compose equivalent
sudo nerdctl compose up -d
sudo nerdctl compose downAutomated Backups
Set up a cron job to snapshot your container volumes. This pairs well with RamNode's VPS snapshot capability.
cat <<'EOF' | sudo tee /usr/local/bin/containerd-backup.sh
#!/bin/bash
BACKUP_DIR="/opt/backups/containerd"
DATE=$(date +%Y%m%d-%H%M%S)
mkdir -p "$BACKUP_DIR"
# Export all running container images
for container in $(sudo nerdctl ps -q); do
NAME=$(sudo nerdctl inspect "$container" | grep -oP '"Name": "\K[^"]+' | head -1)
IMAGE=$(sudo nerdctl inspect "$container" | grep -oP '"Image": "\K[^"]+' | head -1)
sudo nerdctl save -o "$BACKUP_DIR/${NAME}-${DATE}.tar" "$IMAGE" 2>/dev/null
done
# Backup containerd config
cp /etc/containerd/config.toml "$BACKUP_DIR/config.toml.${DATE}"
# Clean up backups older than 7 days
find "$BACKUP_DIR" -name "*.tar" -mtime +7 -delete
find "$BACKUP_DIR" -name "config.toml.*" -mtime +7 -delete
echo "[$(date)] Backup completed: $BACKUP_DIR"
EOF
sudo chmod +x /usr/local/bin/containerd-backup.sh
# Run daily at 3 AM
echo "0 3 * * * root /usr/local/bin/containerd-backup.sh >> /var/log/containerd-backup.log 2>&1" | \
sudo tee /etc/cron.d/containerd-backupTroubleshooting
| Issue | Solution |
|---|---|
| containerd won't start | Check sudo journalctl -xeu containerd. Regenerate config if needed: containerd config default | sudo tee /etc/containerd/config.toml |
| Containers can't reach the internet | Verify sysctl net.ipv4.ip_forward returns 1 and check NAT rules with sudo iptables -t nat -L POSTROUTING -n |
| Image pulls are slow | Configure a registry mirror in /etc/containerd/config.toml under registry.mirrors |
| High memory usage | Check systemctl status containerd | grep Memory. Reduce concurrent operations or snapshot retention |
| Invalid config.toml syntax | sudo rm /etc/containerd/config.toml && containerd config default | sudo tee /etc/containerd/config.toml |
| Missing runc binary | Reinstall runc: sudo install -m 755 runc.amd64 /usr/local/sbin/runc |
| CNI config not loading | Restart containerd and verify config exists in /etc/cni/net.d/ |
🎉 What's Next
- Deploy a reverse proxy — set up Caddy or Traefik in a container for TLS termination and routing
- Add container orchestration — K3s uses containerd as its default runtime and runs well on 2 GB+ VPS
- Monitor with Prometheus — containerd exposes metrics at
/v1/metricsfor scraping - Set up CI/CD pipelines — use BuildKit's remote build capabilities for a container image build server
