Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
How to setup Let's Encrypt for Nginx on Ubuntu 16.04 (including IPv6, HTTP/2 and A+ SLL rating)

How to setup Let's Encrypt for Nginx on Ubuntu 16.04 (including IPv6, HTTP/2 and A+ SLL rating)

There are two main modes to run the Let's Encrypt client (called Certbot):

  • Standalone: replaces the webserver to respond to ACME challenges
  • Webroot: needs your webserver to serve challenges from a known folder.

Webroot is better because it doesn't need to replace Nginx (to bind to port 80).

In the following, we're setting up HTML is served from /var/www/mydomain, and challenges are served from /var/www/letsencrypt.

Nginx snippets

First we create two snippets (to avoid duplicating code in every virtual host configuration).

Create a file /etc/nginx/snippets/letsencrypt.conf containing:

location ^~ /.well-known/acme-challenge/ {
	default_type "text/plain";
	root /var/www/letsencrypt;

Create a file /etc/nginx/snippets/ssl.conf containing:

ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;

ssl_protocols TLSv1.2;
ssl_ecdh_curve secp384r1;
ssl_prefer_server_ciphers on;

ssl_stapling on;
ssl_stapling_verify on;

add_header Strict-Transport-Security "max-age=15768000; includeSubdomains; preload";
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;

Create the folder for the challenges:

sudo mkdir -p /var/www/letsencrypt/.well-known/acme-challenge

Nginx virtual hosts (HTTP-only)

We don't have a certificate yet at this point, so the domain will be served only as HTTP.

Create a file /etc/nginx/sites-available/mydomain.conf containing:

server {
	listen 80 default_server;
	listen [::]:80 default_server ipv6only=on;

	include /etc/nginx/snippets/letsencrypt.conf;

	root /var/www/mydomain;
	index index.html;
	location / {
		try_files $uri $uri/ =404;

Enable the site:

rm /etc/nginx/sites-enabled/default
ln -s /etc/nginx/sites-available/mydomain.conf /etc/nginx/sites-enabled/mydomain.conf

And reload Nginx:

sudo systemctl reload nginx


Install the package:

sudo apt-get install software-properties-common
sudo add-apt-repository ppa:certbot/certbot
sudo apt-get update
sudo apt-get install certbot

Note: there is also a letsencrypt package in APT, but it's a much older version of the client.

Get the certificate

Request the certificate (don't forget to replace with your own email address):

certbot certonly --webroot --agree-tos --no-eff-email --email YOUR@EMAIL.COM -w /var/www/letsencrypt -d -d

It will save the files in /etc/letsencrypt/live/

Note: The flag --no-eff-email opts out of signing up for the EFF mailing list, remove the flag if you'd like to signup.

Nginx virtual hosts (HTTPS-only)

Now that you have a certificate for the domain, switch to HTTPS by editing the file /etc/nginx/sites-available/mydomain.conf and replacing contents with:

## redirects to
server {
	listen 80;
	listen [::]:80;

	include /etc/nginx/snippets/letsencrypt.conf;

	location / {
		return 301$request_uri;

## redirects to
server {
	listen 80 default_server;
	listen [::]:80 default_server ipv6only=on;

	include /etc/nginx/snippets/letsencrypt.conf;

	location / {
		return 301$request_uri;

## redirects to
server {
	listen 443 ssl http2;
	listen [::]:443 ssl http2;

	ssl_certificate /etc/letsencrypt/live/;
	ssl_certificate_key /etc/letsencrypt/live/;
	ssl_trusted_certificate /etc/letsencrypt/live/;
	include /etc/nginx/snippets/ssl.conf;

	location / {
		return 301$request_uri;

## Serves
server {
	listen 443 ssl http2 default_server;
	listen [::]:443 ssl http2 default_server ipv6only=on;

	ssl_certificate /etc/letsencrypt/live/;
	ssl_certificate_key /etc/letsencrypt/live/;
	ssl_trusted_certificate /etc/letsencrypt/live/;
	include /etc/nginx/snippets/ssl.conf;

	root /var/www/mydomain;
	index index.html;
	location / {
		try_files $uri $uri/ =404;

Then reload Nginx:

sudo systemctl reload nginx

Note that redirects to (which redirects to because redirecting to directly would be incompatible with HSTS.

Automatic renewal using Cron

Certbot can renew all certificates that expire within 30 days, so let's make a cron for it. You can test it has the right config by launching a dry run:

certbot renew --dry-run

Create a file /root/

systemctl reload nginx

# If you have other services that use the certificates:
# systemctl restart mosquitto

Make it executable:

chmod +x /root/

Edit cron:

sudo crontab -e

And add the line:

20 3 * * * certbot renew --noninteractive --renew-hook /root/


Congratulations, you should now be able to see your website at πŸ™‚

You can now also test that your domain has A+ SLL rating:

I would also recommend setting up content-specific features like Content Security Policy and Subresource Integrity:

If Let's Encrypt is useful to you, consider donating to Let's Encrypt or donating to the EFF.

DavidOD commented Jan 19, 2017

awesome post. THANK YOU!

gerchen commented Jan 20, 2017


kakopappa commented Jan 24, 2017

Found a lazy way :)

git clone
cd letsencrypt
sudo ./letsencrypt-auto --nginx

Really thankful

MCeddy commented Mar 2, 2017

Thank you :)

boriscy commented Mar 21, 2017

Thanks it worked really well.

Great! Thx :) πŸ‘

rlam3 commented Mar 23, 2017

does this work for 14.04 too?
And what if I have cloudflare in front of nginx? How would this differ?


cecilemuller commented Jun 2, 2017

@rlam3 Sorry I only saw your comment now, Gists don't get notifications unfortunately. To answer your question, yes 14.04 is mostly the same except systemctl commands should be service commands instead, e.g. service nginx reload instead of systemctl reload nginx.

hshhhhh commented Jun 7, 2017

@cecilemuller thank you a lot!

thank you! this is perfect!

blbwd commented Jun 17, 2017

Shall I need to run this code separately for each of my domain?

certbot certonly --webroot --agree-tos --no-eff-email --email YOUR@EMAIL.COM -w /var/www/letsencrypt -d -d


cecilemuller commented Jun 18, 2017

@blbwd You can either do run the command once per domain with a single -d, or a single command with multiple -d (maximum 100 domains per certificate): both ways are fine.

The main difference is how it counts requests in regards to the API rate limits:

  • with multiple -d, it creates a single certificate for all domains specified, so it counts as only one request

  • if you ran the command separately for each domain, it creates a separated certificate for each domain, and each domain counts as one request.

blbwd commented Jun 19, 2017

It works. I am running the script to get certificate separately for each domain. Now when I set automatic renewal, will single cron will renew all of my certificates which I have generated separately for each domain?


cecilemuller commented Jun 19, 2017

@blbwd Yes, a single certbot renew --noninteractive command will attempt to renew all certificates that are near expiration, even if they were generated separately for each domain (because Certbot keeps track of the domains it generated certificates for, so it knows the list).

You can check it by running certbot renew --dry-run (which merely pretends to renew), it should mention all the certificates.

blbwd commented Jun 20, 2017

Thanks a lot. It is working. You have a such great technical and helpful person I had ever known.


cecilemuller commented Jun 20, 2017

Thanks πŸ˜ƒ

RobinCsl commented Jun 28, 2017

Thank you very much. Really helpful!!!
I would like to add that if you are loading index.php by default, leave the lines concerning the location of how the server needs to interpret php files.

dinofizz commented Jul 5, 2017

This is the most concise and helpful write-up I have found! Thank you for taking the time to publish this.

It just works! Thank you!

rajeevkannav commented Jul 25, 2017

Hello, Thanks for this awesome thing.

Just one Question Why we are not writing server_name;
like server_name; single Line ?? this will save two code blocks and iterations

Thanks in advance

Save my time.

astr0naugh7 commented Aug 6, 2017

This worked great for me. Now I want to add another domain onto the same server using it's own SSL cert. How can I edit this to allow for multiple domains/subdirectories? With this current setup, I can't get to the IP as it just 301s to the domain I first setup with this. Is it possible to do that? I would really like to have multiple domains on this one server.

Thanks again.

Awesome, thanks a lot!

crw commented Aug 17, 2017

Came to say, thanks so much! This was extremely helpful.

@astr0naugh7: try this, not sure if it works yet:

location / {
    return 301 https://$server_name$request_uri;

Regardless, plumbing the nginx docs might help.

crw commented Aug 17, 2017

Oh also, @cecilemuller: why no

ssl_dhparam /etc/nginx/ssl/dhparam.pem;

I spend like 30 minutes a server generating the damn dhparam.pem file; is it not necessary to do so?

I tried this many times, and it's not working out. How can I debug what's the issue?

+1 pankaj884 I tried this many times, and it's not working out. How can I debug what's the issue?
I keep getting:
urn:acme:error:unauthorized :: The client lacks sufficient authorization :: Invalid response from

frenchbread commented Aug 29, 2017

Thanks @cecilemuller. This helped a lot!

In addition, how would you add to this NGINX config an instance that works on port :8000 "locally" on a server and should be reverce-proxied to public :8000 port. Including same redirections as with :80 ?

nadj commented Sep 8, 2017

Hi @cecilemuller, thank you for a great tutorial.
Unfortunately I need some help, something really strange is happening on my server.
After the config file is set as you shown I have https enabled, but I'm unable to renew the certificate.
The error from sudo certbot renew --dry-run

Invalid response from

Tried to put a file to /var/www/letscencrypt/.wellknown/acme-challenge/test
And I can't access it from I get redirected to https and my site is showing a 404 error

However I can access the test file from and it shows me the content.

I commented out everything except the ## Serves code block and it works as described above. I'm refreshing the server with sudo service nginx reload
I can't figure out whats happening here

nginx version is 1.10.3, server is Ubuntu 16.04.3 LTS

Can you give me some advice?
Thanks in advance

Yes indeed, thanks!

I tried the test you recommended and it gave an "A" rating. I haven't tried the "content-specific features like Content Security Policy and Subresource Integrity". Here's my .conf so far:

ummm i am having a mess with certbot installations on my server. there is the certbot 0.17.0 executable, the 0.18.2 executable and some python packages. think i should clean up and install it properly from apt-get. which one ppa source are you recommending for ubuntu 17.10?

yifeikong commented Oct 8, 2017

By default, 'renew' will reuse the options used to create obtain or most recently successfully renew each certificate lineage.

I wish I could create a cert via --standalone and then renew it using --wwwroot.

blbwd commented Oct 8, 2017

I installed SSL on my 4 domains on the same day but suddenly certificates of 2 domains expired. I already run "certbot renew --dry-run" but in vain. For those 2 domains it is showing that certificate has been expired 21 days ago. Please suggest.


cecilemuller commented Oct 8, 2017

Sorry @blbwd, better ask at the Certbot repository or on Stackoverflow instead.

The best advice I could give is looking at the output of the dry run command to see if it gives an error (like if it's able to connect), or even run certbot renew to see if it has an error only when it tries to truly renew the certificate (instead of a mere test run).

There should also be some log files in /var/log/letsencrypt you could look at when looking for errors, but they're very verbose.

And if it does renew but doesn't show up in the browser, restart the web server service (although reloading should be enough) and clear your browser cache.


cecilemuller commented Oct 8, 2017

@binarykitchen Sorry I haven't tried 17, I'm only using the LTS versions

blbwd commented Oct 9, 2017

The dry run command did not show any error and all the renew process was done successfully. Tanks a lot for your suggestion to restart server. All the 2 certificates started working as soon as I restarted my nginx server.


cecilemuller commented Oct 9, 2017

I'm glad it worked πŸ˜ƒ

nemchik commented Oct 30, 2017

Is there any reason why X-Frame-Options could not be SAMEORIGIN? Making that change doesn't seem to affect my score on SSLTest.


cecilemuller commented Nov 2, 2017

@nemchik No reason against it, SAMEORIGIN is fine too if the site needs iframes.

mobambi commented Nov 14, 2017

I followed this guide and it works well. Thanks
But after this, when I'm trying to access phpmyadmin page, it returns 403 Forbidden nginx/1.10.3 (Ubuntu)...
Can anyone help me with this problem?

ronnyandre commented Nov 16, 2017

Same issue here @mobambi

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment