Skip to content
Commitpedia
Log in Join & edit

Ansible variables, templates and handlers: configure an nginx site

Use group variables, Jinja2 templates and handlers together: render an nginx server block from variables, test the config and reload nginx only when the file really changed.

Not checked by a trusted editor yet Last edited Outdated?

Edit history of Ansible variables, templates and handlers: configure an nginx site: 1 recorded edit
Edit history · 1 edit
Type
Script
Edits
1
Editors
1
First issued
15 Oct 2026
Last edit
15 Oct 2026
Language
yaml
Licence
CC0-1.0

What this changes

On every web host, as root: installs nginx, creates /var/www/hello with index.html, writes /etc/nginx/conf.d/hello.conf (site on port 8080), starts and enables nginx and reloads it when the config changes. Preview with --check --diff.

How to undo it

On each host: sudo rm /etc/nginx/conf.d/hello.conf && sudo systemctl reload nginx, then sudo rm -rf /var/www/hello. Also run sudo apt purge nginx if nginx was not there before.

Real playbooks avoid hard-coded values. You keep settings in variables, generate config files from templates, and restart or reload services through handlers so they are touched only when something changed. This example sets up a small site on port 8080 on Debian or Ubuntu hosts in an inventory group called web.

Project layout

project/
  inventory.ini
  hello.yml
  group_vars/
    web.yml
  templates/
    index.html.j2
    site.conf.j2

Variables: group_vars/web.yml

Files in group_vars/ named after an inventory group are loaded automatically for every host in that group. group_vars/all.yml would apply to all hosts.

---
site_name: hello
site_root: /var/www/hello
nginx_port: 8080
greeting: "Hello from Ansible"

Variables can also live in the play (vars:), in files loaded with vars_files:, in host_vars/HOSTNAME.yml, or on the command line with -e. When the same name is defined in several places, precedence decides; extra vars from -e always win. Keep each variable in one place and you will rarely need to think about it.

Templates: templates/*.j2

Templates use Jinja2. {{ name }} inserts a variable. Rendering happens on the control node, and the finished file is copied to the host. Facts gathered from the host, such as ansible_facts['hostname'], are available too.

<!doctype html>
<title>{{ site_name }}</title>
<h1>{{ greeting }}</h1>
<p>Served by {{ ansible_facts['hostname'] }} on port {{ nginx_port }}.</p>

That is templates/index.html.j2. Next, templates/site.conf.j2:

# Managed by Ansible. Local changes will be overwritten.
server {
    listen {{ nginx_port }};
    server_name {{ inventory_hostname }};
    root {{ site_root }};
    index index.html;
}

The playbook: hello.yml

---
- name: Configure the hello site
  hosts: web
  become: true
  tasks:
    - name: Install nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true
        cache_valid_time: 3600

    - name: Create the site folder
      ansible.builtin.file:
        path: "{{ site_root }}"
        state: directory
        owner: root
        group: root
        mode: "0755"

    - name: Render the home page
      ansible.builtin.template:
        src: templates/index.html.j2
        dest: "{{ site_root }}/index.html"
        owner: root
        group: root
        mode: "0644"

    - name: Render the nginx server block
      ansible.builtin.template:
        src: templates/site.conf.j2
        dest: "/etc/nginx/conf.d/{{ site_name }}.conf"
        owner: root
        group: root
        mode: "0644"
      notify: nginx config changed

    - name: Make sure nginx is running and starts at boot
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

  handlers:
    - name: Test nginx configuration
      ansible.builtin.command: nginx -t
      changed_when: false
      listen: nginx config changed

    - name: Reload nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded
      listen: nginx config changed

In YAML, a value that starts with {{ must be quoted, as in path: "{{ site_root }}". Without quotes YAML reads the braces as a dictionary and the playbook fails to parse.

How the handlers work

  • The server-block task has notify: nginx config changed. It notifies only when the template task reports changed, that is, when the rendered file differs from what is on the host.
  • Both handlers listen to that topic, so one notification triggers both. Handlers run in the order they are written in the handlers section: first nginx -t tests the configuration, then nginx reloads. If the test fails, the play stops for that host before the reload.
  • Notified handlers run once, after all tasks of the play, even if several tasks notified them. To run them earlier, add a task - ansible.builtin.meta: flush_handlers.
  • changed_when: false on the test task tells Ansible that nginx -t never changes anything, so it does not show up as a change.

Run it

ansible-playbook -i inventory.ini hello.yml --check --diff
ansible-playbook -i inventory.ini hello.yml
curl http://192.0.2.10:8080/

--diff shows the lines a template would change. Edit greeting and run again: only the home page task reports a change and no reload happens. Change nginx_port and the server block changes, so the handlers test and reload nginx. Remember to allow the new port in any firewall on the hosts.

Override a value for one run with an extra var, which beats every other source: ansible-playbook -i inventory.ini hello.yml -e nginx_port=8081.

Official documentation

See also

Repos in the wild

  • Ansible: agentless IT automation ansible/ansible The source repository of ansible-core, the engine behind Ansible playbooks, ad-hoc commands and the ansible-galaxy and ansible-vault tools. GitHub

History

Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.

  1. Commitpedia +4,824 chars

Report a problem

Something wrong, unsafe or copied? Tell the editors.
What is wrong?