OneLinersCommand workbench
Guides
Web & Proxy / Security

Configure an Nginx reverse proxy with TLS renewal

Proxy an HTTP application through Nginx, preserve client metadata, obtain a certificate, and test renewal.

45 min8 stepsChanges system stateRevision 2
Save or explore
Save to collectionCreate a collection in the sidebar first.
0 of 8 steps completed
Goal

Publish an internal web service through a maintained TLS endpoint without exposing the app port.

Supported environments
  • Ubuntu Server 24.04 LTS
  • Nginx 1.24+
  • Certbot 2.x+
Prerequisites
  • DNS A/AAAA records resolve to the proxy public address.
  • Healthy upstream The application answers locally before Nginx is introduced.curl --fail http://127.0.0.1:3000/health
  • Ports Inbound TCP 80 and 443 are allowed.
Operating boundary

OneLiners never runs these steps or stores secrets. Review placeholders, versions, current state, and change-control requirements before using a command.

Full guide

What you will build

System
  • An Nginx virtual host that accepts public requests for one DNS name and forwards them to a healthy application bound to loopback.
  • A maintained TLS endpoint whose certificate is obtained and installed by Certbot, with HTTP redirected to HTTPS.
  • A verification chain covering the upstream, Nginx syntax, public HTTP routing, certificate identity and expiry, and simulated renewal.
Observable outcome
  • The application port remains private while https://app.example.com/health returns the upstream health response through Nginx.
  • The upstream receives the original Host, client address and scheme headers needed for logging and secure URL generation.
  • The certificate contains the requested hostname and Certbot's dry-run renewal succeeds against the ACME staging service.

Architecture

How the parts fit together

Nginx terminates the public TCP and TLS connection, selects a server block by destination and Host/SNI name, then creates a separate HTTP connection to the loopback application.

Public DNS and portsRoute the application hostname to TCP 80 and 443 on the proxy.
NginxSelects the virtual host, terminates TLS, adds trusted forwarding headers and proxies requests.
Loopback upstreamServes the application on 127.0.0.1 without direct public exposure.
Certbot and ACMEProve control of the hostname, deploy certificate paths and schedule future renewal.
  1. The client resolves the application hostname and connects to Nginx.
  2. Nginx selects the matching server block and, after certificate deployment, terminates TLS.
  3. Nginx sends a new HTTP request to the loopback upstream with the reviewed forwarding headers.
  4. The response returns through Nginx, which handles public connection behavior and certificate presentation.

Assumptions

  • The DNS A record, and AAAA record if published, already points to this proxy and is visible from public resolvers.
  • TCP 80 and 443 reach Nginx; HTTP-01 validation still requires public port 80 even when normal users are redirected to HTTPS.
  • The application already answers its health endpoint on loopback and is configured to trust forwarding headers only from Nginx.
  • No CDN or load balancer terminates TLS in front of this host. That architecture requires different client-IP trust and certificate decisions.

Key concepts

Reverse proxy
A server that accepts a client request and creates a separate request to an upstream application.
Virtual host
An Nginx server block selected by listen address and server_name.
TLS termination
Decryption and certificate presentation at Nginx before a separate upstream connection.
ACME HTTP-01
A domain-control challenge fetched by the certificate authority over public HTTP on port 80.
Forwarded headers
Metadata set by the trusted proxy so the upstream can learn the original host, client address and scheme.

Before you copy

Values used in this guide

{{appHost}}

Public DNS hostname for the application.

Example: app.example.com
{{upstreamHost}}

Private or loopback upstream address.

Example: 127.0.0.1
{{upstreamPort}}

TCP port on which the application listens.

Example: 3000
{{adminEmail}}

Operational mailbox for ACME account and expiry notices.

Example: admin@example.com

Security and production boundaries

  • Keep the upstream on loopback or a private network protected by its own policy; TLS at Nginx does not secure an exposed application port.
  • Only trust X-Forwarded-For from controlled proxies. Direct clients can forge this header if the application port is reachable.
  • Certbot modifies Nginx configuration and stores private keys under /etc/letsencrypt. Back up configuration and restrict root access.
  • Do not enable HSTS until HTTPS works for every required subdomain and rollback implications are understood.

Stop before continuing if

  • Stop if the upstream health check fails directly; adding Nginx cannot repair an unhealthy application.
  • Stop if DNS points elsewhere, public port 80 is unavailable, or an unexpected AAAA record routes validation to another host.
  • Stop on any nginx -t error and restore the previous site before reload.
  • Stop if Certbot cannot complete its staging or renewal test; a currently valid certificate without renewal is unfinished.
01

command

Install Nginx and Certbot

caution

Use supported Ubuntu packages.

Why this step matters

Install the proxy, ACME client and Nginx integration from supported Ubuntu repositories so service units and renewal automation share one maintenance path.

What to understand

The Nginx plugin can authenticate the hostname and edit the selected server block for certificate deployment.

Package installation may start Nginx with its default site. Inventory current listeners and configuration before publishing the new hostname.

System changes

  • Installs Nginx, Certbot and python3-certbot-nginx; may start and enable package services and timers.

Syntax explained

apt update
Refreshes package metadata.
apt install --yes
Installs the named supported packages and accepts the prompt.
python3-certbot-nginx
Provides Certbot's Nginx authenticator and installer plugin.
Command
sudo apt update && sudo apt install --yes nginx certbot python3-certbot-nginx
Example output / evidence
Setting up nginx ...
Setting up python3-certbot-nginx ...

Checkpoint: Confirm installed components

nginx -v && certbot --version && systemctl is-active nginx

Continue whenBoth tools report versions and Nginx is active.

Stop whenAPT is incomplete or Nginx fails before any custom site exists.

Security notes

  • Review default sites and listeners; installation alone should not be mistaken for a hardened public service.

Alternatives

  • Use the distribution packages instead of piping a third-party installer into a shell.

Stop conditions

  • Do not continue while package or service state is failed.
02

verification

Verify the upstream directly

read-only

Record the expected status and response before adding the proxy.

Why this step matters

Establish application health directly so later failures can be attributed to Nginx, DNS or TLS rather than an already broken upstream.

What to understand

The request uses loopback and the exact health path the proxy will publish. --fail makes HTTP 400 and 500 responses fail the command.

Capture status, headers and deterministic response body. If the application has no health endpoint, create one that verifies only safe local dependencies.

System changes

  • No persistent changes; sends one local HTTP request.

Syntax explained

--fail
Returns a non-zero exit code for HTTP error status responses.
--silent --show-error
Suppresses progress while preserving error diagnostics.
--include
Prints response headers with the body.
Command
Fill variables0/2 ready

Values stay on this page and are never sent or saved.

curl --fail --silent --show-error --include http://{{upstreamHost}}:{{upstreamPort}}/health
Example output / evidence
HTTP/1.1 200 OK

{"status":"ok"}

Checkpoint: Require a healthy local response

curl --fail --silent --show-error --include http://{{upstreamHost}}:{{upstreamPort}}/health

Continue whenHTTP 200 and the application's expected health payload.

Stop whenConnection fails, status is not successful, or response comes from the wrong application.

Security notes

  • Keep the upstream bound to loopback unless a private network design explicitly requires otherwise.

Alternatives

  • Use a Unix-domain socket health request where the server supports it.

Stop conditions

  • Fix the application before introducing the proxy.
03

config

Create the virtual host

caution

Forward the original host, client address, and protocol to a loopback or private upstream.

Why this step matters

A dedicated server block binds one hostname to one upstream and sets trusted metadata at the proxy boundary.

What to understand

server_name controls host selection, while proxy_pass creates the upstream request. The trailing URI behavior of proxy_pass matters; this baseline preserves the original request URI.

Host, X-Real-IP, X-Forwarded-For and X-Forwarded-Proto let a correctly configured application generate URLs and logs from the original request context.

System changes

  • Creates /etc/nginx/sites-available/app.example.com as a new HTTP virtual host.

Syntax explained

listen 80 / [::]:80
Accepts IPv4 and IPv6 HTTP where those addresses exist.
server_name
Selects this server block for the application hostname.
location /
Applies the proxy to every request path.
proxy_pass
Forwards requests to the private upstream.
proxy_http_version 1.1
Uses HTTP/1.1 for the upstream connection.
proxy_set_header
Replaces client-supplied forwarding metadata with values chosen by Nginx.
File /etc/nginx/sites-available/{{appHost}}
Configuration
Fill variables0/3 ready

Values stay on this page and are never sent or saved.

server {
  listen 80;
  listen [::]:80;
  server_name {{appHost}};
  location / {
    proxy_pass http://{{upstreamHost}}:{{upstreamPort}};
    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 $scheme;
  }
}
Example output / evidence
Site file written

Checkpoint: Review rendered host and upstream

Continue whenNo variables remain; server_name matches public DNS and proxy_pass matches the proven loopback listener.

Stop whenThe upstream is public, the hostname is wrong, or another enabled server block claims the same name.

Security notes

  • The application must trust these headers only from Nginx, not from arbitrary clients.

Alternatives

  • Use an upstream group when multiple application instances require balancing.

Stop conditions

  • Do not enable a file with unresolved variables or a duplicate server_name.
04

command

Enable and validate the site

caution

Stop on any nginx -t failure.

Why this step matters

Enable the reviewed site through Ubuntu's sites-enabled convention and require Nginx's full configuration test before any reload.

What to understand

The symbolic link keeps one editable source in sites-available. nginx -t parses all includes and checks referenced files.

If the link already exists, inspect it rather than replacing it blindly. Remove the default site only after confirming it is not required.

System changes

  • Creates a symbolic link under /etc/nginx/sites-enabled; does not reload Nginx until a later step.

Syntax explained

ln -s
Creates a symbolic link to the reviewed site file.
nginx -t
Validates complete Nginx syntax and referenced files.
&&
Runs validation only after link creation succeeds.
Command
Fill variables0/1 ready

Values stay on this page and are never sent or saved.

sudo ln -s /etc/nginx/sites-available/{{appHost}} /etc/nginx/sites-enabled/{{appHost}} && sudo nginx -t
Example output / evidence
syntax is ok
configuration file test is successful

Checkpoint: Require successful syntax validation

sudo nginx -t

Continue whensyntax is ok and configuration file test is successful.

Stop whenAny emerg, duplicate, missing-file or syntax diagnostic appears.

Security notes

  • Validation is read-only, but enabling a link changes what the next reload will publish.

Alternatives

  • Use an atomic configuration-management symlink deployment with the same nginx -t gate.

Stop conditions

  • Never reload after a failed nginx -t.
05

command

Reload and test HTTP

caution

Reload without dropping connections and test the public hostname.

Why this step matters

Apply the validated HTTP proxy without dropping established connections, then test the public hostname before involving certificate issuance.

What to understand

A reload starts new workers with the new configuration and lets old workers finish active requests.

The hostname test exercises Nginx server selection. Use --resolve during pre-DNS testing only when you understand that it overrides normal resolution for the one command.

System changes

  • Reloads Nginx so the new HTTP virtual host begins handling requests.

Syntax explained

systemctl reload nginx
Applies configuration gracefully.
curl --fail --include
Requires a successful public HTTP response and prints its headers.
Command
Fill variables0/1 ready

Values stay on this page and are never sent or saved.

sudo systemctl reload nginx && curl --fail --include http://{{appHost}}/health
Example output / evidence
HTTP/1.1 200 OK
Server: nginx

Checkpoint: Prove public HTTP routing

curl --fail --include http://{{appHost}}/health

Continue whenHTTP 200 from Nginx with the same application health payload as the direct baseline.

Stop whenThe wrong virtual host responds, status differs, or public DNS reaches another address.

If this step fails

The hostname returns Nginx's default page.

Likely causeDNS, Host selection or an enabled default server block does not match the intended virtual host.

Safe checks
  • getent ahosts app.example.com
  • sudo nginx -T | grep -nE 'listen|server_name'

ResolutionCorrect DNS and server_name selection, validate and reload before requesting a certificate.

Security notes

  • Keep the upstream port blocked publicly even while HTTP on Nginx is open.

Alternatives

  • Use curl --resolve for a controlled pre-DNS check, then repeat with normal public DNS.

Stop conditions

  • Do not request a certificate until public HTTP reaches the correct virtual host.
06

command

Obtain and deploy the certificate

caution

Provide an operational renewal address and review Certbot's generated redirect.

Why this step matters

Obtain and deploy a hostname-valid certificate only after public DNS and HTTP routing are proven, reducing failed ACME attempts and unexpected Nginx edits.

What to understand

The Nginx plugin performs the challenge and installs certificate references in the matching server block. --redirect changes ordinary HTTP requests to HTTPS.

The email belongs to the ACME account for operational notices. Review Certbot's configuration diff and certificate names after completion.

System changes

  • Creates ACME account and certificate state under /etc/letsencrypt and modifies Nginx configuration to serve HTTPS and redirect HTTP.

Syntax explained

--nginx
Uses the Nginx plugin for authentication and installation.
-d
Requests the exact DNS identifier for the certificate.
--redirect
Configures HTTP-to-HTTPS redirection for the authenticated virtual host.
--email
Sets the ACME account contact address.
--agree-tos
Accepts the ACME server subscriber agreement non-interactively.
--no-eff-email
Does not share the account email with EFF.
Command
Fill variables0/2 ready

Values stay on this page and are never sent or saved.

sudo certbot --nginx -d {{appHost}} --redirect --email {{adminEmail}} --agree-tos --no-eff-email
Example output / evidence
Successfully deployed certificate for app.example.com

Checkpoint: Inspect certificate and Nginx after deployment

sudo certbot certificates && sudo nginx -t

Continue whenThe certificate lists the exact hostname, has a future expiry and Nginx validation succeeds.

Stop whenAn unexpected domain is present, private-key paths are missing, or nginx -t fails.

If this step fails

Certbot cannot find a matching server block.

Likely causeserver_name does not exactly match the requested -d hostname or the site is not enabled.

Safe checks
  • sudo nginx -T | grep -n 'server_name app.example.com'
  • readlink -f /etc/nginx/sites-enabled/app.example.com

ResolutionCorrect and enable the HTTP server block, validate, reload and repeat public HTTP testing before retrying.

Security notes

  • Private keys under /etc/letsencrypt must remain root-controlled and included in secure recovery planning.

Alternatives

  • Use certonly with webroot or DNS plugins when automatic Nginx editing is not desired.

Stop conditions

  • Stop if the ACME challenge reaches the wrong host or Certbot leaves Nginx invalid.
07

verification

Dry-run renewal

read-only

A certificate is not ready until automated renewal succeeds.

Why this step matters

A certificate that works today but cannot renew is a scheduled outage, so test the saved renewal configuration immediately.

What to understand

renew --dry-run uses the Let's Encrypt staging server by default, obtains test certificates and does not replace live certificate files.

The test may temporarily reload web-server configuration. Inspect the systemd timer or scheduled task that will invoke future renewal.

System changes

  • Uses temporary ACME challenge state and may reload Nginx; does not save a production certificate.

Syntax explained

renew
Selects installed certificates using their saved renewal configuration.
--dry-run
Tests renewal against staging without storing the test certificate.
Command
sudo certbot renew --dry-run
Example output / evidence
Congratulations, all simulated renewals succeeded

Checkpoint: Require simulated renewal success

sudo certbot renew --dry-run && systemctl list-timers '*certbot*' --no-pager

Continue whenAll simulated renewals succeed and a future automated invocation is visible.

Stop whenAny authenticator, challenge, hook or reload fails.

Security notes

  • Do not repeatedly force production renewals for testing; certificate authorities enforce rate limits.

Alternatives

  • Use certbot reconfigure for supported renewal parameter changes, then repeat the dry run.

Stop conditions

  • Do not finish the change with a failed dry run or absent renewal schedule.
08

verification

Inspect the external endpoint

read-only

Confirm HTTPS, hostname, expiry, and application response.

Why this step matters

An external TLS client proves the public route, SNI hostname, certificate chain, expiry and application response together.

What to understand

curl validates the certificate against the client trust store and requires a successful application response.

openssl s_client supplies the SNI name and x509 prints the leaf subject and validity window without dumping private material.

System changes

  • No persistent changes; creates public HTTPS test connections.

Syntax explained

curl --fail --include
Validates TLS and requires a successful HTTP status while showing headers.
s_client -connect host:443
Opens a diagnostic TLS connection to the endpoint.
-servername
Sends the SNI hostname used for virtual-host certificate selection.
x509 -noout -subject -dates
Prints only leaf identity and validity fields.
Command
Fill variables0/1 ready

Values stay on this page and are never sent or saved.

curl --fail --include https://{{appHost}}/health && openssl s_client -connect {{appHost}}:443 -servername {{appHost}} </dev/null 2>/dev/null | openssl x509 -noout -subject -dates
Example output / evidence
HTTP/2 200
subject=CN = app.example.com
notAfter=...

Checkpoint: Validate public identity and health

curl --fail --include https://{{appHost}}/health && openssl s_client -connect {{appHost}}:443 -servername {{appHost}} </dev/null 2>/dev/null | openssl x509 -noout -subject -dates

Continue whenHTTPS returns the application health response and the leaf certificate covers the hostname with a future notAfter date.

Stop whenTrust validation fails, the wrong certificate is served, or the app response differs from the direct baseline.

Security notes

  • Test from outside the server network and through normal DNS to avoid split-horizon false confidence.

Alternatives

  • Use an independent monitored probe as continuing verification after this manual test.

Stop conditions

  • Do not declare success based only on Nginx service state or a local curl with certificate verification disabled.

Finish line

Verification checklist

Nginxsudo nginx -tConfiguration test succeeds.
HTTPScurl --fail https://app.example.com/healthReturns the application health response.
Renewalsudo certbot renew --dry-runAll simulated renewals succeed.

Recovery guidance

Common problems and safe checks

Nginx returns 502 Bad Gateway.

Likely causeThe upstream is stopped, bound to another address or port, or Nginx is denied access.

Safe checks
  • curl --fail --include http://127.0.0.1:3000/health
  • sudo ss -lntp
  • sudo journalctl -u nginx -n 50 --no-pager

ResolutionRestore the upstream or correct proxy_pass to the verified listener; do not open the application port publicly.

Certbot reports an unauthorized HTTP-01 challenge.

Likely causeDNS resolves to another address, port 80 is blocked, IPv6 points elsewhere, or another server block handles the hostname.

Safe checks
  • getent ahosts app.example.com
  • curl -I http://app.example.com/
  • sudo nginx -T | grep -n 'server_name app.example.com'

ResolutionCorrect public DNS, firewall and virtual-host selection, wait for DNS propagation, then retry without repeatedly consuming production rate limits.

HTTPS works but the application redirects to HTTP or logs the proxy address as the client.

Likely causeThe upstream does not trust Nginx forwarding headers or Nginx did not set the required header.

Safe checks
  • Inspect the rendered Nginx server block
  • Check upstream request logs for Host and X-Forwarded-Proto
  • Confirm the app trusts only the proxy address

ResolutionSet the headers in Nginx and configure the framework's trusted-proxy list narrowly.

certbot renew --dry-run fails while the current site remains valid.

Likely causeThe authenticator path, DNS, port 80, plugin or saved renewal parameters changed after issuance.

Safe checks
  • sudo certbot certificates
  • sudo systemctl list-timers '*certbot*'
  • sudo journalctl -u certbot.service -n 50 --no-pager

ResolutionRepair the saved authentication path and repeat a dry run; do not hand-edit renewal files without the documented Certbot reconfigure process.

After the procedure

Alternatives and next steps

Consider these alternatives

  • Use a DNS-01 ACME authenticator when public port 80 cannot be exposed or wildcard certificates are required.
  • Terminate TLS at a managed edge only when the Nginx-to-edge trust and client-address chain are explicitly configured.
  • Use a Unix socket for a same-host upstream when the application supports it and ownership is easier to constrain than a TCP listener.

Operate it safely

  • Add access and error log monitoring, upstream latency metrics and an external HTTPS health check.
  • Review proxy timeouts and request-size limits using real application behavior rather than copying generic large values.
  • Add HSTS only after every relevant hostname is permanently HTTPS-capable.
  • Test restoration of /etc/nginx and /etc/letsencrypt under the same ownership and permission model.

Reference

Frequently asked questions

Why test the upstream before Nginx?

It creates a known-good baseline. A later 502 can then be localized to the listener, proxy configuration or access controls instead of the application itself.

Why keep port 80 open after redirecting to HTTPS?

HTTP-01 renewal still reaches port 80. The Nginx plugin can serve the challenge while redirecting ordinary requests.

Should proxy_pass use HTTPS?

Use HTTPS for an upstream crossing an untrusted network. For a loopback listener, strict local binding and host security are usually the primary boundary.

Recovery

Rollback

Disable the new virtual host and restore the previous proxy.

  1. Restore the saved site or remove its sites-enabled link.
  2. Run sudo nginx -t.
  3. Reload Nginx only after validation succeeds.
  4. Revoke the certificate only when issuance or its private key is compromised.

Evidence

Sources and review

Verified 2026-07-24Review due 2026-10-22
Nginx reverse proxy guideofficialNginx request processingofficialCertbot user guideofficial