Introduction

Nginx is the most popular web server and reverse proxy. Ansible automates the full lifecycle — install, configure virtual hosts, deploy SSL certificates, set up reverse proxy, and manage load balancing across your fleet.

Install Nginx

# Ubuntu/Debian
- name: Install Nginx
  ansible.builtin.apt:
    name: nginx
    state: present
    update_cache: true

# RHEL/CentOS
- name: Install Nginx
  ansible.builtin.yum:
    name: nginx
    state: present

# Start and enable
- name: Start Nginx
  ansible.builtin.service:
    name: nginx
    state: started
    enabled: true

Basic Configuration

# Deploy main config
- name: Configure Nginx
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    validate: "nginx -t -c %s"
  notify: Reload Nginx

handlers:
  - name: Reload Nginx
    ansible.builtin.service:
      name: nginx
      state: reloaded

nginx.conf.j2

# templates/nginx.conf.j2
user www-data;
worker_processes {{ ansible_processor_vcpus }};
pid /run/nginx.pid;

events {
    worker_connections {{ nginx_worker_connections | default(1024) }};
    multi_accept on;
}

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    server_tokens off;

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # Logging
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    # Gzip
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css application/json application/javascript text/xml;

    # Virtual hosts
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Virtual Host (Server Block)

- name: Deploy virtual host
  ansible.builtin.template:
    src: vhost.conf.j2
    dest: "/etc/nginx/sites-available/{{ domain }}.conf"
    validate: "nginx -t -c /etc/nginx/nginx.conf"
  notify: Reload Nginx

- name: Enable virtual host
  ansible.builtin.file:
    src: "/etc/nginx/sites-available/{{ domain }}.conf"
    dest: "/etc/nginx/sites-enabled/{{ domain }}.conf"
    state: link
  notify: Reload Nginx

- name: Remove default site
  ansible.builtin.file:
    path: /etc/nginx/sites-enabled/default
    state: absent
  notify: Reload Nginx

vhost.conf.j2

# templates/vhost.conf.j2
server {
    listen 80;
    server_name {{ domain }} www.{{ domain }};
    root {{ document_root | default('/var/www/' + domain) }};
    index index.html index.htm;

    location / {
        try_files $uri $uri/ =404;
    }

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;

    # Logging
    access_log /var/log/nginx/{{ domain }}.access.log;
    error_log /var/log/nginx/{{ domain }}.error.log;
}

SSL/TLS with Let's Encrypt

- name: Install Certbot
  ansible.builtin.apt:
    name: [certbot, python3-certbot-nginx]
    state: present

- name: Obtain SSL certificate
  ansible.builtin.command: >
    certbot --nginx -d {{ domain }} -d www.{{ domain }}
    --non-interactive --agree-tos -m {{ admin_email }}
  args:
    creates: "/etc/letsencrypt/live/{{ domain }}/fullchain.pem"

- name: Auto-renew cron
  ansible.builtin.cron:
    name: "Certbot renewal"
    job: "certbot renew --quiet --post-hook 'systemctl reload nginx'"
    hour: "3"
    minute: "30"
    weekday: "1"

SSL Virtual Host Template

# templates/vhost-ssl.conf.j2
server {
    listen 80;
    server_name {{ domain }} www.{{ domain }};
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name {{ domain }} www.{{ domain }};
    root {{ document_root }};

    ssl_certificate /etc/letsencrypt/live/{{ domain }}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/{{ domain }}/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    # HSTS
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    location / {
        try_files $uri $uri/ =404;
    }
}

Reverse Proxy

# templates/reverse-proxy.conf.j2
upstream {{ app_name }} {
{% for server in app_servers %}
    server {{ server }}:{{ app_port | default(8080) }};
{% endfor %}
}

server {
    listen 80;
    server_name {{ domain }};

    location / {
        proxy_pass http://{{ app_name }};
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 90;
    }
}
- name: Deploy reverse proxy
  ansible.builtin.template:
    src: reverse-proxy.conf.j2
    dest: /etc/nginx/sites-available/myapp.conf
  vars:
    app_name: myapp
    domain: app.example.com
    app_servers: "{{ groups['appservers'] | map('extract', hostvars, 'ansible_host') | list }}"
    app_port: 8080
  notify: Reload Nginx

Load Balancing

# templates/load-balancer.conf.j2
upstream backend {
    # Round-robin (default)
    {% for server in backend_servers %}
    server {{ server.host }}:{{ server.port }} weight={{ server.weight | default(1) }};
    {% endfor %}
    
    # Health check
    keepalive 32;
}

# Or: least_conn, ip_hash
upstream backend_sticky {
    ip_hash;
    {% for server in backend_servers %}
    server {{ server.host }}:{{ server.port }};
    {% endfor %}
}

Complete Web Server Playbook

---
- name: Setup Nginx web server
  hosts: webservers
  become: true
  vars:
    domain: example.com
    admin_email: admin@example.com
    document_root: /var/www/{{ domain }}

  tasks:
    - name: Install Nginx
      ansible.builtin.apt:
        name: nginx
        state: present

    - name: Create document root
      ansible.builtin.file:
        path: "{{ document_root }}"
        state: directory
        owner: www-data
        group: www-data

    - name: Deploy Nginx config
      ansible.builtin.template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        validate: "nginx -t -c %s"
      notify: Reload Nginx

    - name: Deploy virtual host
      ansible.builtin.template:
        src: vhost.conf.j2
        dest: /etc/nginx/sites-available/{{ domain }}.conf
      notify: Reload Nginx

    - name: Enable site
      ansible.builtin.file:
        src: /etc/nginx/sites-available/{{ domain }}.conf
        dest: /etc/nginx/sites-enabled/{{ domain }}.conf
        state: link
      notify: Reload Nginx

    - name: Start Nginx
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

  handlers:
    - name: Reload Nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded

Troubleshooting

"nginx: [emerg] bind() to 0.0.0.0:80 failed"

Port 80 already in use:

- name: Check what's using port 80
  ansible.builtin.command: ss -tlnp | grep :80
  changed_when: false

Config Validation

Always validate before applying:

- template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    validate: "nginx -t -c %s"    # Fails if config is invalid

Conclusion

Ansible + Nginx is a powerful combination for web server automation. Use Jinja2 templates for dynamic configs that scale across hosts. Always use validate to catch config errors before they cause downtime. Use handlers for reload (not restart) to avoid dropping connections. Automate SSL with Certbot. For reverse proxy and load balancing, build upstream blocks dynamically from inventory groups.