The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Put NGINX on the only public edge, terminate TLS there, and keep your Node.js container on a private Docker Compose network. Let’s Encrypt supplies the certificate, while a persistent certificate volume and a scheduled renewal command keep HTTPS working after container recreation. The reliable sequence is: point DNS at the host, publish only ports 80 and 443 from NGINX, serve the ACME challenge from a shared webroot, issue the certificate, enable the TLS virtual server, and reload NGINX after every successful renewal.
Antihost offers VPS plans with one-click Docker, NVMe storage and a 14-day free trial.
What the finished deployment looks like
Internet clients connect to NGINX on ports 80 and 443. NGINX answers the HTTP-01 challenge on port 80, redirects ordinary HTTP requests to HTTPS, and terminates TLS on port 443. It then forwards the request over the private Compose network to the Node.js service, normally on port 3000. The Node.js container does not need a public port mapping.
As an Amazon Associate I earn from qualifying purchases.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
This arrangement gives each component one responsibility:
- NGINX: public listener, certificate handling, HTTP-to-HTTPS redirect, and reverse proxy.
- Let’s Encrypt and Certbot: certificate issuance and renewal.
- Node.js: application responses on an internal HTTP connection.
- Docker Compose: service discovery, network isolation, and persistent volume mounts.
NGINX’s HTTPS server requires both an ssl_certificate and an ssl_certificate_key; its upstream is selected with proxy_pass. Keeping those paths and the ACME state outside ephemeral containers is essential.
#1 Best Overall
Prerequisites and DNS checks
Complete these checks before starting a production certificate request:
- A registered domain name, such as
example.com, with anArecord pointing to the Docker host’s IPv4 address. - An
AAAArecord only when the host accepts IPv6 traffic correctly. A broken IPv6 route can make validation fail even when IPv4 works. - Inbound TCP ports 80 and 443 allowed by the cloud firewall, host firewall, and any upstream security group.
- A Node.js image or Dockerfile that listens on the Compose service port, for example
3000, and binds to0.0.0.0inside the container. - A directory on the host for the ACME webroot and another for certificates, both available to the relevant containers.
Use a real hostname in every command below. Replace example.com and [email protected] with your own values; do not request a certificate for an internal-only name.
Recommended Free Tools
Create the Compose services and private network
The following Compose file publishes only NGINX. The application is reachable by the service name app from the shared network, while Certbot shares the challenge and certificate volumes.
services:
app:
build: .
expose:
- '3000'
networks:
- private
nginx:
image: nginx:stable
depends_on:
- app
ports:
- '80:80'
- '443:443'
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./certbot/www:/var/www/certbot:ro
- ./letsencrypt:/etc/letsencrypt:ro
networks:
- private
certbot:
image: certbot/certbot
volumes:
- ./certbot/www:/var/www/certbot
- ./letsencrypt:/etc/letsencrypt
networks:
- private
networks:
private:
expose documents the application port to other Compose services without publishing it on the host. The letsencrypt directory must be writable by Certbot and readable by NGINX. Keep the webroot mount read-only in NGINX; Certbot alone needs to create challenge files.
Bootstrap NGINX before a certificate exists
NGINX cannot start an HTTPS server that references certificate files which do not yet exist. Start with an HTTP-only configuration that serves the ACME directory and redirects normal traffic. This configuration is safe to run before issuance:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
default_type text/plain;
try_files $uri =404;
}
location / {
return 301 https://$host$request_uri;
}
}
The ^~ location keeps the challenge path from falling through to the Node.js application. The redirect applies only after the challenge location has matched, so Let’s Encrypt can retrieve its token over plain HTTP.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →Start NGINX and verify the configuration:
docker compose up -d app nginx
docker compose exec nginx nginx -t
Create a test file and fetch it through the public hostname before requesting a certificate:
Rank #2
mkdir -p certbot/www/.well-known/acme-challenge
echo ok > certbot/www/.well-known/acme-challenge/ping
curl -i http://example.com/.well-known/acme-challenge/ping
A successful response should be HTTP 200 with the body ok. If you receive a redirect loop, a 404, or an application response, fix NGINX routing, DNS, or the firewall first.
Issue the first Let’s Encrypt certificate with Certbot
Webroot method in Compose
With port 80 reachable and the challenge test passing, run Certbot against the shared webroot:
docker compose run --rm certbot certonly
--webroot --webroot-path=/var/www/certbot
--email [email protected]
--agree-tos --no-eff-email
-d example.com -d www.example.com
Certbot writes the live certificate under /etc/letsencrypt/live/example.com/ inside the container, which maps to ./letsencrypt/live/example.com/ on the host. The full chain is in fullchain.pem; the private key is in privkey.pem. Do not copy the private key into the application image or commit it to source control.
Host-installed Certbot
Certbot’s NGINX integration can obtain a certificate and edit an NGINX configuration automatically with:
sudo certbot --nginx
That command is intended for an NGINX installation managed by the host. In a Compose deployment, it will not modify the configuration inside the NGINX container unless you deliberately mount and manage those files from the host. Choose one owner for certificates and renewal; mixing host-managed and container-managed state commonly creates mismatched paths and reload failures.
Enable HTTPS and proxy to Node.js
After issuance succeeds, replace the bootstrap file with the complete server configuration. Keep the challenge location in the port-80 server so future HTTP-01 renewals continue to work.
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
default_type text/plain;
try_files $uri =404;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://app:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
}
Check and reload NGINX without stopping active connections:
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Scan for outdated or missing drivers - takes under a minute3Clear out junk files and repair common Windows errorsRank #3
docker compose exec nginx nginx -t
docker compose exec nginx nginx -s reload
Now test both the redirect and the certificate:
curl -I http://example.com/
curl -I https://example.com/
The first request should return a 301 with an HTTPS Location. The second should complete a trusted TLS handshake and return the Node.js response.
Make the Node.js application proxy-aware
The application receives an internal HTTP request even though the browser used HTTPS. Frameworks that generate absolute URLs, secure cookies, redirects, or rate-limit keys must therefore trust the proxy correctly. For Express, the setting is commonly:
app.set('trust proxy', 1);
Only enable proxy trust when requests can reach the application through your controlled NGINX network. Do not publish the app port directly, or an attacker could send forged forwarding headers without passing through NGINX.
Use the forwarded headers shown in the NGINX configuration to preserve the original host, client address, and HTTPS scheme. WebSocket applications may additionally need an upgrade-aware location and headers; add those only for routes that actually use WebSockets.
Choose HTTP-01 or DNS-01 validation
HTTP-01 is the simplest choice when the public hostname can reach port 80. DNS-01 is the fallback when inbound HTTP is unavailable and the required method for wildcard certificates, but it introduces DNS-provider API credentials.
| Property | HTTP-01 | DNS-01 |
|---|---|---|
| Validation path | Let’s Encrypt fetches a token from http://your-host/.well-known/acme-challenge/. |
Let’s Encrypt checks a temporary DNS TXT record. |
| Inbound requirement | Public port 80 must reach NGINX. | No inbound HTTP requirement, but authoritative DNS must be updated. |
| Wildcard certificates | Not supported. | Supported, including names such as *.example.com. |
| Credentials | No DNS API credential is needed. | Certbot or the ACME companion needs DNS-provider API access. |
| Main failure mode | Wrong DNS, blocked port 80, IPv6 misrouting, or a challenge path intercepted by the app. | Incorrect provider plugin, insufficient DNS permissions, stale public DNS, or an incorrectly delegated zone. |
For DNS-01, store the provider token as a secret with the narrowest permissions available. Never place it in a public Compose file or image layer. The ACME client must be configured with the provider-specific environment variables or plugin settings documented by that provider.
Automate renewal without losing certificates
Let’s Encrypt certificates are short-lived, so issuance is not a one-time task. Renewal works only when the ACME account data and certificate directory survive container recreation. Keep both under the persistent ./letsencrypt mount and back them up securely.
Rank #4
Test the renewal path
Run Certbot’s dry run before scheduling anything:
docker compose run --rm certbot renew --dry-run
A dry run exercises the challenge and renewal configuration without replacing the production certificate. Resolve every error before relying on an automated schedule.
Schedule renewal and reload NGINX
Run a host-level timer or cron job at least twice daily. The command below renews any eligible certificate, then reloads NGINX so workers open the new files:
docker compose run --rm certbot renew --quiet && docker compose exec -T nginx nginx -s reload
Use the absolute path to docker compose in cron, run from the directory containing the Compose file, and log failures. A reload after a renewal attempt is harmless, while skipping the reload leaves NGINX serving the previous certificate until its workers restart. If your deployment uses a systemd timer, place the same sequence in an executable script and make the timer run under an account allowed to access the Docker socket.
Using an ACME companion container instead
A dedicated NGINX ACME companion is an alternative to running Certbot commands yourself. The companion watches the proxy and application containers, stores certificates and ACME account state in persistent volumes, and can trigger a proxy reload after renewal. Its Compose setup still needs a hostname that resolves publicly to the Docker host and shared volumes for certificates and ACME data.
This approach is convenient when many virtual hosts are created or removed through container labels. It is less transparent for a single application: renewal ownership, labels, volume permissions, and reload hooks are controlled by the companion’s conventions. Whichever pattern you choose, do not run both renewal systems against the same certificate directory.
| Decision point | Host or one-shot Certbot container | ACME companion |
|---|---|---|
| Renewal state | You schedule certbot renew and own the persistent volume. |
The companion owns its ACME state and renewal loop. |
| NGINX reload | Your host timer or deploy hook must reload the container. | Usually integrated into the companion workflow. |
| Configuration style | Explicit certificate paths and NGINX files. | Proxy labels, companion environment variables, and shared volumes. |
| Upgrade operations | Upgrade the Certbot image or host package independently. | Upgrade proxy and companion images together according to their compatibility guidance. |
| Best fit | One or a few stable sites where auditable commands matter. | Container platforms with frequently changing virtual hosts. |
Production checks before switching from staging
Use a staging or test ACME endpoint while iterating. Test certificates are not trusted by browsers, but they let you verify routing without consuming production issuance capacity. After a successful dry run, request the production certificate and confirm all of these conditions:
Best Value
- The certificate contains every hostname users will visit, including the
wwwname if it is served. - Both A and, when present, AAAA records resolve to the same host that runs NGINX.
- Port 80 reaches the current NGINX instance from an external network.
- The challenge location returns the token file directly instead of an application redirect or SPA fallback.
- Certificate and ACME directories remain present after
docker compose downfollowed bydocker compose up -d. - NGINX reloads successfully with
nginx -tafter renewal. - The Node.js service is not listed under
ports:and cannot be reached directly from the internet.
Troubleshoot the common failures
HTTP-01 returns a timeout
Check the domain’s A and AAAA records, cloud firewall rules, host firewall, and router forwarding. If an AAAA record points to a machine that does not run NGINX, remove or correct it before retrying.
HTTP-01 returns 404 or application HTML
Confirm that the file exists under ./certbot/www/.well-known/acme-challenge/, that NGINX mounts this directory at /var/www/certbot, and that the challenge location appears before the catch-all location. Test the exact public URL with curl.
Free tools Windows power users keep installed
One-click scans. No signup required.
NGINX fails after enabling TLS
Run docker compose exec nginx nginx -t. A missing file usually means the certificate was written to a different host directory or the domain directory does not match the ssl_certificate path. Check read permissions on the mounted volume and verify that fullchain.pem and privkey.pem exist.
Renewal succeeds but browsers show the old certificate
Certbot may have replaced the files while existing NGINX workers still hold the previous certificate in memory. Execute docker compose exec -T nginx nginx -s reload and inspect the timer logs to ensure the reload runs after a successful renewal.
DNS-01 cannot find the TXT record
Verify that the API token can edit the authoritative zone, that the correct provider plugin variables are present, and that public DNS queries see the new TXT value. Do not assume a local resolver’s cached response reflects the authoritative servers yet.
Operational security checklist
- Publish only NGINX ports 80 and 443; keep Node.js on the private network.
- Protect the private-key and DNS-provider credential files with restrictive host permissions.
- Keep certificate and ACME state on persistent storage that is included in your backup plan.
- Validate NGINX configuration before every reload.
- Monitor renewal logs and alert before certificate expiry.
- Use staging or dry-run validation while changing DNS, Compose mounts, or challenge settings.
- Remove unused hostnames from the certificate request and NGINX
server_namelist.
With these controls, NGINX remains the single public boundary, Let’s Encrypt renewal is repeatable, and the Node.js process can stay on an internal HTTP hop without exposing its port or private application traffic directly to the internet.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

