NGINX with Certbot SSL on Ubuntu 24.04 LTS | cloudimg
Overview
This cloudimg image ships NGINX 1.30 mainline on Ubuntu 24.04 LTS together with Certbot and the python3-certbot-nginx plugin pre-installed. A Mozilla Intermediate TLS snippet and a self-signed fallback certificate are staged in the image so https:// answers from first boot. Point a DNS A record at the VM, run one Certbot command, and you have a browser-trusted Let's Encrypt certificate with automatic renewal already scheduled — no OpenSSL wrangling, no cron edits, no ACME scaffolding to hand-write.
Prerequisites
-
An Azure subscription with permission to create virtual machines
-
An SSH key pair — the public key is uploaded during VM creation
-
A domain name you can edit DNS for — you will create an A record pointing at the VM's public IP before running Certbot
-
An email address for the Let's Encrypt account registration (also receives expiry warnings if renewal ever fails)
Step 1 — Deploy from the Azure Portal
-
Find NGINX SSL (Certbot) on Ubuntu 24.04 LTS by cloudimg in the Azure Marketplace and click Create.
-
On the Basics tab, select or create a Resource Group, choose East US as the region, and set the VM size to Standard_B2s (2 vCPU / 4 GB RAM) for a lightweight reverse proxy, or Standard_D2s_v3 (2 vCPU / 8 GB RAM) and upward for production TLS-terminating workloads.
-
Under Administrator account, choose SSH public key and paste your public key.
-
On the Networking tab, open inbound ports 80 (HTTP + ACME HTTP-01 challenge) and 443 (HTTPS) in addition to 22 (SSH). Port 80 is required — Let's Encrypt's HTTP-01 validation connects to it to confirm you control the domain.
-
Click Review + create, then Create.
Step 2 — Deploy with the Azure CLI
az vm create \
--resource-group my-rg \
--name nginx-ssl \
--image "$(az sig image-version list \
--resource-group AZURE-CLOUDIMG \
--gallery-name cloudimgGallery \
--gallery-image-definition nginx-ssl-certbot-ubuntu-24-04 \
--query '[0].id' -o tsv)" \
--size Standard_D2s_v3 \
--admin-username azureuser \
--ssh-key-values ~/.ssh/id_rsa.pub \
--public-ip-sku Standard
After the VM starts, open HTTP and HTTPS:
az vm open-port --port 80 --resource-group my-rg --name nginx-ssl --priority 1010
az vm open-port --port 443 --resource-group my-rg --name nginx-ssl --priority 1011
Step 3 — Point Your DNS at the VM
Find the VM's public IP:
az vm show --resource-group my-rg --name nginx-ssl --show-details --query publicIps -o tsv
In your DNS provider's control panel, create an A record for the hostname you want to use — for example nginx-ssl.example.com — and set its value to the public IP from the previous command. Wait for propagation before running Certbot:
dig +short nginx-ssl.example.com
The command should return the VM's public IP. If it returns nothing, wait a few minutes for DNS to propagate before continuing.
Step 4 — Connect via SSH
ssh azureuser@nginx-ssl.example.com
On first boot, nginx.service and certbot.timer are both enabled. The timer is a no-op until a certificate is actually issued, so there is nothing to do on first login other than issue a cert.
Step 5 — Verify the Image is Ready
Check that NGINX is running and listening on both ports:
sudo systemctl status nginx --no-pager
sudo ss -tln | grep -E ':80|:443'
Confirm the Certbot version and the NGINX plugin are importable:
certbot --version
python3 -c 'import certbot_nginx; print("certbot_nginx plugin OK")'
Confirm that certbot.timer is enabled — this is the systemd timer that will auto-renew your certificate twice daily once you issue one:
systemctl list-timers --all | grep certbot
Fetch the fallback HTTPS page — the image ships with a self-signed certificate so https:// answers from first boot, before you have run Certbot:
curl -skI https://127.0.0.1/ | head -3
Step 6 — Issue a Let's Encrypt Certificate
This is the one command that turns the self-signed fallback into a browser-trusted Let's Encrypt certificate. Substitute your domain and email address:
sudo certbot --nginx \
-d nginx-ssl.example.com \
-m admin@example.com \
--agree-tos \
--redirect \
--non-interactive
Certbot will:
-
Request a certificate from Let's Encrypt using the HTTP-01 challenge (which is why port 80 must be reachable from the Internet)
-
Install the issued certificate and private key under
/etc/letsencrypt/live/nginx-ssl.example.com/ -
Rewrite
/etc/nginx/conf.d/default.confso:443uses the new certificate and:80redirects tohttps:// -
Reload NGINX
Expected output ends with a Successfully received certificate line and the path to the issued certificate. If this step fails, jump to Step 10 — Troubleshooting before retrying — Let's Encrypt rate-limits failed requests.
Step 7 — Verify HTTPS Redirect and New Certificate
Confirm the redirect now returns 301 to https://:
curl -sI http://nginx-ssl.example.com/ | head -3
Confirm the HTTPS response and inspect the issuer chain — it should now read Let's Encrypt rather than nginx-cloudimg-fallback:
curl -sI https://nginx-ssl.example.com/ | head -3
echo | openssl s_client -servername nginx-ssl.example.com -connect nginx-ssl.example.com:443 2>/dev/null | openssl x509 -noout -issuer -subject -dates
Step 8 — Confirm Auto-Renewal is Scheduled
certbot.timer was enabled when the image was built, so your newly-issued certificate is already registered for automatic renewal. Confirm the next fire time:
systemctl list-timers certbot.timer
Run a dry-run to prove the renewal flow works end-to-end for this specific certificate:
sudo certbot renew --dry-run
The output should include Congratulations, all simulated renewals succeeded — that confirms Certbot can reach Let's Encrypt and NGINX is reloaded correctly at renewal time. No further action is required; Let's Encrypt certificates last 90 days and Certbot will renew 30 days before expiry.
Step 9 — Publish Your Own Content
The active document root is /var/www/html. Replace the welcome page with your own HTML:
sudo tee /var/www/html/index.html > /dev/null <<'HTML'
<!doctype html>
<html><head><meta charset="utf-8"><title>My Site</title></head>
<body><h1>Hello from NGINX over HTTPS.</h1></body></html>
HTML
Reload NGINX to pick up the change:
sudo nginx -t && sudo systemctl reload nginx
Fetch the new page over HTTPS:
curl -s https://nginx-ssl.example.com/ | head -3
For reverse-proxy or load-balancer configurations, add a server block under /etc/nginx/conf.d/ and include the shared TLS params:
include /etc/nginx/snippets/ssl-params.conf;
Step 10 — Troubleshooting
Certbot fails with Connection refused on port 80. The HTTP-01 challenge needs your VM reachable on port 80 from the public Internet. Confirm the Azure NSG allows inbound 80/tcp and that nginx.service is listening:
sudo ss -tln | grep ':80 '
Certbot fails with DNS problem: NXDOMAIN. The A record has not propagated, or it points to the wrong IP. Re-run:
dig +short nginx-ssl.example.com
It must return the VM's public IP. Wait a few minutes and retry.
Rate-limited by Let's Encrypt. Production issuance is limited to 50 certificates per registered domain per week. While you are iterating, use the staging environment:
sudo certbot --nginx -d nginx-ssl.example.com --staging --agree-tos -m admin@example.com --non-interactive
Staging certificates are not browser-trusted (the issuer chain shows (STAGING) Let's Encrypt), but they exercise the full issuance flow without touching the production rate limit. Remove them with sudo certbot delete --cert-name nginx-ssl.example.com before issuing a real one.
Renewal fails silently weeks later. Check the Certbot log:
sudo tail -100 /var/log/letsencrypt/letsencrypt.log
Step 11 — Everyday Operations
Reload the configuration without dropping connections:
sudo nginx -t && sudo systemctl reload nginx
Check error and access logs:
sudo tail -50 /var/log/nginx/error.log
sudo tail -50 /var/log/nginx/access.log
Stop, start, or restart the service:
sudo systemctl restart nginx
List all certificates managed by Certbot on this VM:
sudo certbot certificates
Support
cloudimg provides 24/7/365 expert technical support. Guaranteed response within 24 hours, one hour average for critical issues. Contact support@cloudimg.co.uk.