Automated LetsEncrypt SSL installation with Ansible

Published Updated

9 min read

Learn how to get free Let's Encrypt SSL certificates for Nginx with Ansible and Certbot: one open-source role, any number of domains, HTTPS redirect and automatic renewal on Ubuntu 24.04.


The short version: install our open-source role, list your domains in a vars file, and run one playbook.

shell

1ansible-galaxy role install -r requirements.yml -p roles
2ansible-playbook -i hosts.yml site.yml

The role installs Nginx and Certbot, gets a certificate for every site over HTTP-01, switches the sites to HTTPS and leaves renewal to Certbot's systemd timer. Run it again and it changes nothing. Tested with ansible-core 2.16, 2.21 and 2.22 on Ubuntu 22.04, 24.04 and 26.04 against Pebble, the Let's Encrypt test server.

Below is the whole setup step by step, plus how to check the result and what to do about renewal.

Why automate Let's Encrypt with Ansible

Let's Encrypt gives you free domain-validated (DV) certificates that every browser trusts. There's no paperwork and no waiting for a certificate authority to answer an email. Your server proves it controls the domain over the ACME protocol, and the certificate is issued in seconds.

Why would you need SSL in the first place? If your WordPress site (or any other CMS or framework) has a login form, you don't want usernames and passwords flying over the Internet in plain text. Today it's simply the default: browsers mark plain HTTP sites as "Not secure".

Do you still need a paid certificate? Sometimes. OV and EV certificates put your verified company name into the certificate data and come with a warranty from the CA. But browsers don't show them any differently from a free DV certificate. Chrome moved the EV indicator out of the address bar in version 77, and Firefox did the same in 2019. Buy OV or EV only when a contract, an auditor or a client asks for it. For encryption itself, a Let's Encrypt certificate is just as strong.

So why Ansible? One certbot command on one server is easy. Ten servers, a few domains each, the same Nginx config every time, and a rebuild after an outage is a different story. You want that in code, in Git, and safe to run again.

There's one more reason. Let's Encrypt is moving to shorter certificates: 64 days from February 10, 2027, and 45 days from February 16, 2028. Manual renewal stops being an option.

What the role does

We published the role as ansible-role-letsencrypt-nginx under the MIT license. For every site you list, it:

  • installs Nginx and Certbot from the Ubuntu repositories;
  • creates a shared webroot for ACME challenges and an Nginx snippet that serves it;
  • adds an HTTP server block and requests the certificate with certbot certonly --webroot;
  • adds an HTTPS server block with TLS 1.2 and 1.3 and HTTP/2, and redirects HTTP to HTTPS;
  • installs a deploy hook that reloads Nginx after each renewal and enables certbot.timer.

It uses only ansible.builtin modules, so you don't need any collections.

Requirements

  • Control machine: ansible-core 2.20 or newer, the oldest release that still gets fixes. Check it with ansible --version. The ansible package in the Ubuntu 24.04 repositories ships ansible-core 2.16. The role works with it, but 2.16 reached end of life in July 2025 and gets no fixes, so install a current version with pipx: pipx install ansible-core. See the Ansible installation guide and the support matrix.
  • Target server: Ubuntu 24.04 LTS (22.04 and 26.04 work too), SSH access with your key, and a user that can use sudo. Python 3 is part of every Ubuntu Server install, so there's nothing to add for Ansible.
  • DNS: an A record (and AAAA, if you use IPv6) for every domain, pointing at the server.
  • Firewall: port 80 open to the whole Internet, because Let's Encrypt checks the HTTP-01 challenge on port 80. Port 443 for HTTPS.
  • Nginx: the role installs it if it's missing. If the server already has server blocks for the same domains, remove them or see "Use your own server blocks" below.

Set up and run the playbook

Step 1: Install the role

Create a project folder and a requirements.yml file in it:

yaml

1---
2roles:
3 - name: letsencrypt_nginx
4 src: https://github.com/itsyndicate/ansible-role-letsencrypt-nginx.git
5 scm: git
6 version: main

Install the role into the roles folder next to your playbook:

shell

1ansible-galaxy role install -r requirements.yml -p roles

Step 2: Create the inventory

Save this as hosts.yml and put your server's IP address in ansible_host:

yaml

1---
2web:
3 hosts:
4 web1:
5 ansible_host: 203.0.113.10
6 ansible_user: ubuntu
7 ansible_become: true
8 ansible_python_interpreter: /usr/bin/python3

Ansible connects as the ubuntu user and uses sudo for the changes. There's no need to log in as root.

Step 3: List your domains

Variables for the host go into a separate file, host_vars/web1.yml. The file name must match the host name from the inventory.

yaml

1---
2letsencrypt_email: admin@example.com
3letsencrypt_sites:
4 - domains:
5 - example.com
6 - www.example.com
7 - domains:
8 - shop.example.com
9 root: /var/www/shop
  • letsencrypt_sites - one item per site. Each site gets its own certificate and server block. The first name in domains is the certificate name.
  • root - the folder Nginx serves the site from. By default it's /var/www/ plus the first domain. The role puts a placeholder index.html there if the folder is empty.
  • letsencrypt_email - the contact email for your ACME account. Let's Encrypt stopped sending expiration reminders on June 4, 2025, so this address won't warn you about expiring certificates. You can leave it empty.

While you're testing, point the role at the Let's Encrypt staging environment so failed attempts don't hit the production rate limits:

yaml

1letsencrypt_acme_server: https://acme-staging-v02.api.letsencrypt.org/directory

Staging certificates aren't trusted by browsers. When everything works, remove this line, delete the staging certificates with sudo certbot delete --cert-name example.com and run the playbook again. Certbot won't replace a valid staging certificate on its own.

Step 4: Write the playbook

Save this as site.yml:

yaml

1---
2- name: Secure Nginx sites with Let's Encrypt
3 hosts: web
4 roles:
5 - letsencrypt_nginx

Your project now looks like this:

shell

1.
2├── host_vars
3│ └── web1.yml
4├── hosts.yml
5├── requirements.yml
6├── roles
7│ └── letsencrypt_nginx
8└── site.yml

Step 5: Run it

shell

1ansible-playbook -i hosts.yml site.yml

The first run installs packages, issues the certificates and switches the sites to HTTPS. In our test with two sites on a clean Ubuntu 24.04 server it ended like this (your numbers depend on what's already installed):

shell

1PLAY RECAP *********************************************************************
2web1 : ok=28 changed=16 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Run the same command again. Nothing should change:

shell

1PLAY RECAP *********************************************************************
2web1 : ok=24 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

That's the point of the role. You can run it from cron or CI as often as you like. To add a domain, add it to the list and run the playbook: Certbot expands the existing certificate.

Check the result

Plain HTTP should redirect to HTTPS:

shell

1curl -I http://example.com/

You should see HTTP/1.1 301 Moved Permanently and Location: https://example.com/. Then check HTTPS:

shell

1curl -I https://example.com/

The first line should be HTTP/2 200. To see every certificate on the server with its domains and expiry date:

shell

1sudo certbot certificates

The role writes one file per site to /etc/nginx/sites-available/. For the first site on Ubuntu 24.04 it looks like this:

nginx

1server {
2 listen 80;
3 listen [::]:80;
4 server_name example.com www.example.com;
5
6 include snippets/letsencrypt-acme.conf;
7
8 location / {
9 return 301 https://$host$request_uri;
10 }
11}
12
13server {
14 listen 443 ssl http2;
15 listen [::]:443 ssl http2;
16 server_name example.com www.example.com;
17
18 ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
19 ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
20 ssl_protocols TLSv1.2 TLSv1.3;
21
22 root /var/www/example.com;
23 index index.html;
24
25 location / {
26 try_files $uri $uri/ =404;
27 }
28}

The ACME challenge location stays on port 80, so renewals keep working after the redirect is in place. On Nginx 1.25.1 and newer (Ubuntu 26.04 ships 1.28) the role writes listen 443 ssl; plus http2 on; instead, because that's the current syntax there.

If you want to see how the same thing is done by hand, without Ansible, read our guide on how to install Let's Encrypt for Nginx on Ubuntu.

How renewal works

You don't need your own cron job. The Ubuntu certbot package comes with a systemd timer that runs certbot renew twice a day, and the role makes sure it's enabled. Certbot renews a certificate when it gets close to expiry.

Check the timer:

shell

1systemctl list-timers certbot.timer

Test renewal against the Let's Encrypt staging server without touching your real certificates:

shell

1sudo certbot renew --dry-run

It should end with Congratulations, all simulated renewals succeeded.

Nginx only picks up a new certificate after a reload. The role installs this deploy hook to /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh, and Certbot runs it after every successful renewal:

shell

1#!/bin/sh
2# Certbot runs this after every successful renewal.
3nginx -t -q && systemctl reload nginx

We checked it with a forced renewal: the serial number of the certificate Nginx served changed right after certbot renew finished.

One thing the role can't do for you is tell you when renewal breaks. Since Let's Encrypt no longer sends expiry emails, add expiry checks to your monitoring, or use one of the services Let's Encrypt lists.

Use your own server blocks

If your sites are more than static files (a reverse proxy to an app, PHP-FPM, custom headers), you'll probably want to keep your own Nginx configs. Set this in host_vars:

yaml

1letsencrypt_manage_vhosts: false

The role still installs Certbot, gets the certificates and sets up renewal, but doesn't write server blocks. Before the first run, add the challenge snippet to the port 80 server block of each domain:

nginx

1server {
2 listen 80;
3 server_name example.com www.example.com;
4 include snippets/letsencrypt-acme.conf;
5 location / {
6 return 301 https://$host$request_uri;
7 }
8}

Then point your HTTPS server block at /etc/letsencrypt/live/example.com/fullchain.pem and privkey.pem.

Other variables you may want: letsencrypt_remove_default_site: true removes the stock default site, letsencrypt_key_type: rsa switches from ECDSA to RSA keys for old clients, and letsencrypt_certbot_extra_args passes extra flags to Certbot. The full list is in the role's README.

Certbot or Ansible's own ACME module?

Ansible can also talk to Let's Encrypt directly with the community.crypto.acme_certificate module (module docs), without Certbot on the server. It's a good fit when certificates are created in one place and pushed to many servers or load balancers.

The catch is renewal. With acme_certificate, a certificate is renewed only when someone runs the playbook, so you need a scheduler that never misses. With Certbot, the server renews its own certificates even if nobody touches Ansible for months. For web servers that serve their own domains, that's the safer default, so this role uses Certbot.

Troubleshooting

  • The certificate request fails with a timeout or "unauthorized". Let's Encrypt couldn't fetch the challenge file. Check that the domain resolves to this server (dig +short example.com), that port 80 is open in the cloud firewall, and that no other server block answers for the same domain.
  • You hit a rate limit. Switch to the staging server (Step 3) until the playbook passes, then go back to production.
  • The browser shows an untrusted certificate. You're probably still on a staging certificate. Delete it with sudo certbot delete --cert-name example.com and run the playbook again without the staging URL.

Wrapping up

With the role in place, adding HTTPS to a new site is one more item in a list and one playbook run. The certificates renew themselves, and the playbook is safe to run any time.

For another example of the same approach, see our role that runs GitLab in Docker with Ansible.

Keep in mind that SSL encrypts traffic, but it won't protect your website from every attack. There are many other security practices you should go through to be sure your website and server are protected. We cover them in our article on how to secure your website and server properly.

If you'd rather not maintain playbooks and certificates yourself, our team can automate your infrastructure for you. Take a look at our DevOps automation and CI/CD services.

Alex Kondratiev

Alex Kondratiev

Co-founder and CEO of ITsyndicate. DevOps engineer with 15+ years in cloud infrastructure, Infrastructure as Code, Kubernetes and automation, focused on secure, scalable and resilient systems.

More about Alex

An Infrastructure Review Should Give You a Plan, Not a Dependency

Why we want the review to be useful even if your own team does all the work afterward. Of course we want to help with the implementation. We are an engineering company. We like solving infrastructure problems, and we want relationships where we can take responsibility for the work over time. So why offer an Infrastructure Review that a client can take back to their own team? Because I want the first engagement to earn its fee on its own. The client should leave with a better understanding of their situation, clear priorities, and a practical next step. They can then decide whether they want us to carry out the changes. That is the principle behind how we are building our Infrastructure Review. It shapes what we agree to investigate, how we explain our findings, and what the client can do with them afterward.

Your Infrastructure Can Be Expensive Even When Nothing Is Broken

A quiet production environment can still hide rising costs, wasted engineering capacity, and risks that appear only during growth or recovery. A familiar weekly update sounds like this: production is stable, the latest release went out, and there were no serious customer complaints. The cloud bill is higher again, but there is probably a reasonable explanation. One engineer spent half a day helping with the release, another fixed a monitoring issue, and the database may need attention next quarter. Nothing is broken badly enough to become a leadership problem. That is exactly why the underlying problem can survive for so long. I have seen this pattern more than once. Production looks stable, while senior engineers quietly spend much of their time keeping it that way. The platform works, but the company is paying for that stability through cloud spend, manual effort, slower releases, and assumptions about growth or recovery that nobody has tested recently. Infrastructure doesn’t need to cause an outage to become expensive. Outages are obvious. Inefficiency is quieter, and it can keep working for years while charging you for the privilege.

How to Run a Docker Container on Your Local Machine

This article is a quick, hands-on guide to running Docker locally. It walks through essential CLI commands for managing images and containers (build, pull, run, ps, start/stop, rm/rmi) and shows how to automate image creation with a Dockerfile. You’ll build an image, run a container with port mapping, verify it’s running, and learn tips for environment setup - everything you need to spin up and manage local containers efficiently.

Stay online.
Stay in command.