diff --git a/.github/workflows/ansible-deploy.yml b/.github/workflows/ansible-deploy.yml new file mode 100644 index 0000000000..afc1c1212a --- /dev/null +++ b/.github/workflows/ansible-deploy.yml @@ -0,0 +1,91 @@ +name: Ansible Deployment + +on: + push: + branches: [ main ] + paths: + - 'ansible/**' + - '.github/workflows/ansible-deploy.yml' + pull_request: + branches: [ main ] + paths: + - 'ansible/**' + +jobs: + lint: + name: Lint Ansible Code + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install ansible-lint + run: pip install ansible-lint + + - name: Run ansible-lint + run: | + cd ansible + ansible-lint playbooks/deploy.yml + + deploy: + name: Deploy Application + needs: lint + runs-on: ubuntu-latest + if: github.event_name == 'push' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install Ansible + run: pip install ansible + + - name: Setup SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -H ${{ secrets.VM_HOST }} >> ~/.ssh/known_hosts + + - name: Create inventory + run: | + mkdir -p ansible/inventory + echo "[webservers]" > ansible/inventory/hosts.ini + echo "${{ secrets.VM_HOST }} ansible_user=${{ secrets.VM_USER }}" >> ansible/inventory/hosts.ini + + - name: Decrypt vault password + run: echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass + + - name: Run Ansible playbook + run: | + cd ansible + ansible-playbook playbooks/deploy.yml \ + -i inventory/hosts.ini \ + --vault-password-file /tmp/vault_pass + + - name: Cleanup + if: always() + run: rm -f /tmp/vault_pass + + verify: + name: Verify Deployment + needs: deploy + runs-on: ubuntu-latest + if: success() + + steps: + - name: Check application + run: | + sleep 10 + curl -f http://${{ secrets.VM_HOST }}:8000/health || exit 1 + echo "Application verified successfully" \ No newline at end of file diff --git a/ansible/docs/LAB06.md b/ansible/docs/LAB06.md new file mode 100644 index 0000000000..d846f50a60 --- /dev/null +++ b/ansible/docs/LAB06.md @@ -0,0 +1,128 @@ +# Lab 6 — Advanced Ansible & CI/CD + +## 1. Overview + +This project automates application deployment using Ansible and GitHub Actions. I took a basic Ansible setup and added proper structure, safety features, and CI/CD automation. + +What I Used: + +- Ansible for automation +- Docker Compose for containers +- GitHub Actions for CI/CD +- Ubuntu servers for deployment + +## 2. Blocks & Tags + +I organized each role using blocks to group related tasks: + +Common Role - Groups package tasks and user tasks separately +Docker Role - Separates installation from configuration + +Tag Strategy: + +- packages - Just install packages +- users - Just manage users +- docker_install - Only Docker installation +- docker_config - Only Docker setup +- web_app_wipe - Only cleanup operations + +Example Usage: +```bash +ansible-playbook deploy.yml --tags docker_install + +ansible-playbook deploy.yml --skip-tags common + +ansible-playbook deploy.yml --list-tags +``` + +![](screenshots/tags.jpg) + +![](screenshots/tag-docker.jpg) + +## 3. Docker Compose Migration + +I replaced the old docker run approach with Docker Compose templates. + +Before: Manual container management with multiple tasks +After: Single declarative docker-compose.yml template + +The template supports: + +- Dynamic service names and ports +- Environment variables (including vault secrets) +- Health checks +- Restart policies + +I also added proper role dependencies - the web_app role now automatically pulls in the docker role, so Docker is always installed first. + +## 4. Wipe Logic + +This was tricky - needed a way to completely remove the app, but make it really hard to do by accident. + +The Solution: Double safety - requires BOTH a variable AND a tag: + +```yaml +web_app_wipe: false + +when: web_app_wipe | bool +tags: web_app_wipe +``` + +Test Scenarios: + +- Normal deploy - wipe skipped (safe) +- Wipe only - -e "web_app_wipe=true" --tags web_app_wipe removes everything +- Clean reinstall - -e "web_app_wipe=true" wipes then deploys fresh +- Safety check - tag without variable = nothing happens + +The wipe task removes containers, compose file, and app directory. Optional image/volume cleanup too. + +![](screenshots/without-wipe.jpg) + +![](screenshots/with-wipe.jpg) + +## 5. CI/CD Pipeline + +GitHub Actions automates everything on git push: + +Workflow Steps: + +- Lint - Runs ansible-lint to catch syntax errors +- Deploy - Sets up SSH, decrypts vault, runs playbook +- Verify - Checks health endpoint, confirms container is running + +## 6. What I Learned + +Blocks are great for: + +-Grouping related tasks +- Applying conditions once +- Error handling with rescue/always + +Tag + Variable combo is perfect for dangerous operations like wipe - prevents accidents but still allows automation. + +Idempotency matters - Second run of the playbook shows "ok" not "changed". Docker Compose handles this automatically. + +CI/CD secrets need careful handling - I create temp files and immediately delete them, even on failure. + +## 7. Research Answers + +Q: Rescue block failure? + +A: Always block still runs, but playbook stops for that host. + +Q: Nested blocks? + +A: Yes, used them for package groups inside roles. + +Q: Tag inheritance? + +A: Tasks inherit parent block tags, can add their own. + +Q: Variable + tag why both? + +A: Double safety - tag for selective execution, variable for default-off behavior. + +Q: Self-hosted vs GitHub runner? + +A: Self-hosted is more secure (no SSH keys in GitHub), faster (same network), but needs maintenance. diff --git a/ansible/docs/screenshots/tag-docker.jpg b/ansible/docs/screenshots/tag-docker.jpg new file mode 100644 index 0000000000..89b7ca0ad5 Binary files /dev/null and b/ansible/docs/screenshots/tag-docker.jpg differ diff --git a/ansible/docs/screenshots/tags.jpg b/ansible/docs/screenshots/tags.jpg new file mode 100644 index 0000000000..6f6bc8dd52 Binary files /dev/null and b/ansible/docs/screenshots/tags.jpg differ diff --git a/ansible/docs/screenshots/with-wipe.jpg b/ansible/docs/screenshots/with-wipe.jpg new file mode 100644 index 0000000000..4cb5f72d88 Binary files /dev/null and b/ansible/docs/screenshots/with-wipe.jpg differ diff --git a/ansible/docs/screenshots/without-wipe.jpg b/ansible/docs/screenshots/without-wipe.jpg new file mode 100644 index 0000000000..ff812d8d7a Binary files /dev/null and b/ansible/docs/screenshots/without-wipe.jpg differ diff --git a/ansible/inventory/hosts.ini b/ansible/inventory/hosts.ini index 165cf011d6..786a977658 100644 --- a/ansible/inventory/hosts.ini +++ b/ansible/inventory/hosts.ini @@ -1,2 +1,2 @@ [webservers] -vm ansible_host=62.84.120.249 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/yc-key +vm ansible_host=89.169.183.195 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/yc-key diff --git a/ansible/playbooks/deploy.yml b/ansible/playbooks/deploy.yml index fac6006ca2..157c4fe04d 100644 --- a/ansible/playbooks/deploy.yml +++ b/ansible/playbooks/deploy.yml @@ -7,7 +7,7 @@ - ../group_vars/all.yml roles: - - name: app_deploy + - name: web_app vars: app_environment: ENVIRONMENT: production diff --git a/ansible/roles/app_deploy/tasks/main.yml b/ansible/roles/app_deploy/tasks/main.yml deleted file mode 100644 index 9e2651b7b9..0000000000 --- a/ansible/roles/app_deploy/tasks/main.yml +++ /dev/null @@ -1,71 +0,0 @@ -- name: Login to Docker Hub - docker_login: - username: "{{ dockerhub_username }}" - password: "{{ dockerhub_password }}" - become: yes - no_log: true - -- name: Pull Docker image - docker_image: - name: "{{ docker_image }}" - tag: "{{ docker_image_tag }}" - source: pull - force_source: yes - become: yes - register: pull_result - -- name: Get container info - docker_container_info: - name: "{{ app_container_name }}" - register: container_info - -- name: Stop existing container - docker_container: - name: "{{ app_container_name }}" - state: absent - become: yes - when: container_info.exists - -- name: Run application container - docker_container: - name: "{{ app_container_name }}" - image: "{{ docker_image }}:{{ docker_image_tag }}" - state: started - restart_policy: "{{ restart_policy }}" - ports: - - "{{ app_host_port }}:{{ app_port }}" - env: "{{ app_environment }}" - become: yes - register: run_result - notify: restart app container - -- name: Wait for application to start - wait_for: - port: "{{ app_host_port }}" - host: "localhost" - delay: 5 - timeout: 60 - state: started - become: no - -- name: Check health endpoint - uri: - url: "http://localhost:{{ app_host_port }}{{ health_endpoint }}" - method: GET - status_code: 200 - timeout: 10 - register: health_result - retries: 5 - delay: 3 - until: health_result.status == 200 - become: no - -- name: Display application info - debug: - msg: - - "Application deployed successfully!" - - "Container: {{ app_container_name }}" - - "Image: {{ docker_image }}:{{ docker_image_tag }}" - - "Port: {{ app_host_port }} -> {{ app_port }}" - - "Health check: {{ health_result.status }}" - - "Container status: {{ run_result.container.State.Status | default('running') }}" diff --git a/ansible/roles/common/tasks/main.yml b/ansible/roles/common/tasks/main.yml index 7e7efbf771..05ccff75a2 100644 --- a/ansible/roles/common/tasks/main.yml +++ b/ansible/roles/common/tasks/main.yml @@ -1,23 +1,67 @@ -- name: Update apt cache - apt: - update_cache: yes - cache_valid_time: 3600 - become: yes +- name: Common role tasks + block: + - name: Package installation block + block: + - name: Update apt cache + apt: + update_cache: yes + cache_valid_time: 3600 + + - name: Install common packages + apt: + name: "{{ common_packages }}" + state: present + + - name: Upgrade pip + pip: + name: pip + state: latest + + rescue: + - name: Handle apt cache update failure + debug: + msg: "Apt cache update failed, running fix..." + + - name: Fix missing packages + apt: + name: apt + state: latest + update_cache: yes + when: ansible_os_family == "Debian" + + tags: + - packages -- name: Install common packages - apt: - name: "{{ common_packages }}" - state: present - become: yes + - name: User creation block + block: + - name: Set timezone + timezone: + name: "{{ timezone }}" + when: timezone is defined + + tags: + - users -- name: Set timezone - timezone: - name: "{{ timezone }}" - become: yes - when: timezone is defined + rescue: + - name: Common role failure handler + debug: + msg: "Common role execution failed" + + - name: Log failure + copy: + content: "Common role failed at {{ ansible_date_time.iso8601 }}" + dest: /tmp/common_role_failed.log + mode: '0644' -- name: Upgrade pip - pip: - name: pip - state: latest - become: yes + always: + - name: Log completion + copy: + content: "Common role completed at {{ ansible_date_time.iso8601 }}" + dest: /tmp/common_role_completed.log + mode: '0644' + - debug: + msg: "Common role execution finished" + + become: true + tags: + - common \ No newline at end of file diff --git a/ansible/roles/docker/tasks/main.yml b/ansible/roles/docker/tasks/main.yml index 20f86d18d5..8b77b5d763 100644 --- a/ansible/roles/docker/tasks/main.yml +++ b/ansible/roles/docker/tasks/main.yml @@ -1,37 +1,88 @@ -- name: Add Docker GPG key - apt_key: - url: https://download.docker.com/linux/ubuntu/gpg - state: present - become: yes +- name: Docker role tasks + block: + - name: Docker installation block + block: + - name: Add Docker GPG key + apt_key: + url: https://download.docker.com/linux/ubuntu/gpg + state: present + + - name: Add Docker repository + apt_repository: + repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable" + state: present + + - name: Install Docker packages + apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io + state: present + notify: restart docker + + rescue: + - name: Wait before retry on GPG key failure + wait_for: + timeout: 10 + delegate_to: localhost + + - name: Retry Docker GPG key addition + apt_key: + url: https://download.docker.com/linux/ubuntu/gpg + state: present + + - debug: + msg: "Docker GPG key retry completed" + + tags: + - docker_install -- name: Add Docker repository - apt_repository: - repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable" - state: present - become: yes + - name: Docker configuration block + block: + - name: Ensure Docker service is running + systemd: + name: docker + state: started + enabled: yes + + - name: Add user to docker group + user: + name: "{{ item }}" + group: docker + append: yes + loop: "{{ docker_users }}" + + tags: + - docker_config -- name: Install Docker packages - apt: - name: - - docker-ce - - docker-ce-cli - - containerd.io - state: present - become: yes - notify: restart docker + rescue: + - name: Docker role failure handler + debug: + msg: "Docker role execution failed" + + - name: Log Docker failure + copy: + content: "Docker role failed at {{ ansible_date_time.iso8601 }}" + dest: /tmp/docker_role_failed.log + mode: '0644' -- name: Ensure Docker service is running - systemd: - name: docker - state: started - enabled: yes - become: yes - -- name: Add user to docker group - user: - name: "{{ item }}" - group: docker - append: yes - loop: "{{ docker_users }}" - become: yes + always: + - name: Ensure Docker service is enabled + systemd: + name: docker + enabled: yes + ignore_errors: yes + + - name: Log Docker role completion + copy: + content: "Docker role completed at {{ ansible_date_time.iso8601 }}" + dest: /tmp/docker_role_completed.log + mode: '0644' + + - debug: + msg: "Docker role execution finished" + become: true + tags: + - docker \ No newline at end of file diff --git a/ansible/roles/app_deploy/defaults/main.yml b/ansible/roles/web_app/defaults/main.yml similarity index 65% rename from ansible/roles/app_deploy/defaults/main.yml rename to ansible/roles/web_app/defaults/main.yml index 5a110d938e..0eddcea781 100644 --- a/ansible/roles/app_deploy/defaults/main.yml +++ b/ansible/roles/web_app/defaults/main.yml @@ -5,10 +5,13 @@ app_container_name: "{{ app_name }}" docker_image_tag: latest restart_policy: unless-stopped -app_environment: - ENVIRONMENT: development - LOG_LEVEL: debug +app_environment: production +app_debug: false health_check_retries: 30 health_check_delay: 2 health_endpoint: /health + +web_app_wipe: false +web_app_wipe_images: false +web_app_wipe_volumes: false \ No newline at end of file diff --git a/ansible/roles/app_deploy/handlers/main.yml b/ansible/roles/web_app/handlers/main.yml similarity index 100% rename from ansible/roles/app_deploy/handlers/main.yml rename to ansible/roles/web_app/handlers/main.yml diff --git a/ansible/roles/web_app/meta/main.yml b/ansible/roles/web_app/meta/main.yml new file mode 100644 index 0000000000..74475d7449 --- /dev/null +++ b/ansible/roles/web_app/meta/main.yml @@ -0,0 +1,7 @@ +dependencies: + - role: docker + tags: + - docker + - docker_install + vars: + docker_users: "{{ web_app_users | default(['ubuntu']) }}" diff --git a/ansible/roles/web_app/tasks/main.yml b/ansible/roles/web_app/tasks/main.yml new file mode 100644 index 0000000000..bf22fb201e --- /dev/null +++ b/ansible/roles/web_app/tasks/main.yml @@ -0,0 +1,162 @@ +- name: Include wipe tasks + include_tasks: wipe.yml + tags: + - web_app_wipe + +- name: Deploy application with Docker Compose + block: + - name: Login to Docker Hub + docker_login: + username: "{{ dockerhub_username }}" + password: "{{ dockerhub_password }}" + become: yes + no_log: true + tags: + - docker_login + - compose + + - name: Create application directory + file: + path: "{{ compose_project_dir | default('/opt/' + (web_app_name | default('devops-app'))) }}" + state: directory + mode: '0755' + become: yes + tags: + - app_deploy + - compose + - directories + + - name: Template docker-compose.yml + template: + src: docker-compose.yml.j2 + dest: "{{ compose_project_dir | default('/opt/' + (web_app_name | default('devops-app'))) }}/docker-compose.yml" + mode: '0644' + become: yes + register: compose_file + tags: + - app_deploy + - compose + - templates + + - name: Install Docker Compose Python library + pip: + name: docker-compose + state: present + become: yes + tags: + - app_deploy + - compose + - dependencies + + - name: Deploy with Docker Compose + community.docker.docker_compose_v2: + project_src: "{{ compose_project_dir | default('/opt/' + (web_app_name | default('devops-app'))) }}" + files: + - docker-compose.yml + state: present + pull: always + recreate: always + become: yes + register: compose_result + tags: + - app_deploy + - compose + - deploy + + - name: Wait for application to start + wait_for: + port: "{{ app_host_port | default('8000') }}" + host: "localhost" + delay: 5 + timeout: 60 + state: started + become: no + tags: + - app_deploy + - health_check + + - name: Check health endpoint + uri: + url: "http://localhost:{{ app_host_port | default('8000') }}{{ health_endpoint | default('/health') }}" + method: GET + status_code: 200 + timeout: 10 + register: health_result + retries: 5 + delay: 3 + until: health_result.status == 200 + become: no + tags: + - app_deploy + - health_check + + - name: Display application info + debug: + msg: + - "Application deployed successfully with Docker Compose!" + - "Project directory: {{ compose_project_dir | default('/opt/' + (web_app_name | default('devops-app'))) }}" + - "Container: {{ web_app_name | default('devops-app') }}" + - "Image: {{ docker_image }}:{{ docker_tag | default('latest') }}" + - "Port: {{ app_host_port | default('8000') }} -> {{ app_port | default('8000') }}" + - "Health check: {{ health_result.status }}" + - "Compose status: {{ compose_result is changed | ternary('Changed', 'OK') }}" + tags: + - app_deploy + - debug + + rescue: + - name: Handle deployment failure + debug: + msg: "Docker Compose deployment failed, attempting rollback..." + tags: + - app_deploy + - rescue + + - name: Log failure + copy: + content: | + Deployment failed at {{ ansible_date_time.iso8601 }} + Error: {{ ansible_failed_result.msg | default('Unknown error') }} + dest: "/tmp/{{ web_app_name | default('devops-app') }}_deploy_failed.log" + mode: '0644' + become: yes + tags: + - app_deploy + - rescue + + - name: Attempt to rollback to previous version + community.docker.docker_compose_v2: + project_src: "{{ compose_project_dir | default('/opt/' + (web_app_name | default('devops-app'))) }}" + files: + - docker-compose.yml + state: present + pull: never + become: yes + ignore_errors: yes + when: compose_file.changed + tags: + - app_deploy + - rescue + - rollback + + always: + - name: Cleanup temporary files + file: + path: "/tmp/{{ web_app_name | default('devops-app') }}_*" + state: absent + become: yes + ignore_errors: yes + tags: + - app_deploy + - cleanup + + - name: Display completion message + debug: + msg: "Docker Compose deployment process completed" + tags: + - app_deploy + - debug + + tags: + - web_app + - app_deployment \ No newline at end of file diff --git a/ansible/roles/web_app/tasks/wipe.yml b/ansible/roles/web_app/tasks/wipe.yml new file mode 100644 index 0000000000..d6c7a08473 --- /dev/null +++ b/ansible/roles/web_app/tasks/wipe.yml @@ -0,0 +1,125 @@ +- name: Wipe web application + block: + - name: Display wipe start message + debug: + msg: "Starting wipe process for {{ web_app_name | default('devops-app') }}" + tags: + - web_app_wipe + - wipe_info + + - name: Stop and remove containers with Docker Compose + community.docker.docker_compose_v2: + project_src: "{{ compose_project_dir | default('/opt/' + (web_app_name | default('devops-app'))) }}" + files: + - docker-compose.yml + state: absent + remove_volumes: yes + remove_orphans: yes + become: yes + ignore_errors: yes + register: compose_down_result + tags: + - web_app_wipe + - wipe_containers + + - name: Remove docker-compose.yml file + file: + path: "{{ compose_project_dir | default('/opt/' + (web_app_name | default('devops-app'))) }}/docker-compose.yml" + state: absent + become: yes + ignore_errors: yes + tags: + - web_app_wipe + - wipe_files + + - name: Remove application directory + file: + path: "{{ compose_project_dir | default('/opt/' + (web_app_name | default('devops-app'))) }}" + state: absent + become: yes + ignore_errors: yes + register: dir_removed + tags: + - web_app_wipe + - wipe_directories + + - name: Optionally remove Docker images + block: + - name: Get image ID + docker_image_info: + name: "{{ docker_image }}:{{ docker_tag | default('latest') }}" + register: image_info + become: yes + ignore_errors: yes + + - name: Remove Docker image + docker_image: + name: "{{ docker_image }}:{{ docker_tag | default('latest') }}" + state: absent + become: yes + when: image_info.images | length > 0 + ignore_errors: yes + when: web_app_wipe_images | default(false) | bool + tags: + - web_app_wipe + - wipe_images + + - name: Remove any orphaned volumes + shell: | + docker volume ls -q --filter "label=com.docker.compose.project={{ web_app_name | default('devops-app') }}" | xargs -r docker volume rm + become: yes + ignore_errors: yes + when: web_app_wipe_volumes | default(false) | bool + tags: + - web_app_wipe + - wipe_volumes + + - name: Log wipe completion + copy: + content: | + Application wiped at {{ ansible_date_time.iso8601 }} + Directory removed: {{ dir_removed is changed | ternary('Yes', 'No') }} + Containers removed: {{ compose_down_result is changed | ternary('Yes', 'No') }} + dest: "/tmp/{{ web_app_name | default('devops-app') }}_wipe_{{ ansible_date_time.epoch }}.log" + mode: '0644' + become: yes + tags: + - web_app_wipe + - wipe_log + + - name: Display wipe completion message + debug: + msg: + - "Application {{ web_app_name | default('devops-app') }} wiped successfully!" + - "Directory: {{ compose_project_dir | default('/opt/' + (web_app_name | default('devops-app'))) }} removed" + - "Containers stopped and removed" + - "Log saved to /tmp/{{ web_app_name | default('devops-app') }}_wipe_*.log" + tags: + - web_app_wipe + - wipe_info + + rescue: + - name: Handle wipe failure + debug: + msg: "Wipe process encountered errors, but continuing with cleanup..." + tags: + - web_app_wipe + + - name: Force remove directory if still exists + shell: "rm -rf {{ compose_project_dir | default('/opt/' + (web_app_name | default('devops-app'))) }}" + become: yes + ignore_errors: yes + when: compose_project_dir is defined + tags: + - web_app_wipe + + always: + - name: Final cleanup message + debug: + msg: "Wipe process completed for {{ web_app_name | default('devops-app') }}" + tags: + - web_app_wipe + + when: web_app_wipe | default(false) | bool + tags: + - web_app_wipe \ No newline at end of file diff --git a/ansible/roles/web_app/templates/docker-compose.yml.j2 b/ansible/roles/web_app/templates/docker-compose.yml.j2 new file mode 100644 index 0000000000..b00e921bec --- /dev/null +++ b/ansible/roles/web_app/templates/docker-compose.yml.j2 @@ -0,0 +1,37 @@ +version: '{{ docker_compose_version | default("3.8") }}' + +services: + {{ web_app_name | default('devops-app') }}: + image: {{ docker_image }}:{{ docker_tag | default('latest') }} + container_name: {{ web_app_name | default('devops-app') }} + restart: {{ restart_policy | default('unless-stopped') }} + ports: + - "{{ app_host_port | default('8000') }}:{{ app_port | default('8000') }}" + environment: + - APP_ENV={{ app_environment | default('production') }} + - APP_SECRET_KEY={{ app_secret_key | default('') }} + - APP_DEBUG={{ app_debug | default('false') }} + - DATABASE_URL={{ database_url | default('') }} + - REDIS_URL={{ redis_url | default('') }} + {% if app_extra_env is defined %} + {% for key, value in app_extra_env.items() %} + - {{ key }}={{ value }} + {% endfor %} + {% endif %} + networks: + - {{ web_app_network | default('app_network') }} + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:{{ app_port | default('8000') }}{{ health_endpoint | default('/health') }}"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + +networks: + {{ web_app_network | default('app_network') }}: + driver: bridge \ No newline at end of file