Domains, DNS & SSL
Fix a custom domain that won't verify, DNS that hasn't propagated, an SSL certificate stuck on Provisioning, and the external-ingress (TXT-only) case.
Adding a custom domain has two moving parts: the DNS records you paste at your registrar, and the verification step where Openship looks those records up and confirms them. Almost every domain problem is really one of those two records not resolving the way Openship expects. This page walks through the exact messages you'll see and what each one means.
If you haven't added a domain yet, start with the custom domains guide — this page is for when something is already stuck.
The two records, in one sentence
Every custom domain needs a routing record (an A or CNAME that points the name at your app) and a
TXT ownership record at host _openship-challenge (a secret code that proves the domain is yours).
Verify checks both.
Verification keeps failing
Symptom / what you see
You click Verify and it doesn't turn green. A toast titled "Verification failed" appears, and its body is the exact reason Openship returned — one or both of:
A record not pointing to server for app.example.com(self-hosted) orCNAME record not found for app.example.com(Openship Cloud)TXT record _openship-challenge.app.example.com must equal "…"
(If Openship can't produce a specific reason it falls back to the generic "{hostname} could not be verified yet.")
From the API, POST /api/domains/:id/verify returns 422 with cnameVerified and txtVerified flags that
tell you which check is still failing.
What it means. Openship ran two independent lookups and at least one came back wrong. The message names the record that failed, so you never have to guess. Fix whichever record is mentioned — the other one is already fine.
| Message you see | Which record | What's wrong |
|---|---|---|
A record not pointing to server for … | Routing (A) — self-hosted | The A record is missing, or points at a different IP than your server. |
CNAME record not found for … | Routing (CNAME) — Openship Cloud | The CNAME is missing or doesn't point at the Cloud target. |
TXT record _openship-challenge.… must equal "…" | Ownership (TXT) | The _openship-challenge TXT record is missing, or its value doesn't match the secret code. |
Why the API says “cnameVerified” even on a self-hosted A record
The verify response reports the routing check as cnameVerified regardless of whether your project uses an
A record (self-hosted) or a CNAME (Cloud). It's the same "does the routing record point the right way?"
check — only the record type differs. txtVerified is always the ownership TXT.
If the routing record is failing
How to fix
- Open your project's Domains tab and reopen the DNS Records panel for the domain (or run
openship domain records <domainId>). - Compare the routing row against what's actually at your registrar. Self-hosted expects an
Arecord with host@pointing at your server's public IP; Cloud expects aCNAMEwith host@pointing at the Openship Cloud target. - Fix any mismatch — copy the value from Openship, don't retype it. Delete any old, conflicting record for the
same name first (a stale
A/CNAMEleft in place is a common cause). - Wait for the change to take effect, then click Verify again.
Self-hosted: the IP has to be reachable
The A record must resolve to the same public IP as the server this project is deployed on. If your server
sits behind a NAT or a proxy that changes its address, the lookup won't match and verification will keep
failing — point the record at the address the outside world actually sees.
If the TXT ownership record is failing
How to fix
- At your registrar, add (or correct) a TXT record with host
_openship-challenge— Openship looks it up at_openship-challenge.<your-domain>. - Set its value to the exact string from the DNS Records panel. Use the Copy button; a single extra space or a missing character will fail the match.
- Save, wait for propagation, and click Verify again.
Already verified?
If you re-run Verify on a domain that's already done, Openship simply replies "Already verified" and changes nothing — that's expected, not an error.
DNS hasn't propagated yet
Symptom / what you see
The records look correct at your registrar, but Verify still reports the routing or TXT record as not found — sometimes minutes after you saved them.
What it means. DNS isn't instant. After you save a record, it has to spread across the internet's caches before Openship's lookup can see it. As the DNS Records panel itself notes: "DNS changes can take up to 48 hours to propagate globally." Usually it's a few minutes; occasionally it's much longer.
How to fix
- Double-check there are no typos in the Host field and no leftover conflicting record for the same name.
- Wait. Save the records, give it several minutes, then click Verify again — re-clicking once DNS resolves is the reliable path.
- Openship also ships a pending-verification sweep (
POST /api/domains/verify-pending) that re-checks still-Pending domains and flips them to Verified on their own. It isn't on by default — it only runs if your operator has wired it to a scheduler (cron / systemd timer) — so don't wait on it; re-click Verify.
Confirm propagation yourself
To check what the world currently sees, look the record up directly — e.g. dig TXT _openship-challenge.app.example.com
or dig A app.example.com. If your own lookup doesn't return the new value yet, Openship won't see it either;
that's a propagation delay, not an Openship problem.
SSL stuck on "Provisioning" (or no certificate issued)
Symptom / what you see
The domain is Verified, but the SSL pill stays on Provisioning instead of moving to Active —
so https:// doesn't work yet.
What it means. Verifying a domain kicks off certificate issuance from Let's Encrypt in the background — the verify response comes back fast and the SSL status catches up on its own. Provisioning means the certificate is still being requested (or a request hasn't finished). It normally clears within a minute or two.
How to fix
- Give it a moment, then open the domain's ⋯ menu and click Recheck SSL. This is a read-only check (no certificate is re-issued, no rate limit is spent) that flips the status to Active as soon as the certificate is actually in place.
- If Recheck reports "No valid certificate found for {hostname} yet. If you just deployed, give Let's Encrypt a moment, then recheck." — the certificate genuinely isn't there yet. Wait a little longer and recheck again.
- If it never provisions, click Renew SSL to force a fresh issuance attempt. Any real failure reason (rate limit, ACME outage) surfaces on that action.
Renewing certificates
Once a certificate is Active you normally never touch it again. Renewal targets certificates within about two weeks of expiry: on Openship Cloud, and on self-hosted setups where your proxy or edge (Traefik, Caddy, a load balancer) owns TLS, this is handled for you; a self-hosted operator can also point a scheduled job at the renew endpoint. Renew SSL is the manual fallback if a certificate ever lapses.
“Recheck / Renew SSL” errors that the domain must be verified
SSL actions require a verified domain — attempting them on a pending one returns "Domain must be verified before SSL can be managed". Finish verification first (above), then manage the certificate.
If SSL was never set up on this host
The certificate toolchain (certbot) is installed during a deploy, not by the on-demand Renew/Recheck buttons. If a host has never provisioned SSL for any custom domain, redeploy the project — the deploy preflight installs the toolchain and issues the certificate as part of the build, with the progress in the deploy logs. See Troubleshooting → Deployments.
External ingress: your edge terminates TLS (TXT only)
Symptom / what you see
Your app already sits behind a Cloudflare Tunnel, a load balancer, or another reverse proxy that handles
HTTPS — but Openship keeps asking for an A/CNAME and tries to issue its own certificate, which you don't
want.
What it means. By default Openship assumes it's the edge: it wants the domain to resolve to it and it issues the certificate. When your edge terminates TLS, that's the wrong model — Openship should only confirm you own the name and stay out of the certificate business.
How to fix
- When adding the domain, turn on External ingress & TLS. As the toggle explains: "TLS terminates
upstream (Cloudflare Tunnel, load balancer). Verify by TXT only — the domain need not point at this server,
and we won't issue a certificate." From the API, pass
"externalIngress": trueonPOST /api/domains. - Add only the
TXTrecord with host_openship-challenge— there's no routing record to add, because your edge already controls where the name points. - Click Verify. Openship checks the TXT alone and confirms with "Domain verified — TLS is handled by your external ingress; no certificate is issued here."
What the SSL pill shows
An external-ingress domain reads External TLS for its SSL status — that's correct and final. Openship issues nothing and never runs certbot for it; your edge owns the padlock. Recheck SSL and Renew SSL don't apply.
Turn it on only if you mean it
If your edge does not handle HTTPS, leave External ingress off — otherwise the domain will verify but serve plain HTTP, with no certificate from anyone. When in doubt, leave it off and let Openship issue the certificate.
Still stuck?
One more thing to check: the hostname itself
If Add domain is rejected before you even reach verification, the hostname failed a basic check — e.g.
Domain "app.example.com" is already in use (it's attached elsewhere) or
"…" is not a valid public hostname. (a bare IP, localhost, or a single-label name). Free *.opsh.io-style
subdomains aren't added here either — those live in the project's public endpoints, not as custom domains.
Custom domains guide
The full add → point DNS → verify → HTTPS walkthrough, click by click.
Domains API reference
Every domain endpoint, the preview/verify responses, and the cnameVerified / txtVerified flags.
Project Domains tab
Where domains, verification status, and the SSL pill live in the dashboard.
Deployment & build failures
The build went red — here's how to read the failure and fix the usual causes, from a wrong build command to a compose deploy that needs a decision.
GitHub connection & repos
Fix an expired or unauthorized GitHub connection, a private repo that won't import, a public repo that should just work, and auto-deploy webhooks that never fire.