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:
dig +short code.example.com # must print your server's IP2. 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):
tlsDomains: ["code.example.com"] # ← this is the on-switch
tlsEmail: "you@example.com" # optional: expiry notices
cookieSecure: true # required for sign-in over HTTPSMultiple 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):
sudo systemctl restart code-server4. Verify
Issuing takes a few seconds. Then:
# 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 -1Open 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
| Symptom | Likely cause | Fix |
|---|---|---|
| Log says the challenge failed; no certificate | Port 80 blocked, or DNS not pointing here yet | Open 80/443 in the firewall; check dig +short <domain>; DNS changes can take a few minutes |
bind: permission denied on start | Something 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 use | Old nginx/caddy still on 80/443 | Stop/disable it |
| Browser: TLS/connection error on https | Certificate 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 log | Cert cache wasn't persisted; too many re-issues | Persist dataDir, then wait out the rate-limit window |
| Signed out immediately after signing in | cookieSecure: 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
tlsDomainsempty and usehttp://<ip>/withcookieSecure: 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
tlsDomainsempty, setlistento a loopback port like127.0.0.1:5080, point the proxy at it (WebSocket pass-through required), and keepcookieSecure: truesince the public side is HTTPS.