A general-purpose self-hosting server ends up running a pile of unrelated services. Even worse if it doubles as a homelab - I want to experiment with new software and be able to clean up afterwards. For software that comes as an apt package it’s doable, for something you install manually (perhaps with the infamous “curl | sudo bash”) it will become a mess.
Containers solve that: each service gets its own image with everything it needs, isolated from the others. Of course, now you have an extra layer to manage, but it’s a good tradeoff.
Docker vs other container engines
Docker isn’t the only container engine, and I already wrote a whole post on switching from Docker to Podman for one of my other servers. Short version of that post: whenever there’s a choice between convenience and security, Docker defaults to convenience and Podman to security. Most of the manuals and tutorials assume you use Docker, for Podman you often need to adjust the configs and commands.
For a server I use at my job I want the extra security, even if it means spending a few more minutes setting things up. For a home server sitting behind my router, convenience wins.
Installing Docker with Ansible
As usual, the whole thing is one Ansible task file:
---
- name: Add docker apt repo
ansible.builtin.deb822_repository:
name: docker
types: deb
uris: https://download.docker.com/linux/debian
suites: "{{ ansible_distribution_release }}"
components: stable
signed_by: https://download.docker.com/linux/debian/gpg
state: present
install_python_debian: true
- name: Remove standalone docker-compose package (conflicts with docker-compose-plugin)
ansible.builtin.apt:
name: docker-compose
state: absent
- name: Install docker packages
ansible.builtin.apt:
name: ['docker-ce', 'docker-ce-cli', 'containerd.io', 'python3-docker', 'docker-compose-plugin']
state: present
update_cache: true
Nothing surprising here: add Docker’s own apt repo (Debian’s own docker.io package tends to lag behind), and install the engine plus the Compose plugin. One gotcha I hit: on an old installation (server that started as Debian Bullseye and got updated to Bookworm first and to Trixie next) there was a standalone docker-compose package (the Python-based v1) which conflicts with the new docker-compose-plugin. It needs to be removed first, or the install fails.
Moving the data root before it’s too late
By default Docker stores everything - images, containers, volumes, build cache - under /var/lib/docker, which on this server is a small SSD used for the system only. That’s fine while I’m only running a couple of lightweight containers, but I’m about to install something a lot bigger and more disk-hungry, and I’d rather move the data root to the large HDD before I fill up the space.
- name: Check whether new docker data-root already exists
ansible.builtin.stat:
path: "{{ docker_data_root }}"
register: docker_data_root_stat
when: docker_data_root != '/var/lib/docker'
- name: Move docker data-root to {{ docker_data_root }}
when:
- docker_data_root != '/var/lib/docker'
- not docker_data_root_stat.stat.exists
block:
- name: Get IDs of currently running containers
ansible.builtin.command: docker ps -q
register: docker_running_containers
changed_when: false
- name: Stop docker stack to safely move its data
ansible.builtin.service:
name: "{{ item }}"
state: stopped
loop:
- docker.socket
- docker
- containerd
- name: Create new docker data-root directory
ansible.builtin.file:
path: "{{ docker_data_root }}"
state: directory
owner: root
group: root
mode: '0711'
- name: Copy existing docker data to new data-root
ansible.builtin.command: "rsync -a /var/lib/docker/ {{ docker_data_root }}/"
changed_when: true
- name: Configure docker data-root
ansible.builtin.template:
src: daemon.json.j2
dest: /etc/docker/daemon.json
mode: '0644'
- name: Start docker with new data-root
ansible.builtin.service:
name: docker
state: started
- name: Restart containers that were running before the move
ansible.builtin.command: "docker start {{ item }}"
loop: "{{ docker_running_containers.stdout_lines }}"
- name: Add user to docker group
ansible.builtin.user:
name: "{{ create_user }}"
state: present
groups: docker
append: true
- name: Start docker
ansible.builtin.service:
name: docker
state: started
enabled: true
The docker_data_root_stat check makes the whole block idempotent - if the target directory already exists, I assume the move already happened and skip it, rather than re-running rsync and restarting containers on every playbook run.
The move itself is straightforward but has to happen in the right order: note down which containers are currently running (so I can start the same ones back up afterwards - docker start doesn’t touch anything that wasn’t already running), stop docker.socket, docker and containerd so nothing writes to the data directory mid-copy, rsync the old data
across, then point Docker at the new location. That last part is a one-line daemon.json template:
{
"data-root": "{{ docker_data_root }}"
}
My role defaults to the standard location (/var/lib/docker) and only for this specific server overwrites it with /data/noshare/docker. The old data under /var/lib/docker is left in place - I’ll clean it up by hand once I’ve confirmed everything still works after the move.
Quick reference
Containers
docker ps/docker ps -a- list running / all containersdocker logs -f <container>- follow a container’s logsdocker exec -it <container> bash- get a shell inside a running containerdocker start/docker stop/docker restart <container>- control a container’s statedocker rm <container>- remove a stopped container
Images
docker images- list downloaded imagesdocker pull <image>- fetch (or update) an image without running itdocker build -t <name> .- build an image from a Dockerfile in the current directorydocker rmi <image>- remove an image
Compose
docker compose up -d- start everything defined in docker-compose.yml, detacheddocker compose down- stop and remove a project’s containers and networks (not volumes)docker compose logs -f- follow logs for the whole projectdocker compose ps- list containers belonging to a project
Reclaiming space
docker system df- see how much space images, containers, volumes and build cache are usingdocker system prune- remove stopped containers, unused networks, dangling images and build cachedocker system prune -a- also remove any image not currently used by a containerdocker volume prune- remove volumes not used by any container
Inspecting
docker inspect <container>- full JSON config and state of a container or imagedocker stats- live CPU, memory and network usage per container