100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
DevOps

Deploying a Web App with Ansible

A hands-on walkthrough of provisioning a server, installing dependencies, and deploying a web application with an Ansible playbook, including zero-downtime rollout patterns.

PracticeIntermediate11 min readJul 10, 2026
Analogies

Planning the Deployment Playbook

A typical web application deployment playbook needs to accomplish several distinct phases in order: installing system packages and runtime dependencies (like Python, Node.js, or a JVM), configuring a reverse proxy such as nginx, pulling or copying the application code to the target host, installing application-level dependencies (pip/npm packages), applying database migrations, and finally restarting the application service. Structuring this as an ordered list of roles — common, nginx, app, database — rather than one flat task list makes it possible to reuse the common and nginx roles across many different applications while keeping app-specific logic isolated.

🏏

Cricket analogy: This staged deployment is like a well-drilled run chase: first you set the platform (powerplay overs, like installing base packages), then you accelerate in the middle overs (app deployment), and finally you finish strong in the death overs (service restart) — skip a stage and the whole innings collapses.

yaml
# site.yml
---
- name: Deploy web application
  hosts: webservers
  become: true
  vars_files:
    - group_vars/production/vars.yml
  roles:
    - common
    - nginx
    - app

# roles/app/tasks/main.yml
- name: Create app directory
  ansible.builtin.file:
    path: /opt/myapp
    state: directory
    owner: appuser
    group: appuser
    mode: '0755'

- name: Clone application release
  ansible.builtin.git:
    repo: 'https://github.com/example/myapp.git'
    dest: /opt/myapp
    version: "{{ app_version }}"
  notify: Restart myapp

Templating Configuration with Jinja2

Rather than shipping a static nginx.conf or environment file, Ansible's template module renders a Jinja2 .j2 file, substituting variables that can differ per environment — the domain name, upstream port, or database connection string. This means the same nginx.conf.j2 template produces correct configuration for staging, production, and a developer's test host purely by changing the variables in group_vars/<env>/vars.yml, without duplicating the template file itself. Templates also support conditionals and loops, so a single file can render a different upstream block depending on how many app server instances are defined in the inventory.

🏏

Cricket analogy: A Jinja2 template is like a single scorecard format used for every match, where only the variables — team names, venue, date — change per game, rather than designing a brand-new scorecard layout for every single fixture.

nginx
# roles/nginx/templates/nginx.conf.j2
upstream {{ app_name }}_backend {
{% for host in groups['app_servers'] %}
    server {{ hostvars[host]['ansible_default_ipv4']['address'] }}:{{ app_port }};
{% endfor %}
}

server {
    listen 80;
    server_name {{ domain_name }};

    location / {
        proxy_pass http://{{ app_name }}_backend;
        proxy_set_header Host $host;
    }
}

Handlers combined with notify mean nginx only reloads when the rendered template actually changes — if the variables are identical across a re-run, the template task reports no change and the reload handler never fires, keeping the deployment idempotent and avoiding unnecessary downtime.

Achieving Zero-Downtime Rollouts

For applications running behind a load balancer across multiple hosts, the serial keyword controls how many hosts Ansible updates at once, enabling a rolling deployment where only one host (or a percentage) is taken out of rotation at a time. Combined with max_fail_percentage, a rolling deploy automatically halts if too many hosts fail, preventing a bad release from being rolled out to the entire fleet. A typical pattern removes a host from the load balancer pool, deploys the new release, runs a health check, and only re-adds the host to the pool once the health check passes — repeating this one host (or batch) at a time until the whole fleet is updated.

🏏

Cricket analogy: The serial keyword is like rotating bowlers through a spell one at a time rather than resting the entire bowling attack simultaneously — you keep enough fielders active on the ground while each bowler takes their turn to be rotated out and refreshed.

yaml
---
- name: Rolling deploy of web app
  hosts: webservers
  become: true
  serial: "25%"
  max_fail_percentage: 20
  tasks:
    - name: Remove host from load balancer pool
      community.general.haproxy:
        state: disabled
        host: "{{ inventory_hostname }}"
        backend: app_backend
      delegate_to: "{{ groups['loadbalancer'][0] }}"

    - name: Deploy new release
      include_role:
        name: app

    - name: Wait for health check to pass
      ansible.builtin.uri:
        url: "http://{{ inventory_hostname }}:{{ app_port }}/healthz"
        status_code: 200
      retries: 10
      delay: 5
      register: health
      until: health.status == 200

    - name: Re-add host to load balancer pool
      community.general.haproxy:
        state: enabled
        host: "{{ inventory_hostname }}"
        backend: app_backend
      delegate_to: "{{ groups['loadbalancer'][0] }}"

Forgetting max_fail_percentage on a rolling deploy means a broken release can march through your entire fleet, host by host, until every server is down — always set a fail threshold so Ansible aborts the play after a small number of hosts fail their health check.

  • Structure a deployment as ordered roles (common, nginx, app, database) so pieces are reusable across projects.
  • Use the git or unarchive module to fetch a specific application release, tagged with notify handlers to restart services only on change.
  • Render environment-specific configuration with Jinja2 templates rather than duplicating static config files per environment.
  • Handlers fire only when a watched task reports a change, keeping restarts and reloads minimal and idempotent.
  • Use serial to control rolling deployment batch size and max_fail_percentage to auto-abort a bad rollout.
  • Remove a host from the load balancer pool before deploying to it, and re-add it only after a health check passes.
  • Always include a health-check step (uri module with retries/until) before returning traffic to a freshly deployed host.

Practice what you learned

Was this page helpful?

Topics covered

#DevOps#AnsibleStudyNotes#DeployingAWebAppWithAnsible#Deploying#Web#App#Ansible#WebDevelopment#StudyNotes#SkillVeris