Production Docker Engine Setup & daemon.json Hardening on a Linux VPS

5 min read

Deploying Docker on a modern Linux VPS (Ubuntu or Debian) requires moving beyond standard repository packages. In production, unconstrained container logging, default daemon settings, and improper package channels frequently lead to disk exhaustion, security vulnerabilities, and service downtime during kernel updates.

This runbook outlines the production standard for installing the official Docker Engine, configuring critical daemon-level safeguards in /etc/docker/daemon.json, enabling non-disruptive daemon updates with live-restore, and establishing a structured GitOps directory architecture.


1. Official Docker CE Installation vs. Distro Packages

Many Linux distributions package Docker as docker.io via standard distribution repositories. In a production environment, avoid docker.io:

  • Lagging Versions: Distro packages often lag several minor releases behind upstream security patches and contain outdated Buildx and Compose plugins.
  • Broken Compose Integration: Distro packages often ship obsolete Python-based docker-compose (v1) instead of the integrated Go-based docker compose (v2) plugin.

Automated Installation via Official Docker Repository

# Remove any lingering distro packages
sudo apt-get remove -y docker docker-engine docker.io containerd runc

# Install required prerequisites
sudo apt-get update && sudo apt-get install -y ca-certificates curl gnupg lsb-release

# Add Docker's official 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

# Set up the stable official repository
echo "deb [arch= signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu  stable" |   sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Install Docker Engine, containerd, and Compose v2
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

2. Production daemon.json Configuration

By default, Docker ships with no log rotation limits and stops all containers whenever the Docker daemon service restarts. On a VPS with 25GB to 80GB of storage, runaway container logs (e.g. database debug loops or API healthcheck spam) can completely fill the root filesystem within hours, causing kernel panics.

Create or update /etc/docker/daemon.json with the following production defaults:

sudo tee /etc/docker/daemon.json <<'EOF'
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "live-restore": true,
  "no-new-privileges": true,
  "userland-proxy": false,
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 65535,
      "Soft": 65535
    }
  }
}
EOF

Key Architectural Directives Explained

  • log-driver: json-file with max-size: 10m & max-file: 3: Restricts every container to a maximum of 30MB of logs (three 10MB rotated files). Older logs are automatically purged without manual intervention.
  • live-restore: true: Keeps active containers running uninterrupted even when the Docker daemon restarts or when unattended-upgrades patches the docker-ce package.
  • no-new-privileges: true: Security baseline that prevents container processes from acquiring additional privileges via setuid or setgid binaries.
  • userland-proxy: false: Disables the resource-heavy docker-proxy process, allowing the Linux kernel’s Hairpin NAT to route traffic directly. This reduces memory usage and preserves genuine client IP addresses.

Apply the configuration and restart Docker:

sudo systemctl restart docker
sudo docker info | grep -E "Logging Driver|Live Restore"

3. Host Filesystem & Stack Directory Architecture

Avoid scattering Docker Compose files across arbitrary user home directories. Structure production services inside /opt/komodo/stacks/ with dedicated directory boundaries:

/opt/komodo/stacks/
├── 0.proxy/
│   ├── compose.yaml          # Ingress proxy (Caddy / Cloudflare Tunnel)
│   ├── Caddyfile
│   └── .env
├── vaultwarden/
│   ├── compose.yaml
│   └── .env
└── paperless/
    ├── compose.yaml
    └── .env

Permission Isolation

Grant standard deployment users access to Docker without sudo while securing sensitive configuration files:

# Add deployment user to docker group
sudo usermod -aG docker 

# Lock down stack directories
sudo mkdir -p /opt/komodo/stacks && sudo chown -R : /opt/komodo/stacks
chmod 700 /opt/komodo/stacks

Automating Docker Installation & daemon.json with Ansible

To provision Docker Engine and enforce daemon-level safeguards across your entire VPS fleet declaratively, use the following idempotent Ansible tasks:

# roles/docker/tasks/main.yml
---
- name: Remove legacy distro packages
  ansible.builtin.apt:
    name: [docker, docker-engine, docker.io, containerd, runc]
    state: absent

- name: Create apt keyrings directory
  ansible.builtin.file:
    path: /etc/apt/keyrings
    state: directory
    mode: "0755"

- name: Download official Docker GPG key
  ansible.builtin.get_url:
    url: "https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg"
    dest: /etc/apt/keyrings/docker.asc
    mode: "0644"

- name: Configure official Docker repository (deb822)
  ansible.builtin.deb822_repository:
    name: docker
    types: [deb]
    uris: "https://download.docker.com/linux/{{ ansible_distribution | lower }}"
    suites: ["{{ ansible_distribution_release }}"]
    components: [stable]
    signed_by: /etc/apt/keyrings/docker.asc
    architectures: ["{{ ansible_architecture | replace('x86_64', 'amd64') | replace('aarch64', 'arm64') }}"]
    state: present

- name: Install Docker CE and Compose plugin
  ansible.builtin.apt:
    name:
      - docker-ce
      - docker-ce-cli
      - containerd.io
      - docker-buildx-plugin
      - docker-compose-plugin
    state: present
    update_cache: true

- name: Deploy hardened /etc/docker/daemon.json
  ansible.builtin.copy:
    content: |
      {
        "log-driver": "json-file",
        "log-opts": {
          "max-size": "10m",
          "max-file": "3"
        },
        "live-restore": true,
        "no-new-privileges": true,
        "userland-proxy": false
      }
    dest: /etc/docker/daemon.json
    owner: root
    group: root
    mode: "0644"
  notify: Restart docker

- name: Create external Docker bridge networks
  community.docker.docker_network:
    name: "{{ item }}"
    driver: bridge
    state: present
  loop:
    - web_net
    - db_internal

Related Production Runbooks

Tags:

← Previous:
Next: →