Skip to content

Switch to Let's Encrypt

A common lifecycle: you installed with --cert-source self-signed against a bare IP to get going, and now the box has a real DNS name. This guide migrates the running install to a Let’s Encrypt certificate issued by certbot — no reinstall, and your proxy users and instance state survive.

The same steps work if you started from mkcert or a user-supplied certificate; only the starting certSource differs.

Throughout, substitute your own values for:

  • norsk.example.com — the DNS name pointing at the box
  • admin@example.com — the email Let’s Encrypt sends expiry notices to
  • A DNS A record for the domain pointing at the box’s public IP. Verify it resolves before touching anything:
    Terminal window
    dig +short norsk.example.com
  • Inbound ports 80 and 443 open from the internet. Port 443 is already open if the proxy works today; port 80 is the one commonly missing. On EC2 that means an inbound security-group rule for TCP 80 from 0.0.0.0/0 (and ::/0); on other clouds the equivalent firewall rule.

Certbot’s standalone challenge server needs to bind port 80, which the proxy currently holds:

Terminal window
sudo systemctl stop norsk-ctl
docker stop norsk-proxy
Terminal window
sudo apt-get install -y certbot
sudo certbot certonly --standalone --non-interactive --agree-tos \
-m admin@example.com -d norsk.example.com

Success looks like Successfully received certificate, with the files under /etc/letsencrypt/live/norsk.example.com/.

If it fails with Timeout during connect (likely firewall problem), DNS is resolving (the CA reached out to your IP) but port 80 is blocked — fix the security-group / firewall rule and re-run. If it fails with Address already in use, something still holds port 80 — check sudo ss -tlnp | grep :80.

Certificate fields can’t be changed with config set; re-run init with your current settings plus the certbot trio. Check norsk-ctl config show for your current networkMode and defaultWorkingDirectory first.

Terminal window
sudo runuser -l norsk -c '/usr/local/bin/norsk-ctl init --force \
--network-mode docker \
--working-directory /var/norsk-ctl \
--cert-source certbot \
--cert-path /etc/letsencrypt/live/norsk.example.com/fullchain.pem \
--key-path /etc/letsencrypt/live/norsk.example.com/privkey.pem \
--public-host norsk.example.com'

Confirm it landed in the daemon’s config, not a stray home-dir one:

Terminal window
sudo grep -E "certSource|publicHost" /etc/norsk-ctl/config.yaml

Proxy users (htpasswd) and the instance database live alongside the daemon and survive the re-init — you don’t need to re-supply --proxy-user.

Always drive the daemon through systemd on server installs — norsk-ctl shutdown from a shell without the production env fails with Redirected to proxy — stale or missing proxy secret.

Terminal window
sudo systemctl restart norsk-ctl
curl -sI https://norsk.example.com/

You should get a response with no certificate warning (no curl -k needed).

If the proxy container instead crash-loops with a key permission error (docker logs norsk-proxy): /etc/letsencrypt/live and archive are root-only (0700) by default, and the container may run as a non-root user. Open them up:

Terminal window
sudo chmod 755 /etc/letsencrypt/live /etc/letsencrypt/archive
sudo chmod 640 /etc/letsencrypt/archive/norsk.example.com/privkey1.pem

publicHost changed from the IP to the domain, so instances launched before the switch keep advertising the old address until relaunched. Stop and start each from the UI or CLI.

The old self-signed pair is now unused:

Terminal window
sudo rm /etc/norsk-ctl/certs/cert.pem /etc/norsk-ctl/certs/key.pem
sudo tee /etc/letsencrypt/renewal-hooks/pre/norsk-proxy-stop.sh >/dev/null <<'EOF'
#!/bin/sh
docker stop norsk-proxy
EOF
sudo tee /etc/letsencrypt/renewal-hooks/deploy/norsk-proxy-start.sh >/dev/null <<'EOF'
#!/bin/sh
docker start norsk-proxy
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/pre/norsk-proxy-stop.sh \
/etc/letsencrypt/renewal-hooks/deploy/norsk-proxy-start.sh
sudo certbot renew --dry-run

A passing dry run proves the whole chain — challenge reachability, hooks, certificate deployment — before the first real renewal at ~day 60. Certbot only runs the hooks when a renewal is actually due, so the proxy stays up day-to-day; the outage is a few seconds every ~60 days.

  • Security — the four certificate sources and when to pick each
  • Config — every config.yaml field
  • Ports — what each port is for