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 roles2ansible-playbook -i hosts.yml site.ymlThe 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. Theansiblepackage 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_nginx4 src: https://github.com/itsyndicate/ansible-role-letsencrypt-nginx.git5 scm: git6 version: mainInstall the role into the roles folder next to your playbook:
shell
1ansible-galaxy role install -r requirements.yml -p rolesStep 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.106 ansible_user: ubuntu7 ansible_become: true8 ansible_python_interpreter: /usr/bin/python3Ansible 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.com3letsencrypt_sites:4 - domains:5 - example.com6 - www.example.com7 - domains:8 - shop.example.com9 root: /var/www/shopletsencrypt_sites- one item per site. Each site gets its own certificate and server block. The first name indomainsis 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 placeholderindex.htmlthere 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/directoryStaging 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 Encrypt3 hosts: web4 roles:5 - letsencrypt_nginxYour project now looks like this:
shell
1.2├── host_vars3│ └── web1.yml4├── hosts.yml5├── requirements.yml6├── roles7│ └── letsencrypt_nginx8└── site.ymlStep 5: Run it
shell
1ansible-playbook -i hosts.yml site.ymlThe 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=0Run the same command again. Nothing should change:
shell
1PLAY RECAP *********************************************************************2web1 : ok=24 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0That'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 certificatesThe 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.timerTest renewal against the Let's Encrypt staging server without touching your real certificates:
shell
1sudo certbot renew --dry-runIt 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/sh2# Certbot runs this after every successful renewal.3nginx -t -q && systemctl reload nginxWe 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: falseThe 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.comand 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.
