Skip to content

Domain and SSL

Griffen Fargo edited this page Jul 10, 2026 · 3 revisions

Domain and SSL

Configure custom domains and SSL/TLS certificates for strut stacks using Let's Encrypt.

strut supports two reverse-proxy backends, selected by the REVERSE_PROXY setting in strut.conf (default nginx):

  • nginx (default) — runs configure-domain.sh + certbot on the VPS.
  • caddy — updates the stack's Caddyfile; Caddy obtains and renews certificates automatically. See Caddy Backend below.

Quick Setup

strut my-stack domain api.example.com admin@example.com --env prod

With the default nginx backend, this single command:

  1. Validates DNS resolves to the correct VPS IP
  2. Generates production-ready nginx reverse proxy config
  3. Obtains a Let's Encrypt certificate via certbot (webroot validation)
  4. Configures HTTPS with HTTP→HTTPS redirect
  5. Adds security headers (HSTS, X-Frame-Options, etc.)
  6. Sets up auto-renewal cron (daily at 3 AM)

(With the caddy backend the flow is simpler — see Caddy Backend.)

HTTP Only (No SSL)

strut my-stack domain api.example.com admin@example.com --skip-ssl --env prod

Automatic Provisioning on Deploy (since v0.32.0)

strut <stack> domain above is the manual, one-time setup command. Separately, strut can auto-provision certs as part of every deploy, without you running domain at all — useful when domains are declared alongside the stack itself rather than configured out-of-band.

After a successful deploy and health check, strut looks for a domain in this order:

  1. Compose label strut.domain on any service
  2. DOMAIN env var (single domain)
  3. DOMAINS env var (comma-separated list)
  4. VIRTUAL_HOST env var (nginx-proxy convention, comma-separated)

If found, and AUTO_SSL isn't explicitly disabled, strut checks whether a valid cert already exists (skips if it has more than 30 days left), verifies the domain actually resolves to the VPS IP (skips with a warning on mismatch — never fights your DNS), then runs certbot certonly --webroot (falling back to --standalone if the webroot isn't writable).

# .env or strut.conf
AUTO_SSL=true            # default
SSL_EMAIL=admin@example.com   # required — no email means auto-SSL silently skips
DOMAIN=api.example.com
# Opt out for a stack that manages its own certs
AUTO_SSL=false

This complements, but doesn't replace, the manual domain command — use whichever fits how you already declare domains for a given stack. Both end up in the same place (a certbot-issued cert renewed on the usual cadence).

Prerequisites

1. DNS A Record

api.example.com  A  <VPS_IP>  TTL=300

Verify: dig +short api.example.com

2. Firewall Ports 80 + 443

Your cloud provider's firewall must allow inbound TCP on ports 80 and 443.

3. Stack Deployed with nginx Running

strut my-stack deploy --env prod
strut my-stack status --env prod   # Verify nginx is running

SSL Certificate Details

Property Value
Provider Let's Encrypt (free, trusted)
Validity 90 days
Auto-renewal Daily cron, renews when <30 days remain
Validation Webroot (no downtime)
TLS versions 1.2 and 1.3 only

Test Renewal

sudo certbot renew --dry-run

Check Expiry

strut my-stack exec \
  "docker compose --project-name prod exec nginx openssl s_client \
   -connect localhost:443 -servername api.example.com < /dev/null 2>/dev/null \
   | openssl x509 -noout -dates" --env prod

Multiple Domains

strut my-stack domain api.example.com admin@example.com --env prod
strut my-stack domain api2.example.com admin@example.com --env prod

Wildcard Certificates

The automated script uses webroot validation (no wildcard support). For wildcards:

sudo certbot certonly --manual --preferred-challenges dns -d "*.example.com"

Custom nginx Configuration

After running the domain command, customize:

nano stacks/<stack>/nginx/conf.d/<stack>.conf
strut my-stack exec "docker compose --project-name prod restart nginx" --env prod

Caddy Backend (Automatic HTTPS)

If your stack uses Caddy as its reverse proxy, set REVERSE_PROXY=caddy in strut.conf. Caddy provisions and renews Let's Encrypt certificates automatically — there is no certbot, no webroot validation, and no renewal cron to manage.

The same command drives the Caddy flow:

strut my-stack domain api.example.com admin@example.com --env prod

With the caddy backend, the command:

  1. Updates the stack's Caddyfile on the VPS with a site block for the domain (replacing the # :80 { placeholder with api.example.com {)
  2. Reloads Caddy in place (caddy reload) — no downtime
  3. Pulls the updated Caddyfile back to stacks/<stack>/caddy/Caddyfile
  4. Commits and pushes the change (unless --skip-ssl is passed)

Caddy then obtains the certificate on first request and renews it on its own.

Caddyfile location

The stack's Caddyfile lives at stacks/<stack>/caddy/Caddyfile locally (synced to the VPS). Customize site blocks there, then re-run the domain command or reload Caddy:

nano stacks/<stack>/caddy/Caddyfile
strut my-stack exec \
  "docker compose --project-name prod exec -T caddy caddy reload --config /etc/caddy/Caddyfile" --env prod

--skip-ssl with Caddy

Because Caddy handles ACME natively, --skip-ssl does not disable TLS for the caddy backend — it only skips the automatic git commit/push of the updated Caddyfile. To serve plain HTTP with Caddy, configure an http:// site block in the Caddyfile directly.

System-wide Caddy (a shared gateway proxying multiple stacks) is managed separately — see Gateway Management and Certificate Management.

Security Features (Auto-Configured)

  • HSTS (1 year, includeSubDomains)
  • TLS 1.2+ only, no weak ciphers
  • X-Frame-Options, X-Content-Type-Options headers
  • HTTP→HTTPS redirect for all traffic
  • Optimized timeouts

Test your configuration: https://www.ssllabs.com/ssltest/

Troubleshooting

DNS Not Resolving

dig +short your-domain.com
# Wait for DNS propagation (5-60 minutes)

Port 80/443 Not Accessible

curl -I http://your-domain.com        # Test externally
sudo ufw status                        # Check VPS firewall
docker ps | grep nginx                 # Check nginx running

Certificate Obtainment Failed

sudo journalctl -u certbot
# Common: port 80 blocked by cloud firewall, DNS wrong, rate limit

HTTPS Not Working

strut my-stack logs nginx --follow --env prod
sudo ls -la /etc/letsencrypt/live/your-domain.com/
docker exec <nginx-container> nginx -t

Clone this wiki locally