Setting Up Nginx with Ansible

by Hugh Bien — 06/12/2017

During most of my career, I almost never setup a server from scratch. I'm usually building on top of others' Ansible playbooks. I've always felt a gaping hole in knowledge in terms of sysadmin and ops tasks.

So two years ago, I picked up Servers for Hackers which took a terrific bottom up approach on how to setup and run a server. I've been using it as a reference to setup servers for my side projects, I highly recommend it.

Here's what I used to get Nginx up and running, with SSL, for both static sites and applications needing a reverse proxy. I'm using Ubuntu 16 LTS.

Site Configuration

I keep these tasks under a webserver role.

For the site.yml file, I keep a list of sites for each host group under the sites and sites_nginx vars. The latter maps Nginx templates to their destination.

- hosts: example
    - webserver
      - { src: "nginx.conf", dest: "nginx.conf" }
      - { src: "nginx-example.conf", dest: "sites-available/" }

Let's Encrypt SSL Tasks

Let's Encrypt is amazing! I can get a free SSL certificate, automate its installation, and even automate renewal.

In roles/webserver/tasks/ssl.yml:

- name: install apt-transport-https
  apt: pkg=apt-transport-https state=present update_cache=true

- name: generate dhparams file
  command: openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096
    creates: /etc/ssl/certs/dhparam.pem

- name: install certbot
  apt: pkg=certbot state=present update_cache=true

- name: install python3-certbot-nginx
  apt: pkg=python3-certbot-nginx state=present update_cache=true

- name: check if certificate exists
    path: /etc/letsencrypt/live//cert.pem
  register: letsencrypt_cert
  with_items: ""

- name: stop nginx for certbot
  service: name=nginx state=stopped
  with_items: ""
  when: item.stat.exists == False

- name: generate new certificate
  shell: "certbot certonly --standalone --noninteractive --agree-tos --rsa-key-size 4096 --email YOUR_EMAIL_ADDRESS@EXAMPLE.COM -d "
  with_items: ""
  when: item.stat.exists == False

- name: start nginx for certbot
  service: name=nginx state=started
  with_items: ""
  when: item.stat.exists == False

- name: add crontab to renew certificates
  cron: name="renew ssl" minute="30" hour="8" weekday="0" job="certbot renew --pre-hook \"service nginx stop\" --post-hook \"service nginx start\" >> /var/log/le-renew.log"

This installs certbot and its prerequisites, generates the private key and SSL cert, and adds a cronjob to renew the certificate. Thank you, Let's Encrypt!

Using a 4096 key size will take longer to generate, but is a good trade-off for security.

Nginx Handler and Tasks

Make sure Nginx is started with roles/webserver/handlers/nginx.yml:

- name: start nginx
  service: name=nginx state=started

Then for roles/webserver/tasks/nginx.yml:

- name: install nginx
  apt: pkg=nginx state=present update_cache=true
    - start nginx

- name: remove default sites-available and sites-enabled
  file: path=/etc/nginx//default state=absent
    - { directory: 'sites-available' }
    - { directory: 'sites-enabled' }

- name: setup nginx configurations
  copy: src=../files/ dest=/etc/nginx/ owner=root group=root mode=644
  with_items: ""

- name: setup sites-enabled
  file: src=/etc/nginx/sites-available/
  with_items: ""

This will install Nginx and copy over the configuration file for each site. It follows the standard pattern of keeping configurations in sites-available directory and linking them from sites-enabled. I keep one main nginx.conf along with a different configuration for each site.

Nginx Conf

For the standard roles/webserver/files/nginx.conf:

user www-data;
worker_processes 1; # usually one per core
pid /run/;

events {
  worker_connections 1024;

http {
  # Defaults
  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;
  client_body_timeout 12;
  client_header_timeout 12;
  keepalive_timeout 15;
  send_timeout 10;
  types_hash_max_size 2048;
  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;

  # Compression
  gzip on;
  gzip_comp_level  2;
  gzip_min_length  1000;
  gzip_proxied     expired no-cache no-store private auth;
  gzip_types       text/plain application/x-javascript text/xml text/css application/xml;

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

For individual sites, the configuration should live at roles/webserver/files/nginx-example.conf. A server block should do a permanent 301 redirect to SSL (handling non-SSL connections):

server {
  listen 80 default_server;
  listen [::]:80 default_server;

  root /usr/share/nginx/html;
  index index.html;

  location ~ /.well-known {
    allow all;

  location / {
    return 301 https://$host$request_uri;

The next server block should handle SSL connections. I've added some lines to handle trailing slashes, caching asset files (I usually fingerprint these), and handling 405/500 errors:

server {
  listen 443 ssl;
  listen [::]:443 ssl;

  ssl_certificate /etc/letsencrypt/live/;
  ssl_certificate_key /etc/letsencrypt/live/;

  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_prefer_server_ciphers on;
  ssl_dhparam /etc/ssl/certs/dhparam.pem;
  ssl_session_timeout 1d;
  ssl_session_cache shared:SSL:50m;
  ssl_stapling on;
  ssl_stapling_verify on;

  # to remove trailing slash
  rewrite ^(/.*)\.html(\?.*)?$ $1$2 permanent;
  rewrite ^/([^.]*)/$ /$1 permanent;

  root /home/deploy/example;
  index index.html;

  location ~* \.(jpg|jpeg|png|gif|ico|css|js|eot|ttf|woff|woff2)$ {
    expires max;
    try_files $uri =404;

  location ~ ^/([^.]*)/?$ {
    expires 1h;
    try_files $uri $uri.html $uri/index.html =404;

  error_page 404 =404 /404;
  error_page 500 502 503 504 =500 /500;

The actual directory holding your HTML files and web assets is at /home/deploy/example here. Enabling these SSL Ciphers should get you 100% at SSL Labs.

For reverse proxies, use the proxy_pass directive. In the case below, I'm mapping routes that start with /api to port 8000:

location ~ ^/api/.*$ {
  proxy_pass http://localhost:8000;


I hope that's helpful! I keep a single repository with my deploy playbooks. When I need to add a new site, I simply add a new nginx-*.conf file and update the sites and sites_nginx vars.


Follow me via , RSS feed, or Twitter.

You may also enjoy:
Half Dome · Raise and Rescue · A History of Computing · All Articles →