Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .env.production.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@ APP_ENV=production
# Server Hostname / Public IP
SERVER_PUBLIC_IP=3.25.150.231

# Enable after this hostname resolves to the server and TCP 443 is allowed.
DOMAIN_NAME=cloudmonitorsite.duckdns.org
COMPOSE_PROFILES=tls

# Production CORS Whitelist
# Matches the server's public IP address and optional domain name
CORS_ORIGINS=http://3.25.150.231,http://localhost
CORS_ORIGINS=https://cloudmonitorsite.duckdns.org,http://3.25.150.231,http://localhost

# Optional External API Base URL for Frontend build
# Leave empty ("") to use same-origin reverse-proxy (/api/* -> backend:8000)
Expand Down
33 changes: 13 additions & 20 deletions Caddyfile
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
# CloudMonitor Automated TLS Reverse Proxy Configuration
# Caddy automatically provisions and renews Let's Encrypt certificates
# for the specified DOMAIN_NAME.
# Nginx owns port 80 and performs the public hostname redirect.
{
auto_https disable_redirects
}

{$DOMAIN_NAME:localhost} {
# Security headers
# Renew via TLS-ALPN-01 on the published port 443.
tls {
issuer acme {
disable_http_challenge
}
}
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Strict-Transport-Security "max-age=86400"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
}

# Backend API routes
handle /api/* {
reverse_proxy backend:8000
}

# Backend Health check
handle /health {
reverse_proxy backend:8000
}

# Frontend React SPA
handle {
reverse_proxy frontend:80
}
# Preserve Nginx API rate limits, /api/health, /prom and lifecycle blocks.
reverse_proxy frontend:80
}
41 changes: 41 additions & 0 deletions docs/HTTPS_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Production HTTPS

Public URL: https://cloudmonitorsite.duckdns.org

Set the DuckDNS A record to the EC2 public IPv4 address (currently 3.25.150.231).
Do not use DuckDNS's automatic browser IP detection: that selects your PC's IP.

In the server's untracked `.env`, set `DOMAIN_NAME=cloudmonitorsite.duckdns.org`
and `COMPOSE_PROFILES=tls`. Add `https://cloudmonitorsite.duckdns.org` to
`CORS_ORIGINS`, preserving any other required origins. Never overwrite existing
database credentials or JWT secrets with the example file.

Caddy publishes TCP 443 and obtains/renews a Let's Encrypt certificate using
TLS-ALPN-01. Keep port 443 reachable and the `caddy_data` volume persistent.
Nginx owns port 80 and redirects the DuckDNS hostname to HTTPS. Loopback and
legacy IP requests remain available for existing deployment/agent compatibility.
Use the HTTPS hostname for browser sign-in and new agent configurations.

All HTTPS routes pass through Nginx, preserving login rate limiting and the
blocked Prometheus lifecycle endpoints. HSTS initially uses a one-day lifetime.
The existing CD workflow reads `COMPOSE_PROFILES` from `.env`, so later deploys
keep Caddy enabled. No paid load balancer or DNS service is required.

Validation:

```sh
docker compose -f docker-compose.prod.yml config --quiet
docker compose -f docker-compose.prod.yml run --rm --no-deps caddy caddy validate --config /etc/caddy/Caddyfile
curl -I http://cloudmonitorsite.duckdns.org/ # 308 to HTTPS
curl -fsS https://cloudmonitorsite.duckdns.org/health
curl -fsS https://cloudmonitorsite.duckdns.org/api/health
curl -fsS https://cloudmonitorsite.duckdns.org/prom/api/v1/targets
```

If EC2 is stopped and started, its non-Elastic public IP may change: update
DuckDNS before expecting certificate renewal or traffic to work. A reboot does
not change the IP. Changing the public hostname also requires updating the
hostname-specific redirect in `frontend/nginx.conf`.

For rollback, remove `COMPOSE_PROFILES=tls` from `.env`, revert the HTTPS commit,
and redeploy. Do not delete any Docker volumes.
22 changes: 19 additions & 3 deletions frontend/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,27 @@
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;
limit_req_status 429;

# Caddy sets X-Forwarded-Proto=https. Keep loopback/IP health checks available.
map "$host:$http_x_forwarded_proto" $https_redirect {
default "";
"cloudmonitorsite.duckdns.org:" "https://cloudmonitorsite.duckdns.org";
"cloudmonitorsite.duckdns.org:http" "https://cloudmonitorsite.duckdns.org";
}

map $http_x_forwarded_proto $forwarded_scheme {
default $scheme;
https https;
}

server {
listen 80;
listen [::]:80;
server_name _;

if ($https_redirect != "") {
return 308 $https_redirect$request_uri;
}

root /usr/share/nginx/html;
index index.html;

Expand Down Expand Up @@ -38,7 +54,7 @@ server {
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-Proto $forwarded_scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 60s;
proxy_connect_timeout 10s;
Expand All @@ -53,7 +69,7 @@ server {
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-Proto $forwarded_scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 60s;
proxy_connect_timeout 10s;
Expand Down Expand Up @@ -81,7 +97,7 @@ server {
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-Proto $forwarded_scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 60s;
proxy_connect_timeout 10s;
Expand Down
Loading