Ansible Nginx: Install, Configure & Automate (Complete Guide)
By Luca Berton · Published 2026-04-03 · Category: installation
Complete guide to Ansible Nginx automation. Install Nginx, manage virtual hosts, SSL certificates, reverse proxy, and load balancing with examples.
Nginx is the most popular web server and reverse proxy — and Ansible makes it easy to automate installation, configuration, and deployment across your infrastructure.
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: true
RHEL/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
- https
See also: AWX Behind Reverse Proxy: Nginx, Traefik, Caddy & Apache Setup Guide
Configure 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;
}
}
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'"
See also: Automating Nginx Reverse Proxy Setup for Flask on RHEL
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;
}
}
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;
}
}
See also: Ansible London Meetup 2024 Recap: Highlights and Insights
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: true
Configure 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: true
Load 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: true
FAQ
How do I validate config before reload?
- template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
validate: nginx -t -c %s
The 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: true
Virtual 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: true
Validate Config Before Reload
- template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
validate: "nginx -t -c %s"
become: true
notify: reload nginx
FAQ
How to remove the default site?
- file: { path: /etc/nginx/sites-enabled/default, state: absent }
notify: reload nginx
How to check Nginx status?
- command: nginx -t
become: true
register: nginx_status
reload vs restart?
Reload applies config changes gracefully (no downtime). Restart fully stops and starts Nginx (brief downtime). Always prefer reload.
Related Articles
• Ansible Galaxy guide • rendering files with Ansible template • Ansible handlers guide • Ansible become guide • Ansible Cron Module Guide • Ansible firewalld & ufw Modules GuideCategory: installation