Custom Domain
CP users can attach their own domain name to a deployed service. Once configured, the domain serves the service over HTTPS with an automatically provisioned Let's Encrypt certificate.
How It Works
user domain (e.g. dashboard.example.com)
│ CNAME (DNS only)
▼
cpctl.app
│
▼
public IP (nginx + Let's Encrypt cert, per hostname, auto-provisioned)
│ proxy_pass via internal network
▼
compute cluster
Traefik (routes by Host header via Swarm service labels)
│
▼
Docker Swarm service (e.g. cp-dashboard)User Setup
Add a single DNS record at your domain registrar or DNS provider:
| Name | Type | Target | Proxy |
|---|---|---|---|
dashboard | CNAME | cpctl.app | DNS only (grey cloud) |
No Cloudflare account required. Works with any DNS provider. The record must be DNS only — do not enable proxying.
API
Attach a domain
POST /v1/services/:service/domains
Authorization: Bearer <cp-api-token>
Content-Type: application/json
{
"hostname": "dashboard.koraccel.kr"
}What happens on the server:
- Verifies the CNAME resolves to
cpctl.app - SSHs into the VPS and writes an nginx site config for the hostname
- Runs
certbot --nginxto issue a Let's Encrypt certificate - Reloads nginx
- Updates the Traefik router label on the Swarm service to include the hostname
Response:
{
"hostname": "dashboard.koraccel.kr",
"status": "active",
"service": "cp-dashboard",
"created_at": "2026-07-04T10:52:36.153Z"
}Skip DNS check (for testing):
{ "hostname": "dashboard.koraccel.kr", "skip_dns": true }List domains
GET /v1/services/:service/domains
Authorization: Bearer <cp-api-token>{
"domains": [
{ "hostname": "cp-dashboard.computeportal.io", "status": "active", "created_at": null },
{ "hostname": "dashboard.koraccel.kr", "status": "active", "created_at": null }
]
}Remove a domain
DELETE /v1/services/:service/domains/:hostname
Authorization: Bearer <cp-api-token>Returns 204 No Content. The default <service>.cpctl.app domain cannot be removed.
What happens on the server:
- Removes the nginx site config from the VPS
- Deletes the Let's Encrypt certificate via
certbot delete - Reloads nginx
- Removes the hostname from the Traefik router label
CLI
cpctl domain add <service> <domain> # attach a domain
cpctl domain list <service> # list attached domains
cpctl domain status <service> <domain> # check DNS and TLS status
cpctl domain remove <service> <domain> # detach a domaindomain add
Before running this, add a CNAME record at your DNS provider:
| Name | Type | Target | Proxy |
|---|---|---|---|
dashboard | CNAME | cpctl.app | DNS only (grey cloud) |
Then attach the domain:
cpctl domain add my-service dashboard.example.com✓ Domain dashboard.example.com attached to my-service
domain dashboard.example.com
service my-service
status active
tls pendingTLS is provisioned automatically via Let's Encrypt within a minute or two.
If DNS hasn't propagated yet, use --skip-dns to bypass the CNAME check:
cpctl domain add my-service dashboard.example.com --skip-dnsdomain list
cpctl domain list my-serviceHOSTNAME STATUS
my-service.cpctl.app active
dashboard.example.com activedomain status
cpctl domain status my-service dashboard.example.comdomain dashboard.example.com
status active
tls yes
dns_configured yes
cname_target cpctl.app
created_at 2026-07-01T10:00:00Zdomain remove
cpctl domain remove my-service dashboard.example.comThe default <service>.cpctl.app domain cannot be removed.
MCP Tools
| Tool | Description |
|---|---|
cp_domain_add | Attach a custom hostname to a service |
cp_domain_list | List all hostnames attached to a service |
cp_domain_remove | Detach a custom hostname from a service |
Infrastructure
| Component | Value |
|---|---|
| CNAME target | cpctl.app |
| Edge proxy | Public IP running nginx + certbot |
| Public IP → cluster | Internal private network |
| TLS | Let's Encrypt via certbot HTTP-01, issued per hostname |
| Routing | Traefik Host-header rules on Docker Swarm service labels |
Design Decisions
Why a VPS instead of Cloudflare Tunnel for the entry point? Cloudflare Tunnel records cannot be set to DNS-only while still resolving to an IP. Cross-account proxied CNAMEs trigger Cloudflare Error 1014. Using a VPS with a stable public IP removes any Cloudflare dependency for user domains — users can use any DNS provider and any proxy setting.
Why certbot per hostname instead of a wildcard cert? User domains are arbitrary (any domain, any registrar). A wildcard cert only covers *.cpctl.app. certbot with the nginx plugin issues and renews individual certs automatically.
Why not Cloudflare for SaaS? Requires a paid Cloudflare plan and assumes users stay on Cloudflare. The VPS approach works for any user regardless of their DNS provider.