Skip to content

Reverse proxy

When you deploy Laju Go, the app listens on a port (default 8080) and speaks plain HTTP — no encryption. If a user visits http://your-server-ip:8080, their browser connects directly to your server and everything (passwords, session cookies, form data) travels over the internet in plaintext. Anyone on the path can read it.

You need HTTPS (https://your-domain.com — the lock icon). That requires three things the app doesn’t do itself:

  1. TLS certificate — proves your server is really your-domain.com and encrypts traffic. Browsers refuse to show the lock icon without it.
  2. Listen on port 443 (HTTPS) instead of 8080 — browsers default to 443 for https:// URLs. Port 8080 is non-standard; users would have to type :8080 manually.
  3. A way to get the certificate and renew it — certs expire every 90 days (Let’s Encrypt) or are managed for you (Cloudflare).

A reverse proxy sits between the internet and your app, handles all three, and forwards traffic to your app on port 8080:

User's browser
│ HTTPS (port 443, encrypted)
Reverse proxy (TLS cert, port 443)
│ HTTP (port 8080, local only)
Laju Go (Go binary, port 8080)

The app never sees the internet directly — the proxy terminates TLS, then forwards the decrypted request locally. Your app stays simple (no cert management, no TLS config), and the proxy handles the parts it’s good at.

Which one? If your domain is on Cloudflare, use that — it’s the proxy, nothing to install. Otherwise, Caddy (auto-TLS via Let’s Encrypt) or Nginx + Certbot both work. Details below.

Laju Go listens on a single port (default 8080). It does not terminate TLS — that is the proxy’s job. Whether you run Docker or bare-metal, the proxy sits in front and forwards to 127.0.0.1:8080.

Internet → [Cloudflare edge | Caddy | Nginx] → 127.0.0.1:8080
└→ Laju Go (Go binary)

Pick one option. All three work with both Docker and bare-metal VPS deployments.

Section titled “Option A — Cloudflare (recommended, nothing to install)”

Your domain is on Cloudflare, so the edge handles TLS and proxying; the server only runs the Go binary. Two routes are available — A1 (Proxied DNS + Origin Rule) is recommended as the default: no extra software, no open ports beyond what Cloudflare uses, and the Origin Rule ensures the app sees the correct Host header for CSRF and OAuth. A2 (Tunnel) is the alternative when you want zero inbound ports at all.

A1. Proxied DNS + origin rule (no extra software)

Section titled “A1. Proxied DNS + origin rule (no extra software)”

Cloudflare proxies traffic to your server and handles TLS. No software to install on the server — just DNS and dashboard settings.

Step 1 — DNS record:

  1. Cloudflare Dashboard → DNS → Records.
  2. Add an A record: name your-domain.com (or @), content = your server IP, proxy status Proxied (orange cloud).
    • “Proxied” means traffic goes through Cloudflare’s edge (TLS, DDoS protection, caching). “DNS only” (grey cloud) just resolves the IP and exposes your server directly — don’t use that.
  3. (Optional) Add a www CNAME pointing to your-domain.com, also proxied.

Step 2 — SSL/TLS mode:

Dashboard → SSL/TLS → Overview → set mode to Flexible:

  • Flexible (recommended) — Cloudflare terminates TLS at the edge, talks HTTP to your origin on port 8080. No cert needed on the server. This is the simplest setup and works well for boilerplate apps like Laju Go. Safe because the firewall only allows Cloudflare IPs (see Firewall below).
  • Full (optional) — edge→origin over HTTPS. You need a cert on the origin (Cloudflare Origin CA cert, free from Dashboard → SSL/TLS → Origin Server → Create Certificate). More secure for sensitive apps, but more setup. Skip this unless you have a specific reason.

Multiple domains? Flexible mode is per-zone (per-domain) in Cloudflare, so each domain you point at the same server gets its own TLS at the edge — no extra cert management. Just add another A record and Origin Rule per domain. The same Laju Go instance serves all of them on port 8080.

Step 3 — Origin Rule (set this up now, don’t wait for it to break):

Cloudflare normally passes the original Host header through, but some configurations (CNAME setups, partial proxy, Cloudflare Workers) can rewrite it. Laju Go’s CSRF check compares the Origin header against Host, and OAuth redirect URIs depend on the correct host — so a rewritten Host header breaks both silently. Set up the Origin Rule upfront:

  1. Dashboard → Rules → Origin RulesCreate rule.
  2. Name: Rewrite host for laju-go.
  3. If incoming requests match: Hostname equals your-domain.com.
  4. Rewrite Host header to: your-domain.com.
  5. Deploy.

This guarantees the app sees your-domain.com as the Host on every request — CSRF passes, OAuth callbacks resolve correctly, and redirect URLs point to the right domain. It’s one rule that prevents a class of bugs that are hard to debug because they only manifest on specific Cloudflare configurations.

Step 4 — Firewall: restrict port 8080 to Cloudflare IPs only (see Firewall below) so nobody can bypass the edge and hit your origin directly.

A2. Cloudflare Tunnel (alternative — zero open ports)

Section titled “A2. Cloudflare Tunnel (alternative — zero open ports)”

Nothing to install except cloudflared:

Terminal window
# on the server
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared
chmod +x /usr/local/bin/cloudflared
cloudflared tunnel login # one-time browser auth
cloudflared tunnel create laju-go
# run the tunnel as a service pointing at 127.0.0.1:8080
cloudflared service install

Dashboard → Zero Trust → Networks → Tunnels → add a public hostname your-domain.com → service http://127.0.0.1:8080. No firewall rules needed — the tunnel connects outbound.

Host header still matters. Even through a tunnel, set the Origin Rule (A1 Step 3) so CSRF and OAuth see the correct Host. Without it, some tunnel configurations pass localhost:8080 as the host, which breaks CSRF validation silently.

Caddy handles TLS automatically via Let’s Encrypt.

/etc/caddy/Caddyfile:

your-domain.com {
reverse_proxy 127.0.0.1:8080
}
Terminal window
sudo systemctl enable --now caddy

Caddy passes the Host header through by default, so CSRF and OAuth work without extra configuration.

Option C — Nginx + Certbot (no Cloudflare)

Section titled “Option C — Nginx + Certbot (no Cloudflare)”

/etc/nginx/sites-available/laju-go:

server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
}
}
Terminal window
sudo ln -s /etc/nginx/sites-available/laju-go /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d your-domain.com

The proxy_set_header Host $host; line is critical — without it, Laju Go’s CSRF check sees the proxy’s internal address instead of the real domain, and all POST requests fail with a 403.

Only Cloudflare IPs may reach the app port — this prevents anyone from bypassing the edge and hitting your origin directly. The command below fetches the current IP list once from Cloudflare’s API and creates persistent UFW rules. It does not run per-request; the rules survive reboots. But Cloudflare’s ranges do change, so set up a weekly refresh (see below).

One-time setup:

Terminal window
sudo ufw allow OpenSSH
# Fetch live Cloudflare IP ranges (IPv4) and create UFW rules
curl -s https://api.cloudflare.com/client/v4/ips | jq -r '.result.ipv4_cidrs[]' | \
xargs -I{} sudo ufw allow from {} to any port 8080
sudo ufw enable

Weekly refresh — Cloudflare adds new IP ranges occasionally. Create /etc/cron.weekly/refresh-cloudflare-ufw:

#!/bin/bash
# Remove old Cloudflare rules for port 8080, re-add from live API
ufw status numbered | grep '8080' | awk -F'[][]' '{print $2}' | sort -rn | \
xargs -I{} ufw delete {}
curl -s https://api.cloudflare.com/client/v4/ips | jq -r '.result.ipv4_cidrs[]' | \
xargs -I{} ufw allow from {} to any port 8080
Terminal window
sudo chmod +x /etc/cron.weekly/refresh-cloudflare-ufw

Simpler alternative: use A2 (tunnel) — no inbound ports at all, no firewall rules to maintain.

Terminal window
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Port 8080 must NOT be exposed publicly — only 80/443 reach the app.

If running Docker, map the port to localhost only so the proxy can reach it but the outside world cannot:

docker-compose.yml
ports:
- "127.0.0.1:8080:8080"

Then apply the firewall rules above as if it were bare-metal.