Docker Security Tips for Container Hosts & Ansible Playbook

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
- 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
- Keep packages patched.
I enable unattended security updates for critical fixes:sudo apt install unattended-upgrades sudo dpkg-reconfigure --priority=low unattended-upgrades
- Use read-only filesystems where possible.
For log directories that donโt need writes by all users, mount them read-only or leveragetmpfs
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
orfail2ban
. - 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:
- Generate server and client certificates signed by your own CA.
- 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" }
- 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
- Official images only.
Prefer images from Docker Hubโs โOfficialโ namespace or your own private registry. - Scan images for vulnerabilities.
Integrate tools like Trivy, Anchore, or Clair into your CI pipelines: bashCopyEdittrivy image nginx:latest
- Immutable tags.
Pin your images to specific digests to avoid unexpected changes: yamlCopyEditimage: nginx@sha256:af3e2fc47...
- 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 thestack.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 aninit
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.
- Centralized logs.
Forward container logs to a central ELK stack or Loki + Grafana for easier auditing. - File integrity monitoring.
Run tools like AIDE or Tripwire on the host to detect changes to critical binaries (including Docker daemon, container runtimes). - 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, usedocker run --rm -v my_data:/data -v $(pwd):/backup alpine tar czf /backup/data.tgz /data
. - Configuration as code.
Store all yourdocker-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.