Custom Domains
Attach your own domain names to an app. sh0 routes them through Caddy and obtains the HTTPS certificate on the first visit.
Adding a domain
A domain is attached to one app. The route is pushed to Caddy as soon as the domain is added; sh0 does not wait for DNS.
- Open the app in the dashboard.
- Go to the Domains & SSL tab.
- Click Add Domain and type the name in Domain Name, for example
api.example.com. - Confirm with Add Domain. The tab then shows the DNS record to create (type
A, value: your server IP).
From the CLI, sh0 domains <app> add|list|remove does the same. remove accepts the domain name or its ID and asks for confirmation unless --yes is given:
$ sh0 domains my-api add api.example.com
[ok] Added domain api.example.com to my-api
$ sh0 domains my-api remove api.example.com --yes
[ok] Removed api.example.com from my-apiDNS configuration
At your DNS provider, point the name at the public IP address of the server running sh0 with an A record. The dashboard shows that IP under DNS Configuration, with a copy button.
api.example.com. A <your server IP>For a subdomain, a CNAME to another name that already resolves to the server works just as well: what matters is that the request reaches this machine on ports 80 and 443.
HTTPS certificates
Caddy, the reverse proxy managed by sh0, obtains certificates through ACME (Let's Encrypt, with ZeroSSL as fallback). There is nothing to configure, but the behaviour is worth knowing:
- On demand. The certificate is requested at the first HTTPS handshake for the name, not when you add the domain. Until someone visits
https://api.example.com, the status stays Pending. - Only for names sh0 routes. Before issuing, Caddy asks sh0 whether a route carries the name; any other name presented in a TLS handshake is refused, so nobody can make your server request certificates for arbitrary names.
- HTTP to HTTPS. Plain HTTP requests are redirected to HTTPS by Caddy.
- Renewal. Caddy renews certificates on its own before they expire.
The Domains & SSL tab re-reads the status every 10 seconds while a domain is pending. Through the API, a pending domain carries an ssl_pending_reason field that says why, for example: “No TLS handshake for this name has reached this server yet: certificates are issued on demand at the first visit. Check that DNS points here, then open the site once over HTTPS.”
Behind a CDN (Cloudflare proxy)
When a CDN terminates TLS in front of your server (Cloudflare with the orange cloud, for instance), the HTTP-01 validation request stops at the CDN and the certificate may never be issued. Three ways out:
- DNS only (grey cloud): the simplest; Caddy then talks to visitors directly.
- DNS-01 validation, per domain: Caddy proves ownership by writing a TXT record through the Cloudflare API instead of answering over HTTP.
- A custom certificate, such as a Cloudflare origin certificate, uploaded as described below.
DNS-01 needs a Caddy build that carries the Cloudflare module (sudo sh0 install-dns-caddy, then restart sh0) and a Cloudflare API token with Zone:DNS:Edit in the dashboard settings. It is switched on per domain through the API only; the dashboard has no control for it yet. On a server without the module, the call is refused (actual response):
$ sudo sh0 install-dns-caddy # then restart sh0
$ curl -X PUT https://panel.example.com/api/v1/apps/<app-id>/domains/<domain-id>/tls-challenge \
-H "Authorization: Bearer $SH0_TOKEN" -H "Content-Type: application/json" \
-d '{"challenge":"dns-01"}'
{"error":{"code":"VALIDATION_ERROR","message":"This server's Caddy carries no DNS provider module, so DNS-01 cannot be used. Run `sh0 install-dns-caddy` and restart sh0; until then this domain would keep HTTP-01 and the setting would be a lie."}}sh0 also refuses DNS-01 when no token is configured, and when the domain is outside the zone the token controls: the challenge could never be answered there. "challenge":"http-01" switches back.
Custom certificates
To use a certificate from your own authority instead of ACME, use the SSL Certificates section of the same tab:
- Either Generate CSR (the private key is created and kept on the server) and submit the CSR to your CA, or Upload Certificate with the certificate and its private key in PEM format, plus the intermediate chain if your CA provides one.
- Once the signed certificate is uploaded, switch the domain to Use Custom SSL.
- Use Auto SSL returns to automatic certificates. Deleting a certificate also returns the domains that used it to automatic mode. Renewing a custom certificate is up to you.
Rules and limits
- No wildcards.
*.example.comis rejected (“Domain labels must contain only alphanumeric characters and hyphens”). Add each subdomain explicitly. - Workers cannot have a domain. An app of kind worker serves no HTTP port; switch it to web first.
- One owner per name. A name already attached to a file storage instance is refused.
- Normalised. Names are trimmed and lower-cased before being stored; each label is 1 to 63 characters, letters, digits and hyphens.
- Permissions. Adding, removing a domain or changing its validation requires the developer role on the app's project.
Troubleshooting
If the SSL Status stays Pending:
- Read
ssl_pending_reasonfromGET /api/v1/apps/<id>/domains: it names the cause sh0 sees. - Check that the name resolves to the server's public IP (
dig +short). - Check that ports 80 and 443 are open in every firewall in front of the server, including your cloud provider's.
- Open the site once over HTTPS: without a first handshake, no certificate is requested.
- Behind a CDN, use one of the three options above.
$ dig +short api.example.com
$ curl -I http://api.example.com