Skip to content

HTTPS & Domain

The server gets and renews free Let's Encrypt certificates by itself — no certbot, no nginx, no cron jobs. You point a domain at the server, list it in the config, and you're done: certificates are issued at startup and renewed automatically about a month before they expire.

1. Point your domain at the server

Create a DNS A record (and/or AAAA for IPv6) for your domain — say code.example.com — pointing to the VPS's public IP. Verify:

bash
dig +short code.example.com     # must print your server's IP

2. Open ports 80 and 443

Both must be reachable from the internet:

  • 80 — used by Let's Encrypt to verify you own the domain (and later for the http→https redirect). Certificates cannot be issued without it.
  • 443 — the HTTPS port your browser uses.

Check your cloud provider's firewall/security group, plus any local firewall (ufw, firewalld). Also make sure no old nginx/caddy/apache is already sitting on those ports.

3. Turn it on in the config

Edit /etc/code.yaml (or use code-server-setup):

yaml
tlsDomains: ["code.example.com"]   # ← this is the on-switch
tlsEmail: "you@example.com"        # optional: expiry notices
cookieSecure: true                 # required for sign-in over HTTPS

Multiple domains are fine: tlsDomains: ["code.example.com", "c.example.org"] — each gets its own certificate, and each must resolve to this server.

Then restart (or Ctrl+S in the setup TUI):

bash
sudo systemctl restart code-server

4. Verify

Issuing takes a few seconds. Then:

bash
# watch the certificate get issued
sudo journalctl -u code-server | grep -i "tls: certificate"
# expect: tls: certificate ok  host=code.example.com  remaining_days=89

# from anywhere: valid HTTPS answer
curl -sI https://code.example.com | head -1

Open https://code.example.com in a browser — padlock, no warnings. Once that works, optionally set hsts: true to force all traffic onto HTTPS.

How renewal works (nothing to do)

  • Certificates and the Let's Encrypt account key are cached under <dataDir>/cert/cache (by default /var/lib/code/data/cert/cache).
  • A background check runs twice a day and renews any certificate that is within ~30 days of expiry. The new certificate is swapped in live — no restart, no downtime.
  • Restarts and upgrades reuse the cached certificate; nothing is re-issued unless needed.

Keep the cache

The cert cache lives inside dataDir, so it survives upgrades automatically. If you wipe or fail to persist that directory (easy to get wrong with containers), every restart requests fresh certificates and you'll hit Let's Encrypt's rate limit (~5 per week for the same domain), locking you out for days. Persist dataDir. See Run with Docker.

Troubleshooting

SymptomLikely causeFix
Log says the challenge failed; no certificatePort 80 blocked, or DNS not pointing here yetOpen 80/443 in the firewall; check dig +short <domain>; DNS changes can take a few minutes
bind: permission denied on startSomething replaced the packaged service unit (it normally grants the port permission)Reinstall the package, or run on high ports behind a proxy
address already in useOld nginx/caddy still on 80/443Stop/disable it
Browser: TLS/connection error on httpsCertificate not issued yet (or issuing failed)Check the log per step 4; the server refuses HTTPS for a domain until its certificate exists
too many certificates in the logCert cache wasn't persisted; too many re-issuesPersist dataDir, then wait out the rate-limit window
Signed out immediately after signing incookieSecure: true but you're browsing over plain http://Use the https:// URL (or set hsts: true to force it)

No domain? / TLS terminated elsewhere?

  • IP only, no domain: leave tlsDomains empty and use http://<ip>/ with cookieSecure: false. Fine for a quick trial on a trusted network — don't use it over the open internet for real work.
  • Already have a reverse proxy / tunnel doing TLS (nginx, Caddy, frp, Cloudflare): keep tlsDomains empty, set listen to a loopback port like 127.0.0.1:5080, point the proxy at it (WebSocket pass-through required), and keep cookieSecure: true since the public side is HTTPS.