--- # Clean Redeploy Traefik and Gitea Stacks # Complete redeployment with backup, container recreation, and verification # # Usage: # # With automatic backup # ansible-playbook -i inventory/production.yml playbooks/setup/redeploy-traefik-gitea-clean.yml \ # --vault-password-file secrets/.vault_pass # # # With existing backup # ansible-playbook -i inventory/production.yml playbooks/setup/redeploy-traefik-gitea-clean.yml \ # --vault-password-file secrets/.vault_pass \ # -e "backup_name=redeploy-backup-1234567890" \ # -e "skip_backup=true" - name: Clean Redeploy Traefik and Gitea hosts: production gather_facts: yes become: no vars: traefik_stack_path: "{{ stacks_base_path }}/traefik" gitea_stack_path: "{{ stacks_base_path }}/gitea" gitea_url: "https://{{ gitea_domain }}" traefik_container_name: "traefik" gitea_container_name: "gitea" backup_base_path: "{{ backups_path | default('/home/deploy/backups') }}" skip_backup: "{{ skip_backup | default(false) | bool }}" backup_name: "{{ backup_name | default('') }}" tasks: # ======================================== # 1. BACKUP (unless skipped) # ======================================== - name: Set backup name fact ansible.builtin.set_fact: actual_backup_name: "{{ backup_name | default('redeploy-backup-' + ansible_date_time.epoch) }}" when: not skip_backup - name: Display backup note ansible.builtin.debug: msg: | ⚠️ NOTE: Backup should be run separately before redeploy: ansible-playbook -i inventory/production.yml playbooks/maintenance/backup-before-redeploy.yml \ --vault-password-file secrets/.vault_pass \ -e "backup_name={{ actual_backup_name }}" Or use existing backup with: -e "backup_name=redeploy-backup-XXXXX" -e "skip_backup=true" when: not skip_backup - name: Display redeployment plan ansible.builtin.debug: msg: | ================================================================================ CLEAN REDEPLOY TRAEFIK AND GITEA ================================================================================ This playbook will: 1. ✅ Backup ({% if skip_backup %}SKIPPED{% else %}Performed{% endif %}) 2. ✅ Stop and remove Traefik containers (keeps acme.json) 3. ✅ Stop and remove Gitea containers (keeps volumes/data) 4. ✅ Sync latest stack configurations 5. ✅ Redeploy Traefik stack 6. ✅ Redeploy Gitea stack 7. ✅ Restore Gitea configuration (app.ini) 8. ✅ Verify service discovery 9. ✅ Test Gitea accessibility ⚠️ IMPORTANT: - SSL certificates (acme.json) will be preserved - Gitea data (volumes) will be preserved - Only containers will be recreated - Expected downtime: ~2-5 minutes {% if not skip_backup %} - Backup location: {{ backup_base_path }}/{{ actual_backup_name }} {% endif %} ================================================================================ # ======================================== # 2. STOP AND REMOVE CONTAINERS # ======================================== - name: Stop Traefik stack ansible.builtin.shell: | cd {{ traefik_stack_path }} docker compose down register: traefik_stop changed_when: traefik_stop.rc == 0 failed_when: false - name: Remove Traefik containers (if any remain) ansible.builtin.shell: | docker ps -a --filter "name={{ traefik_container_name }}" --format "{{ '{{' }}.ID{{ '}}' }}" | xargs -r docker rm -f 2>/dev/null || true register: traefik_remove changed_when: traefik_remove.rc == 0 failed_when: false - name: Stop Gitea stack (preserves volumes) ansible.builtin.shell: | cd {{ gitea_stack_path }} docker compose down register: gitea_stop changed_when: gitea_stop.rc == 0 failed_when: false - name: Remove Gitea containers (if any remain, volumes are preserved) ansible.builtin.shell: | docker ps -a --filter "name={{ gitea_container_name }}" --format "{{ '{{' }}.ID{{ '}}' }}" | xargs -r docker rm -f 2>/dev/null || true register: gitea_remove changed_when: gitea_remove.rc == 0 failed_when: false # ======================================== # 3. SYNC CONFIGURATIONS # ======================================== - name: Get stacks directory path ansible.builtin.set_fact: stacks_source_path: "{{ playbook_dir | dirname | dirname | dirname }}/stacks" delegate_to: localhost run_once: true - name: Sync stacks directory to production server ansible.builtin.synchronize: src: "{{ stacks_source_path }}/" dest: "{{ stacks_base_path }}/" delete: no recursive: yes rsync_opts: - "--chmod=D755,F644" - "--exclude=.git" - "--exclude=*.log" - "--exclude=data/" - "--exclude=volumes/" - "--exclude=acme.json" # Preserve SSL certificates - "--exclude=*.key" - "--exclude=*.pem" # ======================================== # 4. ENSURE ACME.JSON EXISTS # ======================================== - name: Check if acme.json exists ansible.builtin.stat: path: "{{ traefik_stack_path }}/acme.json" register: acme_json_stat - name: Ensure acme.json exists and has correct permissions ansible.builtin.file: path: "{{ traefik_stack_path }}/acme.json" state: touch mode: '0600' owner: "{{ ansible_user }}" group: "{{ ansible_user }}" become: yes register: acme_json_ensure # ======================================== # 5. REDEPLOY TRAEFIK # ======================================== - name: Deploy Traefik stack community.docker.docker_compose_v2: project_src: "{{ traefik_stack_path }}" state: present pull: always register: traefik_deploy - name: Wait for Traefik to be ready ansible.builtin.shell: | cd {{ traefik_stack_path }} docker compose ps {{ traefik_container_name }} | grep -Eiq "Up|running" register: traefik_ready changed_when: false until: traefik_ready.rc == 0 retries: 12 delay: 5 failed_when: traefik_ready.rc != 0 # ======================================== # 6. REDEPLOY GITEA # ======================================== - name: Deploy Gitea stack community.docker.docker_compose_v2: project_src: "{{ gitea_stack_path }}" state: present pull: always register: gitea_deploy - name: Wait for Gitea to be ready ansible.builtin.shell: | cd {{ gitea_stack_path }} docker compose ps {{ gitea_container_name }} | grep -Eiq "Up|running" register: gitea_ready changed_when: false until: gitea_ready.rc == 0 retries: 12 delay: 5 failed_when: gitea_ready.rc != 0 - name: Wait for Gitea to be healthy ansible.builtin.shell: | cd {{ gitea_stack_path }} docker compose exec -T {{ gitea_container_name }} curl -f http://localhost:3000/api/healthz 2>&1 | grep -q "status.*pass" && echo "HEALTHY" || echo "NOT_HEALTHY" register: gitea_health changed_when: false until: gitea_health.stdout == "HEALTHY" retries: 30 delay: 2 failed_when: false # ======================================== # 7. RESTORE GITEA CONFIGURATION # ======================================== - name: Restore Gitea app.ini from backup ansible.builtin.shell: | if [ -f "{{ backup_base_path }}/{{ actual_backup_name }}/gitea-app.ini" ]; then cd {{ gitea_stack_path }} docker compose exec -T {{ gitea_container_name }} sh -c "cat > /data/gitea/conf/app.ini" < "{{ backup_base_path }}/{{ actual_backup_name }}/gitea-app.ini" docker compose restart {{ gitea_container_name }} echo "app.ini restored and Gitea restarted" else echo "No app.ini backup found, using default configuration" fi when: not skip_backup register: gitea_app_ini_restore changed_when: false failed_when: false # ======================================== # 8. VERIFY SERVICE DISCOVERY # ======================================== - name: Wait for service discovery (Traefik needs time to discover Gitea) ansible.builtin.pause: seconds: 15 - name: Check if Gitea is in traefik-public network ansible.builtin.shell: | docker network inspect traefik-public --format '{{ '{{' }}range .Containers{{ '}}' }}{{ '{{' }}.Name{{ '}}' }} {{ '{{' }}end{{ '}}' }}' 2>/dev/null | grep -q {{ gitea_container_name }} && echo "YES" || echo "NO" register: gitea_in_network changed_when: false - name: Test direct connection from Traefik to Gitea ansible.builtin.shell: | cd {{ traefik_stack_path }} docker compose exec -T {{ traefik_container_name }} wget -qO- --timeout=5 http://{{ gitea_container_name }}:3000/api/healthz 2>&1 | head -5 || echo "CONNECTION_FAILED" register: traefik_gitea_direct changed_when: false failed_when: false # ======================================== # 9. FINAL VERIFICATION # ======================================== - name: Test Gitea via HTTPS (with retries) ansible.builtin.uri: url: "{{ gitea_url }}/api/healthz" method: GET status_code: [200] validate_certs: false timeout: 10 register: gitea_https_test until: gitea_https_test.status == 200 retries: 20 delay: 3 changed_when: false failed_when: false - name: Check SSL certificate status ansible.builtin.shell: | cd {{ traefik_stack_path }} if [ -f acme.json ] && [ -s acme.json ]; then echo "SSL certificates: PRESENT" else echo "SSL certificates: MISSING or EMPTY" fi register: ssl_status changed_when: false - name: Final status summary ansible.builtin.debug: msg: | ================================================================================ REDEPLOYMENT SUMMARY ================================================================================ Traefik: - Status: {{ traefik_ready.rc | ternary('Up', 'Down') }} - SSL Certificates: {{ ssl_status.stdout }} Gitea: - Status: {{ gitea_ready.rc | ternary('Up', 'Down') }} - Health: {% if gitea_health.stdout == 'HEALTHY' %}✅ Healthy{% else %}❌ Not Healthy{% endif %} - Configuration: {% if gitea_app_ini_restore.changed %}✅ Restored{% else %}ℹ️ Using default{% endif %} Service Discovery: - Gitea in network: {% if gitea_in_network.stdout == 'YES' %}✅{% else %}❌{% endif %} - Direct connection: {% if 'CONNECTION_FAILED' not in traefik_gitea_direct.stdout %}✅{% else %}❌{% endif %} Gitea Accessibility: {% if gitea_https_test.status == 200 %} ✅ Gitea is reachable via HTTPS (Status: 200) URL: {{ gitea_url }} {% else %} ❌ Gitea is NOT reachable via HTTPS (Status: {{ gitea_https_test.status | default('TIMEOUT') }}) Possible causes: 1. SSL certificate is still being generated (wait 2-5 minutes) 2. Service discovery needs more time (wait 1-2 minutes) 3. Network configuration issue Next steps: - Wait 2-5 minutes and test again: curl -k {{ gitea_url }}/api/healthz - Check Traefik logs: cd {{ traefik_stack_path }} && docker compose logs {{ traefik_container_name }} --tail=50 - Check Gitea logs: cd {{ gitea_stack_path }} && docker compose logs {{ gitea_container_name }} --tail=50 {% endif %} {% if not skip_backup %} Backup location: {{ backup_base_path }}/{{ actual_backup_name }} To rollback: ansible-playbook -i inventory/production.yml playbooks/maintenance/rollback-redeploy.yml \ --vault-password-file secrets/.vault_pass \ -e "backup_name={{ actual_backup_name }}" {% endif %} ================================================================================