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

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

To serve an application at https://app.example.com, point its DNS name at the reverse proxy, terminate public TLS at that proxy, and forward requests to an application port that is not publicly exposed. Then configure the app to trust the proxy’s original-scheme and client-address headers, automate certificate renewal and proxy reloads, and test the complete path—including redirects, cookies, and WebSockets.

This guide uses NGINX for a concrete setup and includes Caddy and framework-specific examples. The sample backend is 127.0.0.1:3000; substitute your application’s actual address and port.

Choose how TLS works on each hop

HTTPS at the browser-to-proxy connection does not by itself encrypt the proxy-to-application connection. Decide explicitly which component owns the public certificate and whether the internal connection needs encryption.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Mode What happens When it fits
TLS termination The proxy decrypts browser HTTPS and forwards HTTP to the app. A straightforward deployment where the backend is on localhost or a private, access-controlled network.
TLS re-encryption The proxy decrypts public HTTPS, then establishes a separate HTTPS connection to the backend. The proxy and app communicate over a shared or untrusted network, or policy requires encryption in transit on every hop.
TLS passthrough The proxy forwards the TLS connection without decrypting it; the application presents the certificate. A case where the app must own TLS. Because the proxy cannot inspect HTTP requests, HTTP-aware routing and header handling are limited. HAProxy documents termination, initiation, and offloading as distinct modes: HAProxy introduction.

The NGINX example below uses TLS termination: the browser connects over HTTPS, while NGINX forwards HTTP to the local app. That internal hop is separate from the public HTTPS connection.

Prepare DNS, ports, and the backend

Before requesting a certificate, make sure the hostname reaches the proxy and the app is reachable from it.

  • Create an A record for app.example.com pointing to the proxy’s public IPv4 address. Add an AAAA record only if IPv6 traffic is routed and firewalled to that proxy.
  • Allow inbound TCP ports 80 and 443 to the proxy in the host firewall and any cloud security group. HTTP-01 certificate validation needs public port 80; normal public HTTPS uses 443.
  • Keep the application listening on a private interface, such as 127.0.0.1:3000, a private address, or a container network. Firewall its port from the public internet: direct access can bypass proxy controls and expose an unencrypted route.
  • Have administrative access to configure the proxy and a plan for certificate renewal.

Check DNS answers and local listeners with:

dig +short A app.example.com
dig +short AAAA app.example.com
ss -ltnp

If an AAAA record exists, ensure IPv6 works end to end. Some clients may prefer a broken IPv6 route even when IPv4 works.

Obtain a certificate

A public certificate must cover the hostname users visit. For a conventional public site, ACME automates certificate issuance and renewal. The validation method determines what must be reachable or controlled.

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.

HTTP-01 with Certbot

With HTTP-01, the ACME client places a token at http://app.example.com/.well-known/acme-challenge/<TOKEN>, and validation occurs over public port 80; it cannot use an arbitrary port. Configure the proxy to serve the challenge directory or route that path to the ACME client’s webroot. Do not send it to an application that cannot serve the token. See Let’s Encrypt challenge types.

For a Certbot webroot setup, create a directory and request a certificate:

sudo mkdir -p /var/www/acme
sudo certbot certonly \
  --webroot -w /var/www/acme \
  -d app.example.com

The NGINX plugin can also configure a certificate, or obtain one without installing it into the NGINX configuration:

sudo certbot --nginx
sudo certbot certonly --nginx

See Certbot usage documentation and the Certbot NGINX instructions. If you use HTTP-01, keep port 80 reachable and preserve the challenge path when adding an HTTP-to-HTTPS redirect.

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.

DNS-01 for private services or wildcard names

DNS-01 proves control by publishing a TXT record at _acme-challenge.example.com. It can issue wildcard certificates and works when the application itself is private, provided the certificate authority can check the public DNS record. For unattended renewal, use a DNS provider API with narrowly scoped credentials rather than relying on manual TXT updates. Details are in the challenge documentation.

TLS-ALPN-01 when port 80 is unavailable

TLS-ALPN-01 validates over port 443 and can be useful when public port 80 cannot be exposed. The ACME client or front-end proxy must support this method. Compare the validation requirements in Let’s Encrypt’s challenge guide.

Caddy automatic HTTPS

If Caddy’s operational model suits your deployment, a hostname-based Caddyfile can obtain and renew public certificates and redirect HTTP to HTTPS:

app.example.com {
    reverse_proxy 127.0.0.1:3000
}

Caddy expects public DNS to point to the server and ports 80 and 443 to be reachable for its usual automatic HTTPS flow. A command-line alternative is:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
caddy reverse-proxy --from app.example.com --to 127.0.0.1:3000

See Caddy automatic HTTPS and its reverse-proxy quick start. Explicit ACME tooling may fit better when certificates need to be shared across proxies or infrastructure systems.

Configure NGINX to serve HTTPS and proxy the app

NGINX needs an HTTPS listener, a certificate, and its matching private key. Use the complete certificate chain—normally Certbot’s fullchain.pem—rather than only the leaf certificate. NGINX expects the server certificate before intermediate certificates in the chain. The private key should be readable by NGINX’s master process and inaccessible to untrusted users. See NGINX HTTPS configuration and Certbot certificate files.

# HTTP: serve the ACME challenge and redirect other requests
server {
    listen 80;
    listen [::]:80;
    server_name app.example.com;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/acme;
        default_type text/plain;
        try_files $uri =404;
    }

    location / {
        return 308 https://$host$request_uri;
    }
}

# HTTPS: terminate TLS and forward requests to the application
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name app.example.com;

    ssl_certificate     /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;

    location / {
        proxy_pass http://127.0.0.1:3000;

        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 $scheme;
        proxy_set_header X-Forwarded-Host  $host;

        proxy_http_version 1.1;
    }
}

The proxy_set_header directives control what NGINX sends upstream; $proxy_add_x_forwarded_for appends the connecting address to an existing forwarded chain. These headers matter only if the application is configured to trust them safely. See the NGINX proxy module documentation.

Test the configuration and reload NGINX so it begins serving the certificate:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
sudo nginx -t
sudo systemctl reload nginx

NGINX syntax and defaults can vary by installed version; validate using the local installation and its documentation.

Choose an HTTP-to-HTTPS redirect

The example uses 308, a permanent redirect that explicitly preserves the request method. A 301 is widely compatible, but historical client behavior can change a POST into a GET. For APIs or other non-idempotent requests, consider method preservation and check client compatibility before choosing. See RFC 9110 redirect semantics.

Keep the ACME challenge location available on port 80; do not blindly redirect that path or assume every validation client and authority will follow the redirect as intended. For API-only endpoints, rejecting plaintext HTTP can be safer than redirecting a request that contains credentials or a body. See the OWASP TLS Cheat Sheet.

Configure the application to trust only the proxy

After TLS termination, the backend connection may be HTTP. Without reliable original-scheme information, an application can generate http:// links, omit secure cookie attributes, construct incorrect OAuth callback URLs, or redirect repeatedly to HTTPS. The proxy should overwrite the forwarded scheme, host, and client-address headers, and the app should accept them only from a known proxy address or network. The standardized Forwarded header and the widely used X-Forwarded-* headers carry this kind of information; neither is trustworthy merely because it is present. See MDN’s Forwarded reference and X-Forwarded-Proto.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Have the proxy overwrite authoritative forwarded values rather than pass through client-supplied values.
  • Restrict direct access to the backend so clients cannot bypass the proxy and submit forged headers.
  • Configure the app to trust the proxy’s exact address or network. In a multi-proxy path, document the trusted hops and header order instead of trusting every proxy indiscriminately.

Express

For a proxy on loopback, Express can trust loopback; use the proxy’s exact address or subnet when that is more appropriate for your topology:

app.set('trust proxy', 'loopback');

Express warns that unrestricted proxy trust can let clients supply forged forwarding values. See Express: behind proxies.

Django

Set the HTTPS indicator only if the proxy reliably sets and strips the header, and the backend is not directly reachable by hostile clients:

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

See Django’s SECURE_PROXY_SSL_HEADER setting and security guidance.

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

ASP.NET Core

Process forwarded headers before HTTPS redirection, and list only proxies that are trusted to supply them:

builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders =
        ForwardedHeaders.XForwardedFor |
        ForwardedHeaders.XForwardedProto;

    options.KnownProxies.Add(IPAddress.Parse("10.0.0.10"));
});

app.UseForwardedHeaders();
app.UseHttpsRedirection();

Use the proxy address that applies in your network. Microsoft’s proxy and load-balancer guidance explains trusted proxy configuration.

Handle WebSockets, streaming, and long-lived requests

WebSocket upgrade headers are hop-by-hop headers, so NGINX does not automatically pass them upstream. For an app whose WebSocket endpoint is /socket/, configure that route explicitly:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

location /socket/ {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade    $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_set_header Host       $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 60m;
}

Put the map directive in NGINX’s http context, not inside a server or location. NGINX documents the upgrade handling and a default 60-second inactivity timeout in its WebSocket proxying guide.

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

Server-sent events, long polling, streaming responses, and large downloads may need their own buffering and timeout decisions. Do not apply WebSocket settings to all traffic without checking their effect on normal requests.

Encrypt the proxy-to-application hop when needed

HTTP to a backend on localhost or a private, access-controlled host can be reasonable for some deployments. Use upstream HTTPS when the connection crosses an untrusted or shared network, policy requires in-transit encryption, the backend is in another host, region, or data center, or mutual TLS is required.

With NGINX, point proxy_pass to an HTTPS upstream and verify its certificate against a trusted CA:

proxy_pass https://backend.internal.example:8443;
proxy_ssl_server_name on;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/nginx/internal-ca.pem;

Do not make disabled verification a permanent workaround: it removes the authentication protection of that TLS hop. See NGINX secure upstream guidance. Caddy also documents HTTPS upstreams and does not recommend disabling verification.

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

Enable HSTS only after HTTPS is reliable

HTTP Strict Transport Security tells browsers to use HTTPS for a host. Send it only over HTTPS; browsers ignore HSTS received over HTTP. A basic header is:

Strict-Transport-Security: max-age=31536000

Start with a short max-age, check that browser and non-browser clients work, and increase it deliberately. Add includeSubDomains only when every present and future subdomain supports HTTPS. Use preload only after reviewing its long-term consequences and meeting the preload requirements. Once HSTS is active, browsers will not let users bypass certificate errors for the covered host. See MDN’s HSTS reference and the OWASP HSTS Cheat Sheet.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Automate renewal and reload the proxy

Certificate policies change. On February 24, 2026, Let’s Encrypt announced a transition from 90-day certificates toward shorter 64-day and eventually 45-day defaults. Treat renewal automation as essential, and check current policy rather than relying on a fixed lifetime assumption. See the certificate-lifetime announcement.

Certbot can test renewal without replacing live certificates:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
sudo certbot renew --dry-run

Renewed files on disk do not guarantee that a running NGINX process has loaded them. Configure a deploy hook or equivalent action after successful renewal:

sudo certbot renew \
  --deploy-hook "systemctl reload nginx"

Certbot documents --deploy-hook for actions that run after a successful renewal in its renewal documentation. Check the renewal timer or scheduler used by your system, and verify the certificate served externally after a real renewal.

Verify the public request path

Run tests from outside the server where possible, and check IPv4 and IPv6 separately if DNS publishes both.

DNS and reachability

dig +short A app.example.com
dig +short AAAA app.example.com
nc -vz app.example.com 80
nc -vz app.example.com 443

Confirm the returned addresses belong to the intended proxy and both public ports reach it.

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

Redirect and certificate

Check that HTTP redirects to the correct HTTPS URL:

curl -I http://app.example.com/

Inspect the status and Location header. Test the certificate chain and SNI selection with:

openssl s_client \
  -connect app.example.com:443 \
  -servername app.example.com \
  -showcerts </dev/null

The -servername option sends SNI, which matters when multiple hostnames share an IP address. NGINX selects a certificate during the TLS handshake, before it can see the HTTP request’s host. See NGINX HTTPS and SNI documentation.

End-to-end application behavior

Make a verbose HTTPS request:

curl -v https://app.example.com/

Check the certificate name and SAN, expiry, HTTP status, redirects, HSTS, cookie attributes such as Secure and SameSite, and whether generated absolute URLs use https://. Before DNS is changed, you can test a specific proxy IP while retaining the hostname and SNI:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl --resolve app.example.com:443:203.0.113.10 \
     https://app.example.com/

Replace 203.0.113.10 with the proxy’s test address.

Renewal behavior

sudo certbot renew --dry-run
sudo systemctl status certbot.timer

Confirm the dry run succeeds and that your system’s renewal timer or scheduler is active. After renewal, verify that the proxy reloads and presents the new certificate.

Troubleshoot common failures

Symptom Likely cause What to check or change
Browser repeatedly redirects The proxy terminates TLS, but the app believes the request arrived over HTTP. Set the original scheme header, configure the framework’s trusted-proxy behavior, and ensure forwarded-header middleware runs before HTTPS redirection.
Wrong or default certificate DNS points elsewhere, the name does not match the NGINX server block, or a default virtual host is selected. Check DNS and use openssl s_client -servername app.example.com to inspect SNI certificate selection.
Certificate-chain error The server is presenting only the leaf certificate. Configure fullchain.pem, which includes the server certificate and intermediates. See NGINX certificate-chain guidance.
ACME validation fails Port 80 does not reach the right proxy, the challenge is blocked or routed incorrectly, DNS is stale, or IPv6 points to a broken endpoint. Check public DNS, firewall and CDN rules, unauthenticated access to the challenge path, IPv6 routing, and challenge consistency across multiple proxies.
App logs show the proxy as every client The client-address header is missing, or the app is not configured to trust it. Forward X-Forwarded-For and configure trust for only the proxy network.
Clients can spoof their address or HTTPS state The backend is reachable directly, or the app trusts arbitrary forwarded headers. Firewall the backend and overwrite authoritative headers at the proxy.
WebSockets fail or disconnect Upgrade headers are missing, the upstream is not using HTTP/1.1, the route differs, or the timeout is too short. Check the WebSocket location, upgrade directives, and an appropriate read timeout. See NGINX WebSocket documentation.
Mixed-content warnings or insecure login cookies The app sees an HTTP request internally and generates HTTP URLs or omits the Secure cookie attribute. Fix trusted-proxy and original-scheme handling before relying on production URL, cookie, or OAuth settings.
Renewal succeeds, but users still see the old certificate The certificate files changed, but the proxy did not reload. Use a successful-renewal deploy hook and inspect the certificate served from outside the host.

For a deployment with a CDN or multiple proxies, define which trusted edge owns the public certificate, redirect, client-IP extraction, forwarded headers, HSTS, WebSocket upgrades, and access logs. Only that trusted edge should set authoritative forwarding values.

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.

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