Skip to content
+1 (813) 212-3723 support@layeronecloud.com
Self-hosting VPS guides

How to Set Up a Reverse Proxy VPS with Caddy and HTTPS

Build a small Caddy reverse proxy VPS with automatic HTTPS, exact DNS and firewall rules, loopback or private backends, validation, logs, backups, and upgrades.

7 min read

A reverse proxy accepts public HTTP and HTTPS traffic, selects a backend from the requested hostname, and forwards the request to an application on a private port. It gives several services one controlled internet edge and keeps ports such as 3000, 8000, and 8123 off the public interface.

This guide uses Caddy on Ubuntu 24.04 LTS x86-64. Caddy's official stable package runs as a systemd service and automatically obtains and renews public TLS certificates when DNS points to the VPS and ports 80 and 443 are reachable. The examples proxy ordinary HTTP and WebSocket traffic; raw TCP/UDP proxying requires a different design or non-standard modules.

Reverse proxy VPS requirements and LayerOne cost

Item Practical starting point
Minimum RAM 512 MB for one or two light sites with bounded logs and no local application stack
Recommended RAM 1 GB for several small proxies, certificate operations, and OS update headroom
Expected CPU usage Often below 1–3% of one vCPU when idle; TLS handshakes, compression, large responses, and high request rates can saturate the vCPU
Storage requirement 10 GB is a small floor for Ubuntu, packages, Caddy state, and logs; 20 GB leaves safer update and log headroom
Exact LayerOne SKU gc.nano (layerone-nano): 1 vCPU, 1 GB RAM, 20 GB disk
Expected monthly cost $0.0041/hour, about $3.00 for 730 hours at the catalog's monthly equivalent

The proxy does not remove the backend's resource cost. CPU scales with traffic, cipher work, compression, response size, and connection behavior; storage scales mainly with access logs and retained diagnostics. Measure production latency and saturation before consolidating critical services behind one edge.

LayerOne meters the VPS hourly from prepaid credit. The monthly equivalent excludes taxes, optional services, separate backend VPS instances, third-party services, and transfer beyond the account allowance. Confirm the current pricing matrix.

1. Prepare DNS and the backend

Complete Your first hour on a new server. At the DNS provider, create one A record per service and point it at this VPS, for example:

app.example.com  -> 203.0.113.10
api.example.com  -> 203.0.113.10

Confirm each record from the VPS:

getent ahostsv4 app.example.com
getent ahostsv4 api.example.com

Before involving Caddy, prove every backend responds from the proxy host:

curl -I http://127.0.0.1:3000/
curl -I http://127.0.0.1:8000/health

Use the backend's actual health path. If the application runs in Docker, bind its host port to loopback, such as 127.0.0.1:3000:3000. If it runs on another VPS, put both servers on a private network and let the backend firewall accept its application port only from the proxy's private /32 address.

2. Open only the edge ports

Replace the administrator CIDR, then configure UFW:

ADMIN_CIDR="198.51.100.24/32"
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from "$ADMIN_CIDR" to any port 22 proto tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status numbered

Use the same rules in the LayerOne cloud firewall: inbound DROP; TCP 22 from the administrator CIDR; TCP 80 and 443 from 0.0.0.0/0; no public backend ports. Read The cloud firewall.

Port Exposure Purpose
TCP 22 Administrator CIDR only Host SSH
TCP 80 Public HTTP-to-HTTPS redirect and ACME validation
TCP 443 Public HTTPS applications and certificate validation
TCP 2019 Loopback only Caddy administration API; never publish it
TCP 3000/8000/8123/etc. Loopback or private network only Application backends

Docker can publish ports past UFW. Bind Docker backends to 127.0.0.1 and keep the LayerOne cloud firewall at inbound DROP; see Docker on a VPS.

3. Install Caddy from its official stable repository

Install Caddy's Debian/Ubuntu repository exactly once:

sudo apt update
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key'   | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt'   | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg
sudo chmod o+r /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddy

The package starts the caddy service. Check it before editing configuration:

caddy version
sudo systemctl status caddy --no-pager
sudo ss -lntp | grep -E ':(80|443|2019)'

The admin endpoint should show only on loopback.

4. Define hostname-to-backend routes

If this is a fresh Caddy installation, replace /etc/caddy/Caddyfile with the following after changing names and ports. If it already contains sites, add the new site blocks instead of deleting working configuration:

app.example.com {
    encode zstd gzip
    reverse_proxy 127.0.0.1:3000
}

api.example.com {
    reverse_proxy 127.0.0.1:8000
}

Format, validate, and reload:

sudo caddy fmt --overwrite /etc/caddy/Caddyfile
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
sudo journalctl -u caddy -n 100 --no-pager

caddy validate must pass before every reload. The systemd reload applies a valid configuration without intentionally stopping active connections.

5. Verify DNS, HTTPS, headers, and isolation

Test from a machine outside the VPS network:

curl -I http://app.example.com/
curl -I https://app.example.com/
curl -I https://api.example.com/health

HTTP should redirect to HTTPS, the certificate should name the requested host, and each hostname should reach the correct backend. Also prove the backend port is not public:

nc -zv 203.0.113.10 3000

That last connection must fail from a remote machine. On the proxy VPS, curl http://127.0.0.1:3000/ must still succeed.

Caddy sets the forwarding headers applications normally need. Configure the application to trust proxy headers only from the immediate Caddy address; never trust arbitrary internet clients to supply their own client-IP headers. Test login redirects, absolute URLs, uploads, streaming responses, and WebSockets rather than treating a successful home page as complete verification.

6. Proxy to a backend on a private LayerOne network

For a separate application server at 10.10.0.20:8000, use:

internal-app.example.com {
    reverse_proxy 10.10.0.20:8000
}

On the application server, bind the service to 10.10.0.20 and allow TCP 8000 only from the proxy's private address, for example 10.10.0.10/32. Do not open 8000 on either server's public interface. A private network is isolation, not encryption; use HTTPS or another authenticated encrypted protocol between tiers when the data requires it.

7. Logs, capacity, and failure behavior

Read service logs and resource use:

sudo journalctl -u caddy --since today --no-pager
systemctl show caddy -p MemoryCurrent -p CPUUsageNSec
df -h / /var/lib/caddy

Do not enable indefinite per-request logs without rotation and a privacy policy. Access logs can contain addresses, paths, query strings, and user agents. If an application accepts large uploads, align its limits and timeouts deliberately; do not copy a global setting that silently changes every service.

A reverse proxy is also a shared failure point. Keep backend health monitoring outside this VPS, watch certificate errors and 5xx rates, and avoid moving unrelated critical systems behind one tiny proxy without a recovery plan.

8. Back up and update Caddy

Back up /etc/caddy/Caddyfile and any referenced snippets, credentials, or manually managed certificates. Caddy stores managed certificate state under its service data directory, normally /var/lib/caddy; protect that copy as secret material. Certificates can be reissued, but retaining state reduces needless issuance and preserves non-certificate assets.

Upgrade the stable package, validate, and retest every hostname:

sudo apt update
sudo apt install --only-upgrade caddy
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl status caddy --no-pager
curl -I https://app.example.com/

Store a copy off-server and test rebuilding Caddy on a clean VPS. LayerOne does not back up the guest disk; see Backups and snapshots.

Security checklist

  • Keep Caddy and Ubuntu on supported stable packages.
  • Leave the admin API on loopback and never publish TCP 2019.
  • Expose only ports 80 and 443 publicly; bind backends to loopback or a private IP.
  • Restrict SSH to a trusted CIDR or a private management path.
  • Trust forwarded headers only from the proxy address in each application.
  • Do not place credentials in the Caddyfile if a restricted environment file or secret mechanism is available.
  • Add authentication at the application or a deliberately configured identity layer; HTTPS alone does not make an admin panel private.
  • Review access-log retention and avoid logging secrets in URLs.

Official references

Still stuck

Chat with us from the portal.

Ask the assistant from the Chat bar. During business hours you can ask for a person and a human joins live.