Home ยป Containers ยป Docker Security Tips for Container Hosts & Ansible Playbook
Containers

Docker Security Tips for Container Hosts & Ansible Playbook

Docker Security Tips to protect your containers and harden your Docker host from threats and vulnerabilities

Running containers in your lab or production environment is one of the best things you can do, not only for learning, but running workloads efficiently. Over the last couple of years, I have developed a security checklist that I apply to every Docker host. In this post, I’ll walk through the steps I take, from host hardening to securing the runtime. This will help to keep your containers safer and security a priority.

Why Docker host security matters

Containers are designed to share the host kernel. It means that a break-out or some type of security exploit in any container has the potential to affect the entire system. Even in the home lab, a compromised container can expose things like your management interfaces, storage mounts, or other sensitive services. Think about Proxmox, Kubernetes control planes, and monitoring agents as targets.

As you add more and more containerized services, reverse proxies, AI inference stacks, etc, you are creating more attack surface for these types of exploits. So, getting your base Docker host locked down early helps to prevent security headaches later on when you have dozens or hundreds of containers.

1. Start with a minimal installation and updates

  1. Pick a slim OS image.
    I usually run Ubuntu Server LTS (24.04) or Debian Bookworm, but stripped down. Disable unneeded services: sudo systemctl disable bluetooth.service sudo systemctl disable avahi-daemon.service
  2. Keep packages patched.
    I enable unattended security updates for critical fixes: sudo apt install unattended-upgrades sudo dpkg-reconfigure --priority=low unattended-upgrades
  3. Use read-only filesystems where possible.
    For log directories that donโ€™t need writes by all users, mount them read-only or leverage tmpfs for ephemeral files.

2. Lock down SSH and user access

  • Key-based SSH only. Disable password authentication in /etc/ssh/sshd_config: PasswordAuthentication no PermitRootLogin no
  • Non-standard port and rate limiting. Move SSH off port 22 (e.g., to 2202) and throttle attempts using ufw or fail2ban.
  • Least-privilege users. I create a โ€œdockeradminโ€ user, add it to the docker group, and never log in as root.
    • sudo adduser dockeradmin sudo usermod -aG docker dockeradmin

3. Secure the Docker Daemon

Use TLS for the Remote API

If you expose the Docker socket over the network (e.g., for remote management or Portainer), always secure it with TLS:

  1. Generate server and client certificates signed by your own CA.
  2. Update /etc/docker/daemon.json:
    • { "hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"], "tlsverify": true, "tlscacert": "/etc/docker/ssl/ca.pem", "tlscert": "/etc/docker/ssl/server-cert.pem", "tlskey": "/etc/docker/ssl/server-key.pem" }
  3. Restart Docker: sudo systemctl restart docker

Drop unnecessary capabilities

By default, containers run with a broad set of Linux capabilities. I drop all capabilities and then add only whatโ€™s needed for a given service:

docker run --cap-drop ALL --cap-add NET_BIND_SERVICE nginx:latest

In docker-compose.yml:

services:
  web:
    image: nginx:latest
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE

4. Runtime security with seccomp and AppArmor

  • Use Dockerโ€™s default seccomp profile (enabled by default) or customize it to block syscalls you donโ€™t need.
  • Enable AppArmor or SELinux on the host. On Ubuntu, ensure your container images include AppArmor support and Docker is launched with --security-opt apparmor=docker-default.

Example custom seccomp profile in daemon.json:

{
  "seccomp-profile": "/etc/docker/seccomp.json"
}

You can start from Dockerโ€™s default profile and fine-tune it by removing or adding syscall permissions.

5. Image hygiene

  1. Official images only.
    Prefer images from Docker Hubโ€™s โ€œOfficialโ€ namespace or your own private registry.
  2. Scan images for vulnerabilities.
    Integrate tools like Trivy, Anchore, or Clair into your CI pipelines: bashCopyEdittrivy image nginx:latest
  3. Immutable tags.
    Pin your images to specific digests to avoid unexpected changes: yamlCopyEditimage: nginx@sha256:af3e2fc47...
  4. Minimal base images.
    Use Alpine or Distroless images where possible to reduce attack surface.

6. Network security using segmentation and firewalls

Per-Container Networks

Rather than using the default bridge, I define custom networks that isolate traffic flows:

networks:
  internal:
    driver: bridge
    internal: true
  public:
    driver: macvlan
    driver_opts:
      parent: eth0.40
  • internal prevents containers from accessing the host network or the internet.
  • public connects only to VLAN 40 for DMZ services.

Host-Level Firewalling

Use ufw or iptables to restrict outgoing and incoming traffic:

# Allow Docker daemon port only from management subnet
sudo ufw allow from 10.0.10.0/24 to any port 2376 proto tcp

# Deny all inter-container traffic by default
sudo ufw deny in on docker0

Service Mesh or Proxy

For multi-container apps, consider using an internal reverse proxy (Traefik or Nginx) to centralize TLS and access control, rather than exposing ports directly.

7. Secrets

Never bake passwords or API keys into images or docker-compose.yml. Instead:

  • Docker secrets (in Swarm mode): echo "supersecret" | docker secret create db_password - And in the stack.yml: services: db: image: postgres secrets: - db_password secrets: db_password: external: true
  • HashiCorp Vault or Bitwarden CLI for standalone Docker:
    Fetch secrets at container start via an init script or sidecar.

8. Monitoring and logging

Always monitor and have logging going on your Docker hosts that you can ingest and use for troubleshooting and security.

  1. Centralized logs.
    Forward container logs to a central ELK stack or Loki + Grafana for easier auditing.
  2. File integrity monitoring.
    Run tools like AIDE or Tripwire on the host to detect changes to critical binaries (including Docker daemon, container runtimes).
  3. Prometheus + Node Exporter.
    Track container CPU, memory, and network usage; alert on anomalous spikes.

9. Backups

Always backup your Docker persistent volumes.

  • Docker volume snapshots.
    For named volumes, use docker run --rm -v my_data:/data -v $(pwd):/backup alpine tar czf /backup/data.tgz /data.
  • Configuration as code.
    Store all your docker-compose.yml, TLS certs, and seccomp profiles in Git. In case of host rebuild, you can bring containers back up quickly.
  • Periodic drills.
    I rehearse full host rebuilds on a spare node every quarterโ€”to verify my backups and automation scripts.

Example Ansible playbook

Below is an example Ansible playbook that you can use as a framework to do some of your security hardening with your docker hosts.

### Ansible Playbook: Hardening Docker Hosts
# Description: Implements host and Docker security tasks from the blog post.
# Usage: ansible-playbook -i inventory docker_hardening.yml

- name: Harden Docker Container Host
  hosts: docker_hosts
  become: yes
  vars:
    ssh_port: 2202
    docker_tls:
      ca_cert: /etc/docker/ssl/ca.pem
      server_cert: /etc/docker/ssl/server-cert.pem
      server_key: /etc/docker/ssl/server-key.pem
  tasks:

    - name: Host: Disable unnecessary services
      systemd:
        name: "{{ item }}"
        enabled: no
        state: stopped
      loop:
        - bluetooth.service
        - avahi-daemon.service

    - name: Host: Install and configure unattended-upgrades
      apt:
        name: unattended-upgrades
        state: latest
      notify: enable-unattended

    - name: Host: Ensure SSH root login is disabled
      lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^PermitRootLogin'
        line: 'PermitRootLogin no'
        state: present
      notify: restart-sshd

    - name: Host: Disable SSH password authentication
      lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^PasswordAuthentication'
        line: 'PasswordAuthentication no'
        state: present
      notify: restart-sshd

    - name: Host: Change SSH port
      lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^#?Port'
        line: 'Port {{ ssh_port }}'
        state: present
      notify: restart-sshd

    - name: Host: Create dockeradmin user
      user:
        name: dockeradmin
        groups: docker
        shell: /bin/bash
        state: present
        create_home: yes

    - name: Docker: Ensure TLS certificates directory exists
      file:
        path: /etc/docker/ssl
        state: directory
        owner: root
        mode: '0750'

    - name: Docker: Copy TLS certificate files
      copy:
        src: "files/{{ item }}"
        dest: "{{ docker_tls[item] }}"
        owner: root
        mode: '0640'
      loop:
        - ca_cert
        - server_cert
        - server_key
      vars:
        docker_tls:
          ca_cert: /etc/docker/ssl/ca.pem
          server_cert: /etc/docker/ssl/server-cert.pem
          server_key: /etc/docker/ssl/server-key.pem
      notify: restart-docker

    - name: Docker: Configure daemon.json
      copy:
        dest: /etc/docker/daemon.json
        content: |
          {
            "hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"],
            "tlsverify": true,
            "tlscacert": "{{ docker_tls.ca_cert }}",
            "tlscert": "{{ docker_tls.server_cert }}",
            "tlskey": "{{ docker_tls.server_key }}",
            "seccomp-profile": "/etc/docker/seccomp.json"
          }
      notify: restart-docker

    - name: Docker: Place custom seccomp profile
      template:
        src: templates/seccomp.json.j2
        dest: /etc/docker/seccomp.json
        owner: root
        mode: '0644'
      notify: restart-docker

    - name: Docker: Create custom bridge network (internal)
      community.docker.docker_network:
        name: internal_net
        driver: bridge
        internal: yes

    - name: Docker: Create macvlan network (public)
      community.docker.docker_network:
        name: public_net
        driver: macvlan
        driver_options:
          parent: eth0.40
        ipam_options:
          subnet: 10.0.40.0/24
          gateway: 10.0.40.1

    - name: Firewall: Allow SSH on custom port
      ufw:
        rule: allow
        port: "{{ ssh_port }}"
        proto: tcp
        from_ip: 10.0.10.0/24

    - name: Firewall: Deny all on docker0
      ufw:
        rule: deny
        in_interface: docker0

  handlers:
    - name: restart-sshd
      systemd:
        name: sshd
        state: restarted

    - name: restart-docker
      systemd:
        name: docker
        state: restarted

    - name: enable-unattended
      command: dpkg-reconfigure --frontend=noninteractive unattended-upgrades
      async: 600
      poll: 0

Wrapping Up

Keep in mind that security of your hosts and environment needs to be continually reviewed. I pick a time every few months to do the following steps:

  • Review my daemon.json and security profiles
  • Rotate TLS certificates and SSH keys
  • Re-scan images for newly discovered CVEs
  • Update my blog with any new tricks or lessons learned

By following these practices though, youโ€™ll create a much more in-depth defense posture for your container hosts. What security steps do you perform in your environment? I would like to learn from you.

Brandon Lee

Brandon Lee is the Senior Writer, Engineer and owner at Virtualizationhowto.com, and a 7-time VMware vExpert, with over two decades of experience in Information Technology. Having worked for numerous Fortune 500 companies as well as in various industries, He has extensive experience in various IT segments and is a strong advocate for open source technologies. Brandon holds many industry certifications, loves the outdoors and spending time with family. Also, he goes through the effort of testing and troubleshooting issues, so you don't have to.

Related Articles

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.