Ansible vs Nginx: Automation vs Web Server Guide
By Luca Berton · Published 2026-04-03 · Category: installation
Ansible vs Nginx: Ansible automates config management, Nginx is a web server. Install, configure, and automate Nginx with Ansible playbooks.
Nginx is the most popular web server and reverse proxy — and Ansible makes it easy to automate installation, configuration, and deployment across your infrastructure.
Ansible vs Nginx: What's the Difference?
Ansible and Nginx aren't competitors — they solve different problems. Ansible is an automation and configuration management tool, while Nginx is a web server and reverse proxy. This guide uses Ansible to install, configure, and automate Nginx deployments, so you get consistent, repeatable results across every server.
See also: AWX Behind Reverse Proxy: Nginx, Traefik, Caddy & Apache Setup Guide
Install Nginx
Ubuntu/Debian
- name: Install Nginx on Ubuntu
hosts: webservers
become: true
tasks:
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
- name: Ensure nginx is running
ansible.builtin.service:
name: nginx
state: started
enabled: trueRHEL/CentOS/AlmaLinux
- name: Install Nginx on RHEL
hosts: webservers
become: true
tasks:
- name: Install EPEL repository
ansible.builtin.dnf:
name: epel-release
state: present
- name: Install nginx
ansible.builtin.dnf:
name: nginx
state: present
- name: Start and enable nginx
ansible.builtin.service:
name: nginx
state: started
enabled: true
- name: Open firewall ports
ansible.posix.firewalld:
service: "{{ item }}"
permanent: true
immediate: true
state: enabled
loop:
- http
- httpsConfigure Virtual Hosts
- name: Configure nginx virtual hosts
hosts: webservers
become: true
vars:
sites:
- name: myapp.example.com
root: /var/www/myapp
port: 80
- name: api.example.com
root: /var/www/api
port: 80
tasks:
- name: Create document roots
ansible.builtin.file:
path: "{{ item.root }}"
state: directory
owner: www-data
group: www-data
mode: '0755'
loop: "{{ sites }}"
- name: Deploy virtual host configs
ansible.builtin.template:
src: vhost.conf.j2
dest: "/etc/nginx/sites-available/{{ item.name }}.conf"
loop: "{{ sites }}"
notify: reload nginx
- name: Enable virtual hosts
ansible.builtin.file:
src: "/etc/nginx/sites-available/{{ item.name }}.conf"
dest: "/etc/nginx/sites-enabled/{{ item.name }}.conf"
state: link
loop: "{{ sites }}"
notify: reload nginx
handlers:
- name: reload nginx
ansible.builtin.service:
name: nginx
state: reloaded{# templates/vhost.conf.j2 #}
server {
listen {{ item.port }};
server_name {{ item.name }};
root {{ item.root }};
index index.html;
access_log /var/log/nginx/{{ item.name }}-access.log;
error_log /var/log/nginx/{{ item.name }}-error.log;
location / {
try_files $uri $uri/ =404;
}
}See also: Automating Nginx Reverse Proxy Setup for Flask on RHEL
SSL/TLS with Let's Encrypt
- name: Setup SSL with Certbot
hosts: webservers
become: true
vars:
domain: myapp.example.com
email: admin@example.com
tasks:
- name: Install Certbot
ansible.builtin.apt:
name:
- certbot
- python3-certbot-nginx
state: present
- name: Obtain SSL certificate
ansible.builtin.command: >
certbot --nginx -d {{ domain }}
--non-interactive --agree-tos
--email {{ email }}
--redirect
args:
creates: "/etc/letsencrypt/live/{{ domain }}/fullchain.pem"
- name: Setup auto-renewal
ansible.builtin.cron:
name: "certbot renewal"
special_time: weekly
job: "certbot renew --quiet --post-hook 'systemctl reload nginx'"Reverse Proxy
# templates/reverse-proxy.conf.j2
upstream backend {
{% for server in backend_servers %}
server {{ server }}:{{ backend_port | default(8080) }};
{% endfor %}
}
server {
listen 443 ssl http2;
server_name {{ domain }};
ssl_certificate /etc/letsencrypt/live/{{ domain }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ domain }}/privkey.pem;
location / {
proxy_pass http://backend;
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;
}
}See also: Networking Throttle Strategies for Managing 3000 Servers with Ansible
Load Balancing
upstream app_servers {
{% for server in groups['appservers'] %}
server {{ hostvars[server].ansible_host }}:8080{% if server == groups['appservers'][0] %} weight=3{% endif %};
{% endfor %}
}
server {
listen 80;
server_name {{ domain }};
location / {
proxy_pass http://app_servers;
proxy_next_upstream error timeout http_502 http_503;
}
}Security Hardening
- name: Deploy hardened nginx config
ansible.builtin.template:
src: nginx-security.conf.j2
dest: /etc/nginx/conf.d/security.conf
notify: reload nginx{# templates/nginx-security.conf.j2 #}
# 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;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'" always;
# Hide nginx version
server_tokens off;
# SSL hardening
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_stapling on;
ssl_stapling_verify on;FAQ
Should I use a role or write my own tasks?
For production, use a maintained role like geerlingguy.nginx from Ansible Galaxy. Write custom tasks for specific requirements or learning.
How do I test nginx config changes safely?
Use validate parameter: ansible.builtin.template: validate: 'nginx -t -c %s'. This runs nginx -t before applying changes.
How do I handle nginx on both Debian and RedHat?
Use ansible_os_family conditions or role defaults: config paths differ (/etc/nginx/sites-enabled/ on Debian vs /etc/nginx/conf.d/ on RedHat).
Install Nginx
- name: Install Nginx
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
become: true
- name: Start and enable
ansible.builtin.service:
name: nginx
state: started
enabled: true
become: trueConfigure Virtual Host
- name: Deploy vhost config
ansible.builtin.template:
src: vhost.conf.j2
dest: /etc/nginx/sites-available/{{ domain }}
become: true
notify: reload nginx
- name: Enable vhost
ansible.builtin.file:
src: /etc/nginx/sites-available/{{ domain }}
dest: /etc/nginx/sites-enabled/{{ domain }}
state: link
become: true
notify: reload nginx
- name: Remove default site
ansible.builtin.file:
path: /etc/nginx/sites-enabled/default
state: absent
become: true
notify: reload nginx{# templates/vhost.conf.j2 #}
server {
listen 80;
server_name {{ domain }} www.{{ domain }};
root {{ document_root }};
index index.html;
location / {
try_files $uri $uri/ =404;
}
access_log /var/log/nginx/{{ domain }}-access.log;
error_log /var/log/nginx/{{ domain }}-error.log;
}Reverse Proxy
server {
listen 80;
server_name {{ domain }};
location / {
proxy_pass http://{{ backend_host }}:{{ backend_port }};
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;
}
}SSL with Let's Encrypt
- name: Install certbot
apt:
name: [certbot, python3-certbot-nginx]
become: true
- name: Obtain SSL certificate
command: >
certbot --nginx -d {{ domain }} -d www.{{ domain }}
--non-interactive --agree-tos -m {{ admin_email }}
become: true
args:
creates: /etc/letsencrypt/live/{{ domain }}
- name: Auto-renewal cron
cron:
name: "certbot renewal"
minute: "0"
hour: "3"
job: "certbot renew --quiet"
become: trueLoad Balancer
upstream backend {
{% for host in groups['appservers'] %}
server {{ hostvars[host].ansible_host }}:8080;
{% endfor %}
}
server {
listen 80;
server_name {{ domain }};
location / {
proxy_pass http://backend;
}
}Complete Role
# roles/nginx/tasks/main.yml
- apt: name=nginx state=present update_cache=true
become: true
- template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
validate: nginx -t -c %s
become: true
notify: reload nginx
- template:
src: vhost.conf.j2
dest: "/etc/nginx/sites-available/{{ item.domain }}"
loop: "{{ nginx_sites }}"
become: true
notify: reload nginx
- file:
src: "/etc/nginx/sites-available/{{ item.domain }}"
dest: "/etc/nginx/sites-enabled/{{ item.domain }}"
state: link
loop: "{{ nginx_sites }}"
become: true
- service: name=nginx state=started enabled=true
become: trueFAQ
How do I validate config before reload?
- template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
validate: nginx -t -c %sThe validate parameter runs the check before writing the file.
reload vs restart?
reload applies config changes without dropping connections. restart stops/starts the process. Always prefer reload for config changes.
How do I handle multiple sites?
Loop over a list of site configs and generate per-site files in sites-available/.
Install Nginx
- ansible.builtin.apt:
name: nginx
state: present
update_cache: true
become: true
- service:
name: nginx
state: started
enabled: true
become: trueVirtual Host Template
- template:
src: vhost.conf.j2
dest: "/etc/nginx/sites-available/{{ domain }}"
become: true
notify: reload nginx
- file:
src: "/etc/nginx/sites-available/{{ domain }}"
dest: "/etc/nginx/sites-enabled/{{ domain }}"
state: link
become: true
notify: reload nginx{# templates/vhost.conf.j2 #}
server {
listen 80;
server_name {{ domain }} www.{{ domain }};
root {{ document_root }};
index index.html;
access_log /var/log/nginx/{{ domain }}_access.log;
error_log /var/log/nginx/{{ domain }}_error.log;
location / {
try_files $uri $uri/ =404;
}
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
}Reverse Proxy
server {
listen 80;
server_name {{ domain }};
location / {
proxy_pass http://{{ backend_host }}:{{ backend_port }};
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;
}
}SSL with Let's Encrypt
- apt: { name: [certbot, python3-certbot-nginx] }
become: true
- command: >
certbot --nginx -d {{ domain }} -d www.{{ domain }}
--non-interactive --agree-tos --email {{ admin_email }}
become: true
args:
creates: "/etc/letsencrypt/live/{{ domain }}/fullchain.pem"Load Balancer
upstream backend {
{% for server in backend_servers %}
server {{ server }}:{{ backend_port }} weight={{ loop.index0 == 0 | ternary(3, 1) }};
{% endfor %}
}
server {
listen 80;
server_name {{ domain }};
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
}
}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;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;Handler
handlers:
- name: reload nginx
service:
name: nginx
state: reloaded
become: true
- name: restart nginx
service:
name: nginx
state: restarted
become: trueValidate Config Before Reload
- template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
validate: "nginx -t -c %s"
become: true
notify: reload nginxFAQ
How to remove the default site?
- file: { path: /etc/nginx/sites-enabled/default, state: absent }
notify: reload nginxHow to check Nginx status?
- command: nginx -t
become: true
register: nginx_statusreload vs restart?
Reload applies config changes gracefully (no downtime). Restart fully stops and starts Nginx (brief downtime). Always prefer reload.
Related Articles
Category: installation