Halloween DealsAmazon USHalloween week deals: useful picks before month-endAmazon US: current deals, useful picks and tech finds.Check DealsClean PCRecommendedOne scan can reveal what keeps slowing WindowsLook for cleanup and repair opportunities.Run ScanHalloween DealsAmazon USOctober picks are moving - compare before checkoutAmazon US: current deals, useful picks and tech finds.See Picks×
VGSOURCES
Get listed

CertbotPATCH 2026.10.16

How to Secure a Node.js App with Nginx, Let’s Encrypt and Docker Compose

By VGSources Team 10 min read

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Prerequisites and DNS checks

Complete these checks before starting a production certificate request:

  • A registered domain name, such as example.com, with an A record pointing to the Docker host’s IPv4 address.
  • An AAAA record 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 to 0.0.0.0 inside 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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:

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Test the renewal path

Run Certbot’s dry run before scheduling anything:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

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:

  • The certificate contains every hostname users will visit, including the www name 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 down followed by docker compose up -d.
  • NGINX reloads successfully with nginx -t after 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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_name list.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

CertbotDocker ComposeHTTPSLet's EncryptNGINXNode.jsTLS

Leave a reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Recommended PC Tool
Recommended PC Tool
Outdated Drivers Are Slowing You DownFree scan - exact matches
PC Slower Than It Used to Be?Free scan - under a minute

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.