lava was created by hand without an ip_range, so any container could be given Caddy's fixed IP (172.20.0.5). After a reboot the containers race for the low addresses and Caddy often fails with "Address already in use". - The docker role now creates lava as 172.20.0.0/16 with dynamic addresses limited to 172.20.128.0/17, so 172.20.0.x is free for fixed IPs, and warns if the network still has the old layout. - Gitea gets fixed IP 172.20.0.6 once the network has the reserved range, since its ufw-docker SSH rule points at its IP. - ufw-docker allow rules for Caddy (80, 443) and Gitea SSH are managed by the playbook and follow IP changes. - migrate-lava-network.yml recreates the existing network once: it takes down every compose project on lava (volumes kept), recreates the network, and brings them back up, Caddy first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
130 lines
5.5 KiB
YAML
130 lines
5.5 KiB
YAML
---
|
|
# One-off: recreate the shared docker network with a reserved address range.
|
|
#
|
|
# Why: lava was created without an ip_range, so any container can be given
|
|
# Caddy's fixed IP (172.20.0.5) and Caddy then fails with "Address already
|
|
# in use". Docker can't change a network's range in place, so every stack on
|
|
# it is taken down, the network is recreated, and the stacks come back up.
|
|
# Volumes (all data) are kept. Expect a few minutes of downtime for everything.
|
|
#
|
|
# Run: ansible-playbook migrate-lava-network.yml -i hosts.yml --vault-password-file vault_pass
|
|
# Then: ansible-playbook site.yml -i hosts.yml --vault-password-file vault_pass
|
|
# (gives gitea its fixed IP and fixes its firewall rule)
|
|
#
|
|
# The list of stacks is saved to /root/lava-migration-<time>.txt first, so a
|
|
# failed run can be finished by hand with "docker compose ... up -d".
|
|
|
|
- hosts: docker
|
|
become: true
|
|
gather_facts: false
|
|
vars:
|
|
net: "{{ docker.network_name }}"
|
|
|
|
tasks:
|
|
- name: Pick the recovery file name once
|
|
ansible.builtin.set_fact:
|
|
record: "/root/lava-migration-{{ lookup('pipe', 'date +%Y%m%d-%H%M%S') }}.txt"
|
|
|
|
- name: Look up the network
|
|
community.docker.docker_network_info:
|
|
name: "{{ net }}"
|
|
register: before
|
|
|
|
- name: Stop if the network already has the reserved range
|
|
ansible.builtin.meta: end_play
|
|
when: >-
|
|
before.exists and
|
|
(before.network.IPAM.Config | map(attribute='IPRange', default='') | first) == docker.ip_range
|
|
|
|
# One line per compose project: project|working_dir|config_files
|
|
- name: List compose projects on the network
|
|
ansible.builtin.shell: |
|
|
for id in $(docker ps -aq --filter network={{ net }}); do
|
|
docker inspect "$id" -f '{{ '{{' }}index .Config.Labels "com.docker.compose.project"{{ '}}' }}|{{ '{{' }}index .Config.Labels "com.docker.compose.project.working_dir"{{ '}}' }}|{{ '{{' }}index .Config.Labels "com.docker.compose.project.config_files"{{ '}}' }}'
|
|
done | grep -v '^|' | sort -u
|
|
register: projects_raw
|
|
changed_when: false
|
|
check_mode: false
|
|
|
|
- name: List containers on the network that aren't from compose
|
|
ansible.builtin.shell: |
|
|
for id in $(docker ps -aq --filter network={{ net }}); do
|
|
p=$(docker inspect "$id" -f '{{ '{{' }}index .Config.Labels "com.docker.compose.project"{{ '}}' }}')
|
|
[ -z "$p" ] && docker inspect "$id" -f '{{ '{{' }}.Name{{ '}}' }}' | sed 's,^/,,'
|
|
done; true
|
|
register: loose_raw
|
|
changed_when: false
|
|
check_mode: false
|
|
|
|
- name: Parse project list
|
|
ansible.builtin.set_fact:
|
|
projects: >-
|
|
{% set out = [] %}{% for line in projects_raw.stdout_lines %}{% set p = line.split('|') %}{% set _ = out.append({'name': p[0], 'dir': p[1], 'files': p[2]}) %}{% endfor %}{{ out }}
|
|
loose: "{{ loose_raw.stdout_lines | select | list }}"
|
|
|
|
- name: Show what will be migrated
|
|
ansible.builtin.debug:
|
|
msg:
|
|
- "{{ projects | length }} compose projects: {{ projects | map(attribute='name') | join(', ') }}"
|
|
- "Non-compose containers to detach: {{ loose | join(', ') or 'none' }}"
|
|
|
|
- name: Save the project list for recovery
|
|
ansible.builtin.copy:
|
|
dest: "{{ record }}"
|
|
content: "{{ projects_raw.stdout }}\n"
|
|
mode: '0600'
|
|
|
|
- name: Take down every project on the network (volumes are kept)
|
|
ansible.builtin.command:
|
|
argv: "{{ ['docker', 'compose', '-p', item.name, '--project-directory', item.dir]
|
|
+ (item.files.split(',') | map('regex_replace', '^', '--file=') | list)
|
|
+ ['down'] }}"
|
|
loop: "{{ projects }}"
|
|
loop_control:
|
|
label: "{{ item.name }}"
|
|
|
|
- name: Detach non-compose containers
|
|
ansible.builtin.command: docker network disconnect -f {{ net }} {{ item }}
|
|
loop: "{{ loose }}"
|
|
|
|
- name: Remove the old network
|
|
community.docker.docker_network:
|
|
name: "{{ net }}"
|
|
state: absent
|
|
|
|
- name: Create the network with the reserved range
|
|
community.docker.docker_network:
|
|
name: "{{ net }}"
|
|
driver: bridge
|
|
ipam_config:
|
|
- subnet: "{{ docker.subnet }}"
|
|
gateway: "{{ docker.gateway }}"
|
|
iprange: "{{ docker.ip_range }}"
|
|
|
|
# Caddy first so the sites come back as soon as possible
|
|
- name: Bring projects back up
|
|
ansible.builtin.command:
|
|
argv: "{{ ['docker', 'compose', '-p', item.name, '--project-directory', item.dir]
|
|
+ (item.files.split(',') | map('regex_replace', '^', '--file=') | list)
|
|
+ ['up', '-d'] }}"
|
|
loop: "{{ (projects | selectattr('name', 'equalto', 'caddy') | list)
|
|
+ (projects | rejectattr('name', 'equalto', 'caddy') | list) }}"
|
|
loop_control:
|
|
label: "{{ item.name }}"
|
|
register: ups
|
|
ignore_errors: true
|
|
|
|
- name: Make sure public traffic still reaches Caddy
|
|
ansible.builtin.command: /usr/local/bin/ufw-docker.sh allow caddy-caddy-1 {{ item }}
|
|
loop: [80/tcp, 443/tcp, 443/udp]
|
|
register: caddy_ufw
|
|
changed_when: "'Skipping' not in caddy_ufw.stdout"
|
|
|
|
- name: Report
|
|
ansible.builtin.debug:
|
|
msg:
|
|
- "Failed to start: {{ ups.results | selectattr('failed') | map(attribute='item.name') | join(', ') or 'none' }}"
|
|
- "Recovery list: {{ record }}"
|
|
- "Detached (reconnect or remove by hand): {{ loose | join(', ') or 'none' }}"
|
|
- "Next: run site.yml to give gitea its fixed IP and fix its firewall rule."
|