Unlike Apache and Nginx, Let’s Encrypt has no way of autoconfiguring your Node.js app, as it can work in arbitrary ways, while the former two usually follow a predefined (and machine readable) configuration.
How to configure a Node.js Express server to handle Let’s Encrypt HTTP authorization then?
As usual, there are several use cases, depending on your current configuration.
Let’s Encrypt with Node.js
To start an HTTPS server, you’ll need a certificate and the private key. Read them from the filesystem, and fire up the server. For example:
This example uses the
mz/fs library, which provides Promised filesystem methods.
Depending on your requirements, you might want a graceful shutdown handler. As the above server reads the certificate on startup, it won’t use a new one unless restarted. To make sure the connections are not terminated abruptly, you can use a library:
Note that it will not automagically handle websockets and long-running connections. If you use any of them, you need to implement some sort of auto-retrying in the client applications.
We write articles like this regularly. Join our mailing list and let's keep in touch.
Scenario #1: HTTP is not used at all
If your app does not use HTTP (port 80), which might be the case for API-only endpoints, it is straightforward to configure Let’s Encrypt. If you use the above example with the certificates and the graceful shutdown, you are already set up Node-wise.
For certbot, use standalone authorization to get the initial certificate, then start the app:
To renew, use the
--deploy-hook to restart the app:
Scenario #2: Shut down the app during renewal
If your app uses both HTTP and HTTPS and you don’t want to modify it in any way to support Let’s Encrypt, then just shut it down during certificate renewal.
The downside is a few seconds of downtime roughly every 60 days. If the clients use some kind of auto-retrying, they might not even notice.
To get the initial certificate, use the same script as in the previous case:
The only difference is that during renewal stop the app in the
--pre-hook and start it again with the
Scenario #3: HTTP redirect with a public folder
For most of the apps, this is the most likely scenario. When you serve all your content on HTTPS, but still use HTTP just to redirect the visitors. This is the preferred way for public-facing websites, as browsers still request the HTTP site first. If there were no redirect, users would be welcomed with an error page.
Also, in most cases, there is a public folder that is served as-is. This is usually where the assets, like the CSS, JS, HTML, and image files are.
This setup allows Let’s Encrypt to use that public library for the HTTP auth. During that, it places a file into the directory, and a remote auth server tries to fetch it via HTTP, gets redirected to HTTPS, and the auth will be successful.
Node-wise, you need an HTTP server that does the redirect:
If you use graceful shutdown, don’t forget to use that for the HTTP server too:
Since Let’s Encrypt requires your app to be running even for the first auth, you need to handle the case when no certificate is present.
A simple solution uses the pem library to generate a self-signed certificate if none is found:
The certbot configuration uses webroot auth, and you need to specify the path of the public folder:
--deploy-hook restarts the app when a new certificate is available.
The renew works the same, so you don’t even need to specify any parameters:
Scenario #4: HTTP redirect with no public folder
If you use HTTP and you don’t have a dedicated static files folder, then you need to configure a path for the Let’s Encrypt auth challenge. This basically means some kind of public folder, but with a limited scope. This enables Let’s Encrypt to authorize, but don’t expose anything else.
To serve a folder under the
/.well-known/acme-challenge path, use:
The HTTP server config that does the redirect as well as the challenge is like:
Since you still use the webroot auth, you need to handle the no-cert case, the same as above.
The certbot configs are also the same as above:
Scenario #5: You do not want server restarts
All the above cases are based on automatic renewals that also restart the server. In some cases this might not be acceptable, as they happen albeit rarely, still somewhat randomly. If the restarts take a long time, that could cause problems even though they only run roughly every other month.
One solution is to schedule the renewals to a part of the day when a restart is acceptable.
Another solution is to forget renewals altogether and periodically restart the server in a maintenance window. The restart then updates the certificates before starting the app.
In this case, you don’t need to worry about the authorization process and the graceful shutdown. In fact, you don’t need any changes to your server apart from using the current Let’s Encrypt certificate.
Just use the standalone script from Scenario #1, and you are all set up:
certbot certonly --standalone --keep-until-expiring --agree-tos -d $CERT_URL ...other_params && ...start_app
--keep-until-expiring flag instructs certbot to keep the certificate if it is not near expiry. Usually, this is the right choice, as it prevents multiple restarts to use up your quota.
But if you want to make sure that after every restart you have a new certificate, use
--force-renewal instead. I don’t recommend it though, as you might easily find yourself rate-limited.