Files
grm/ansible/roles/gitea_runner/tasks/rootless_docker.yml
T
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 563dec6d95
CI / validate (pull_request) Successful in 1m33s
CI / molecule-tests (6) (pull_request) Successful in 4m12s
CI / molecule-tests (4) (pull_request) Successful in 4m23s
CI / molecule-tests (2) (pull_request) Successful in 4m25s
CI / molecule-tests (5) (pull_request) Successful in 6m17s
CI / molecule-tests (3) (pull_request) Successful in 7m7s
CI / molecule-tests (1) (pull_request) Successful in 7m47s
CI / auto-merge (pull_request) Successful in 55s
fix: pin containerd.io to compatible version for Docker 28.x
Docker 28.x vendors containerd v2.1.x internally. containerd.io >= 2.3
ships a shim that returns a protobuf BootstrapResult which the vendored
2.1.x code cannot parse, causing every container start to fail with:
  "failed to create TTRPC connection: unsupported protocol"

The role now:
- Detects the installed Docker and containerd.io versions
- If Docker < 29 and containerd.io >= 2.3, downgrades to the latest
  compatible 2.2.x from the Docker APT repository
- Holds the containerd.io package to prevent future auto-upgrades

This was triggered by containerd.io 2.2.6 → 2.3.3 auto-upgrading via
apt, which broke all CI runners simultaneously.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-06 21:01:13 +02:00

394 lines
16 KiB
YAML

---
- name: Ensure keyrings directory exists (Debian/Ubuntu)
ansible.builtin.file:
path: "/etc/apt/keyrings"
state: directory
mode: "0755"
when: ansible_facts['os_family'] == 'Debian'
- name: Download and dearmor Docker GPG key (Debian/Ubuntu)
ansible.builtin.shell: |
set -o pipefail
curl -fsSL "https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }}/gpg" \
| gpg --dearmor --yes -o {{ gitea_runner_docker_gpg_key_path }}
args:
creates: "{{ gitea_runner_docker_gpg_key_path }}"
executable: /bin/bash
when: ansible_facts['os_family'] == 'Debian'
- name: Add Docker APT repository (Debian/Ubuntu)
ansible.builtin.copy:
dest: /etc/apt/sources.list.d/docker.list
content: "{{ gitea_runner_docker_apt_source_line }}\n"
mode: "0644"
register: gitea_runner_docker_apt_repo
when: ansible_facts['os_family'] == 'Debian'
- name: Update apt cache after adding Docker repo (Debian/Ubuntu)
ansible.builtin.apt:
update_cache: true
when:
- ansible_facts['os_family'] == 'Debian'
- gitea_runner_docker_apt_repo is changed
# Install Docker packages from the upstream Docker APT repository.
# We do NOT pin to 28.x because recent Ubuntu releases (e.g. 26.04/plucky)
# may not have 28.x packages in the Docker repo, and Docker 29 is safe
# for rootless mode when the daemon.json disables the containerd snapshotter
# and sets a conservative default nofile ulimit (see daemon.json tasks below).
- name: Install rootless Docker dependencies (Debian/Ubuntu)
ansible.builtin.apt:
name:
- uidmap
- slirp4netns
- passt
- fuse-overlayfs
- docker-ce
- docker-ce-cli
- docker-ce-rootless-extras
- containerd.io
- docker-compose-plugin
- rsync
state: present
register: gitea_runner_docker_install
when: ansible_facts['os_family'] == 'Debian'
# Docker 28.x vendors containerd v2.1.x. containerd.io >= 2.3 ships a shim that
# returns a protobuf BootstrapResult the vendored 2.1.x code cannot parse, causing
# "failed to create TTRPC connection: unsupported protocol" on every container start.
# Detect the mismatch and downgrade containerd.io to the latest compatible 2.2.x.
- name: Check installed Docker CE version (Debian/Ubuntu)
ansible.builtin.command: dpkg-query -W -f='${Version}' docker-ce
register: gitea_runner_docker_version_check
changed_when: false
when: ansible_facts['os_family'] == 'Debian'
- name: Check installed containerd.io version (Debian/Ubuntu)
ansible.builtin.shell: "set -o pipefail; dpkg-query -W -f='${Version}' containerd.io 2>/dev/null | cut -d: -f2 | cut -d- -f1"
args:
executable: /bin/bash
register: gitea_runner_containerd_version_check
changed_when: false
when: ansible_facts['os_family'] == 'Debian'
- name: Determine if containerd.io is incompatible with installed Docker
ansible.builtin.set_fact:
gitea_runner_containerd_needs_downgrade: >-
{{
gitea_runner_containerd_version_check.stdout.split('.')[0] | int > gitea_runner_containerd_max_compatible_major
or (
gitea_runner_containerd_version_check.stdout.split('.')[0] | int == gitea_runner_containerd_max_compatible_major
and gitea_runner_containerd_version_check.stdout.split('.')[1] | int > gitea_runner_containerd_max_compatible_minor
)
}}
gitea_runner_docker_major: "{{ gitea_runner_docker_version_check.stdout.split('.')[0] | default('0') | int }}"
when: ansible_facts['os_family'] == 'Debian'
- name: Find latest compatible containerd.io version (Debian/Ubuntu)
ansible.builtin.shell: |
set -o pipefail
apt-cache madison containerd.io \
| awk -F'|' '{print $2}' \
| tr -d ' ' \
| grep -E '^{{ gitea_runner_containerd_max_compatible_major }}\.{{ gitea_runner_containerd_max_compatible_minor }}\.' \
| head -1
args:
executable: /bin/bash
register: gitea_runner_containerd_compatible_version
changed_when: false
when:
- ansible_facts['os_family'] == 'Debian'
- gitea_runner_containerd_needs_downgrade | default(false)
- gitea_runner_docker_major | int < 29
- name: Downgrade containerd.io to compatible version (Debian/Ubuntu)
ansible.builtin.apt:
name: "containerd.io={{ gitea_runner_containerd_compatible_version.stdout }}"
state: present
allow_downgrades: true
register: gitea_runner_containerd_downgrade
when:
- ansible_facts['os_family'] == 'Debian'
- gitea_runner_containerd_needs_downgrade | default(false)
- gitea_runner_docker_major | int < 29
- gitea_runner_containerd_compatible_version.stdout | length > 0
- name: Hold containerd.io package to prevent auto-upgrade (Debian/Ubuntu)
ansible.builtin.dpkg_selections:
name: containerd.io
selection: hold
when: ansible_facts['os_family'] == 'Debian'
- name: Update pacman cache (Arch Linux)
community.general.pacman:
update_cache: true
when: ansible_facts['os_family'] == 'Archlinux'
changed_when: false
- name: Install rootless Docker dependencies (Arch Linux)
community.general.pacman:
name:
- docker
- docker-compose
- slirp4netns
- passt
- fuse-overlayfs
- rsync
# rootlesskit is the userspace networking/namespace driver for rootless Docker.
# It is NOT a dependency of the "docker" package on Arch and must be installed explicitly.
- rootlesskit
state: present
when: ansible_facts['os_family'] == 'Archlinux'
# The tun kernel module is required by rootlesskit (both slirp4netns and pasta
# drivers create a tap device inside a user namespace). On Arch Linux, CONFIG_TUN=m
# so the module must be loaded. If the running kernel doesn't match the installed
# kernel (e.g. after a pacman -Syu that upgraded linux but didn't reboot), modprobe
# will fail — in that case we warn but don't fail, as a reboot will fix it.
- name: Load tun kernel module for rootless networking
community.general.modprobe:
name: tun
state: present
ignore_errors: true
register: gitea_runner_tun_module
when: ansible_facts['os_family'] == 'Archlinux'
- name: Warn if tun module could not be loaded (kernel mismatch — reboot needed)
ansible.builtin.debug:
msg: >-
WARNING: Could not load the tun kernel module. This is likely because the
running kernel ({{ ansible_facts['kernel'] }}) does not match the installed
kernel modules. A reboot is required before rootless Docker can start.
when:
- ansible_facts['os_family'] == 'Archlinux'
- gitea_runner_tun_module is failed
# Arch Linux's "docker" package does not ship the rootless setup scripts (dockerd-rootless-setuptool.sh
# and dockerd-rootless.sh), unlike Debian/Ubuntu's docker-ce-rootless-extras. No official Arch package
# provides them, so fetch them from the upstream moby/moby contrib/ directory. They are installed
# alongside the docker binaries (/usr/bin) because dockerd-rootless-setuptool.sh derives its BIN
# directory from its own location and expects docker/dockerd/rootlesskit to be co-located there.
- name: Fetch rootless Docker setup scripts (Arch Linux)
ansible.builtin.get_url:
url: "https://raw.githubusercontent.com/moby/moby/{{ gitea_runner_rootless_scripts_ref }}/contrib/{{ item.name }}"
dest: "{{ gitea_runner_rootless_scripts_install_dir }}/{{ item.name }}"
mode: "0755"
owner: root
group: root
loop:
- name: dockerd-rootless-setuptool.sh
- name: dockerd-rootless.sh
when: ansible_facts['os_family'] == 'Archlinux'
- name: Check if rootless Docker is already set up
ansible.builtin.stat:
path: "{{ gitea_runner_home }}/.config/systemd/user/docker.service"
register: gitea_runner_rootless_docker_check
# Create the systemd override BEFORE running the setuptool so that when
# the setuptool starts docker.service, it picks up the pasta network driver
# instead of the default slirp4netns (which may fail on some kernels).
- name: Ensure systemd user override directory exists
ansible.builtin.file:
path: "{{ gitea_runner_home }}/.config/systemd/user/docker.service.d"
state: directory
mode: "0755"
owner: "{{ gitea_runner_service_user }}"
group: "{{ gitea_runner_service_user }}"
when:
- gitea_runner_docker_rootless_setup
- not gitea_runner_rootless_docker_check.stat.exists
- name: Pre-configure rootless Docker network driver override
ansible.builtin.copy:
dest: "{{ gitea_runner_home }}/.config/systemd/user/docker.service.d/override.conf"
content: |
[Service]
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET={{ gitea_runner_docker_rootless_net_driver }}"
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER={{ 'implicit' if gitea_runner_docker_rootless_net_driver == 'pasta' else 'builtin' }}"
{% if gitea_runner_docker_rootless_net_driver == 'pasta' %}
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS=--ipv6"
{% endif %}
mode: "0644"
owner: "{{ gitea_runner_service_user }}"
group: "{{ gitea_runner_service_user }}"
when:
- gitea_runner_docker_rootless_setup
- not gitea_runner_rootless_docker_check.stat.exists
# Write daemon.json BEFORE the setuptool starts dockerd, so Docker 29
# starts with containerd snapshotter disabled from the very first boot.
# Without this, Docker 29 uses containerd snapshots by default, which
# causes instability in rootless mode.
- name: Ensure Docker config directory exists (pre-setup)
ansible.builtin.file:
path: "{{ gitea_runner_home }}/.config/docker"
state: directory
mode: "0755"
owner: "{{ gitea_runner_service_user }}"
group: "{{ gitea_runner_service_user }}"
when:
- gitea_runner_docker_rootless_setup
- not gitea_runner_rootless_docker_check.stat.exists
- name: Pre-configure rootless Docker daemon.json (disable containerd snapshotter)
ansible.builtin.copy:
dest: "{{ gitea_runner_home }}/.config/docker/daemon.json"
content: |
{
"live-restore": {{ gitea_runner_docker_live_restore | to_json }},
"shutdown-timeout": {{ gitea_runner_docker_shutdown_timeout }},
"max-concurrent-downloads": {{ gitea_runner_docker_max_concurrent_downloads }},
"max-concurrent-uploads": {{ gitea_runner_docker_max_concurrent_uploads }},
"default-ulimits": {
"nofile": {"Name": "nofile", "Hard": {{ gitea_runner_docker_default_nofile }}, "Soft": {{ gitea_runner_docker_default_nofile }}}
},
"features": {
"containerd-snapshotter": false
},
{% if gitea_runner_docker_rootless_net_driver == 'pasta' %}
"ipv6": true,
"ip6tables": true,
"fixed-cidr-v6": "{{ gitea_runner_docker_ipv6_cidr }}",
"dns": ["10.0.2.3", "8.8.8.8"]
{% else %}
"ipv6": false,
"dns": ["8.8.8.8", "1.1.1.1"]
{% endif %}
}
mode: "0644"
owner: "{{ gitea_runner_service_user }}"
group: "{{ gitea_runner_service_user }}"
when:
- gitea_runner_docker_rootless_setup
- not gitea_runner_rootless_docker_check.stat.exists
- name: Set up rootless Docker for runner user
ansible.builtin.command: dockerd-rootless-setuptool.sh install
args:
creates: "{{ gitea_runner_home }}/.config/systemd/user/docker.service"
become: true
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid }}/bus"
DOCKERD_ROOTLESS_ROOTLESSKIT_NET: "{{ gitea_runner_docker_rootless_net_driver }}"
when:
- gitea_runner_docker_rootless_setup
- not gitea_runner_rootless_docker_check.stat.exists
- name: Start rootless Docker daemon (systemd user service)
ansible.builtin.command: systemctl --user start docker
become: true
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid }}/bus"
changed_when: true
when: gitea_runner_docker_rootless_setup
- name: Enable rootless Docker daemon (systemd user service)
ansible.builtin.command: systemctl --user enable docker
become: true
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid }}/bus"
changed_when: true
when: gitea_runner_docker_rootless_setup
- name: Ensure Docker config directory exists
ansible.builtin.file:
path: "{{ gitea_runner_home }}/.config/docker"
state: directory
mode: "0755"
owner: "{{ gitea_runner_service_user }}"
group: "{{ gitea_runner_service_user }}"
when: gitea_runner_docker_rootless_setup
- name: Configure rootless Docker network driver
ansible.builtin.copy:
dest: "{{ gitea_runner_home }}/.config/systemd/user/docker.service.d/override.conf"
content: |
[Service]
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET={{ gitea_runner_docker_rootless_net_driver }}"
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER={{ 'implicit' if gitea_runner_docker_rootless_net_driver == 'pasta' else 'builtin' }}"
{% if gitea_runner_docker_rootless_net_driver == 'pasta' %}
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS=--ipv6"
{% endif %}
mode: "0644"
owner: "{{ gitea_runner_service_user }}"
group: "{{ gitea_runner_service_user }}"
register: gitea_runner_docker_network_override
when: gitea_runner_docker_rootless_setup
- name: Reload systemd user daemon if network config changed
ansible.builtin.command: systemctl --user daemon-reload
become: true
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid }}/bus"
changed_when: true
when:
- gitea_runner_docker_rootless_setup
- gitea_runner_docker_network_override is changed
- name: Configure rootless Docker daemon
ansible.builtin.copy:
dest: "{{ gitea_runner_home }}/.config/docker/daemon.json"
content: |
{
"live-restore": {{ gitea_runner_docker_live_restore | to_json }},
"shutdown-timeout": {{ gitea_runner_docker_shutdown_timeout }},
"max-concurrent-downloads": {{ gitea_runner_docker_max_concurrent_downloads }},
"max-concurrent-uploads": {{ gitea_runner_docker_max_concurrent_uploads }},
"default-ulimits": {
"nofile": {"Name": "nofile", "Hard": {{ gitea_runner_docker_default_nofile }}, "Soft": {{ gitea_runner_docker_default_nofile }}}
},
"features": {
"containerd-snapshotter": false
},
{% if gitea_runner_docker_rootless_net_driver == 'pasta' %}
"ipv6": true,
"ip6tables": true,
"fixed-cidr-v6": "{{ gitea_runner_docker_ipv6_cidr }}",
"dns": ["10.0.2.3", "8.8.8.8"]
{% else %}
"ipv6": false,
"dns": ["8.8.8.8", "1.1.1.1"]
{% endif %}
}
mode: "0644"
owner: "{{ gitea_runner_service_user }}"
group: "{{ gitea_runner_service_user }}"
register: gitea_runner_docker_ipv6_config
when: gitea_runner_docker_rootless_setup
- name: Restart rootless Docker if config changed
ansible.builtin.command: systemctl --user restart docker
become: true
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid }}/bus"
changed_when: true
when:
- gitea_runner_docker_rootless_setup
- gitea_runner_docker_ipv6_config is changed or gitea_runner_docker_network_override is changed
- name: Wait for rootless Docker daemon to be ready
ansible.builtin.command: docker version
become: true
become_user: "{{ gitea_runner_service_user }}"
environment:
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock"
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid }}/bus"
register: gitea_runner_docker_ready
until: gitea_runner_docker_ready.rc == 0
retries: 10
delay: 2
changed_when: false
when: gitea_runner_docker_rootless_setup