Skip to content

Custom Domains & SSL

Satusky supports custom domains with automatic TLS certificates from Let’s Encrypt. This guide walks through both approaches: letting Satusky manage your DNS (simplest) and bringing your own DNS provider.

  • A running deployment (1ctl deploy list shows status completed)
  • A domain you own
  • Your deployment ID from 1ctl deploy list

If your domain is delegated to Satusky’s nameservers, DNS is handled automatically by external-dns. Pass --domain when deploying:

Terminal window
1ctl deploy --domain api.myapp.com --image myapp:latest --machine compute-main-01

The CLI reports success and shows your custom domain as the app URL. external-dns picks up the annotation on the public routing resource and creates the DNS record automatically. Skip to Verifying HTTPS below.


Use this when your domain is managed through Cloudflare, Route 53, Namecheap, or any other DNS provider.

Step 1: Find your cluster’s LoadBalancer IP

Section titled “Step 1: Find your cluster’s LoadBalancer IP”
Terminal window
1ctl cluster zones

Example output:

ZONE LABEL CLUSTER
---- ----- -------
my-kul-1b MY — Kuala Lumpur (KUL-1B) kul

This shows which zones are available, but does not display the LoadBalancer IP directly. To find the IP, look at any existing public route in your namespace:

Terminal window
1ctl ingress list

Example output:

DOMAIN INGRESS ID DEPLOYMENT ID DNS CONFIG CREATED
─────────────────────── ──────────────────────────────────── ──────────────────────────────────── ────────── ────────────
frontend.satusky.com a604088b-d3c7-4d91-8348-b02904164d5e c16a1454-8e1e-4b95-b2cb-b761c96188ab default yesterday
backend-api.satusky.com a9bc0999-1d12-4176-b275-7188df69a856 7f1fab9e-5f87-4612-b306-3da846b95d18 default 23 hours ago

The ADDRESS column shown by kubectl get gateway on a production cluster holds the LoadBalancer IP (e.g., 10.110.153.235 for my-kul-1b). Contact your Satusky administrator or check the cluster documentation for the public IP assigned to your zone’s gateway controller.

In your DNS provider, add:

TypeNameValueTTL
Aapi.myapp.com<loadbalancer_ip>300

DNS propagation can take a few minutes up to 48 hours depending on your TTL settings.

Terminal window
1ctl deploy --domain api.myapp.com --image myapp:latest --machine compute-main-01

The CLI accepts the --domain flag and reports the custom domain as your app’s live URL. On a production cluster with Cloudflare integration, this also updates the public routing resource with your domain and sets the required external-dns annotations. Without that integration, the default *.satusky.com hostname continues to work and the custom domain will not route traffic until DNS and the routing rule are both in place.

The issuer tells cert-manager to obtain a Let’s Encrypt certificate for your domain:

Terminal window
1ctl issuer create \
--deployment-id <your-deployment-id> \
--environment staging

Use --environment staging first to test without hitting Let’s Encrypt rate limits. Staging certificates are not trusted by browsers but behave identically for testing. Switch to --environment production once you have confirmed the flow works end-to-end.

Terminal window
1ctl issuer list

Once the issuer shows Ready, cert-manager is configured. The certificate itself is provisioned when the ACME HTTP-01 challenge completes (cert-manager temporarily serves a token at http://api.myapp.com/.well-known/acme-challenge/<token>). This requires DNS to be fully propagated before certificate provisioning can succeed.


Once DNS has propagated and the certificate is provisioned:

Terminal window
curl -I https://api.myapp.com

Expected output:

HTTP/2 200
server: nginx
...

You can also check the certificate:

Terminal window
curl -sv https://api.myapp.com 2>&1 | grep "SSL certificate verify"
# SSL certificate verify ok.

  1. 1ctl issuer create creates a cert-manager Issuer resource in your namespace.
  2. When the public route is annotated with cert-manager.io/issuer: <app>-letsencrypt-nginx, cert-manager picks it up.
  3. cert-manager initiates an ACME HTTP-01 challenge: it temporarily serves a token at http://api.myapp.com/.well-known/acme-challenge/<token>.
  4. Let’s Encrypt fetches that URL to verify you control the domain.
  5. On success, cert-manager receives the signed certificate and stores it as a Kubernetes Secret named <app>-letsencrypt-nginx-private-key-tls.
  6. The Gateway reads the Secret and begins serving HTTPS.
  7. cert-manager automatically renews the certificate before it expires (Let’s Encrypt certificates are valid for 90 days).

To attach an additional domain to an existing deployment, re-run deploy with the new --domain flag, or delete and recreate the public route:

Terminal window
1ctl ingress delete --deployment-id <id>
1ctl deploy --domain www.myapp.com --image myapp:latest --machine compute-main-01

Then create an issuer for the new domain:

Terminal window
1ctl issuer create \
--deployment-id <id> \
--environment production

Note: 1ctl ingress supports list and delete subcommands. There is no upsert subcommand — use 1ctl deploy --domain to update the domain associated with a deployment.


Certificate stuck in “pending”: DNS hasn’t propagated yet. Check with dig api.myapp.com — it should return the LoadBalancer IP. cert-manager cannot complete the HTTP-01 challenge until the domain resolves publicly.

issuer create returns an error: cert-manager is not installed on the cluster, or the backend cannot reach the Kubernetes API to create the Issuer resource. Confirm cert-manager is running (kubectl get pods -n cert-manager) and that your Satusky backend has the necessary RBAC permissions.

curl returns a self-signed certificate warning: cert-manager is still provisioning the certificate. Wait a few minutes and retry. If it persists, run kubectl describe certificate -n <your-namespace> to see the cert-manager status and any ACME challenge errors.

HTTP-01 challenge fails: Your domain must be publicly reachable on port 80 for the ACME challenge. Ensure there is no firewall blocking inbound HTTP traffic to the LoadBalancer IP.

Wrong IP in A record: Check the ADDRESS column of kubectl get gateway -n <your-namespace> to confirm the correct LoadBalancer IP for your zone’s gateway controller.

cluster zones does not show a LoadBalancer IP: The 1ctl cluster zones command lists zone names and labels only. The LoadBalancer IP is a cluster-level networking detail — find it from kubectl get gateway (ADDRESS column) or ask your Satusky administrator.