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
  roles:
    - webserver
  vars:
    sites:
      - example.com
    sites_nginx:
      - { src: "nginx.conf", dest: "nginx.conf" }
      - { src: "nginx-example.conf", dest: "sites-available/example.com" }

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
  args:
    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
  stat:
    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
  notify:
    - start nginx

- name: remove default sites-available and sites-enabled
  file: path=/etc/nginx//default state=absent
  with_items:
    - { 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/
        dest=/etc/nginx/sites-enabled/
        state=link
        owner=root
        group=root
        force=yes
  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/nginx.pid;

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;
  server_name example.com;

  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;
  server_name example.com;

  ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_prefer_server_ciphers on;
  ssl_dhparam /etc/ssl/certs/dhparam.pem;
  ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
  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;
}

Conclusion

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 →