# Domains, DNS & SSL
URL: https://synquic.tech/docs/troubleshooting/domains-ssl.md

Fix domain verification, DNS propagation, missing routes, certificate issuance and renewal, external ingress, uploads and custom proxy config.

A working custom domain needs DNS that reaches the server (or your own proxy), a route to the right app, and a
certificate on the server or at your proxy. This page explains the failure messages and which part to check.

If you haven't added a domain yet, start with the [custom domains guide](/docs/guides/custom-domains); this page
is for when something is already stuck. The [Domains page](/docs/dashboard/domains) is the quickest overview: it
shows each domain's live DNS answer, certificate state and the reason for the last failure.

<Callout title="How verification works on Synq">
Verifying a domain obtains (or reuses) a valid certificate through ACME (Let's Encrypt). Synq doesn't compare
the public IP with the server's and doesn't check an ownership `TXT` record. With **External ingress & TLS**,
Synq accepts the domain as it is and leaves TLS to your proxy.
</Callout>

## Verification keeps failing

<Callout type="error" title="Symptom / what you see">
You press **Verify** and the live log ends in a failure, or the domain shows a red "Last attempt ..." line on
the Domains page. From the API, `POST /api/domains/:id/verify` returns the failure message; the `cnameVerified`
and `txtVerified` flags belong to hosted-cloud DNS checks and don't mean anything on your own server.
</Callout>

**What it means.** Let's Encrypt couldn't confirm the name through this server. Synq reads certbot's output and
puts the real cause first. The common ones:

| The message says | Cause | Fix |
| --- | --- | --- |
| Port 80 for the domain isn't reachable from the internet | A firewall or cloud security group blocks port 80, the name points elsewhere, or another proxy holds port 80. | Open port 80 to the internet, check the `A` record, and make sure the Synq edge owns port 80 (see [The edge](/docs/guides/the-edge#taking-over-a-server-thats-already-serving)). |
| The domain doesn't resolve to this server yet | The `A` record is missing or hasn't propagated. | Add the record, wait, then retry. |
| Cloudflare's proxy is answering the certificate challenge | Cloudflare redirected the challenge to HTTPS and then refused the server's certificate (errors 525/526). | Set the record to **DNS only** until the first certificate is issued, or set Cloudflare's SSL/TLS mode to **Full** until then, or upload a Cloudflare Origin CA certificate. See [Cloudflare's proxy](/docs/guides/cloudflare-tunnel#cloudflares-proxy-without-a-tunnel). |
| The challenge was redirected from HTTP to HTTPS and then answered with a 404 | Usually Cloudflare's "Always Use HTTPS" redirecting the challenge. | Same as above. Nothing is occupying port 80, so taking over another proxy won't help. |
| Port 80 answered but not with our ACME challenge | Another web server still serves port 80. | Take over or migrate the existing proxy. |
| Let's Encrypt rate limit reached | Too many attempts or certificates for this name. | Wait before retrying. Test with the staging directory next time (see [Certificate authority settings](/docs/guides/https-certificates#certificate-authority-settings)). |
| Couldn't start the certificate challenge listener | A certificate run from an earlier attempt hasn't exited yet. Nothing was requested from Let's Encrypt. | Retry. If it persists, find the process holding the port named in the message with `ss -ltnp` and stop it. |
| Can't reach the server over SSH | The project's server is offline or unreachable. | Bring the server back, then verify again. |

**How to fix, step by step**

1. Check the hostname points at your server: `dig A app.example.com` should return the server's public IP. A
   free subdomain (`<slug>.<domain>`) relies on the wildcard record `*.<domain>` instead.
2. Check for an `AAAA` record: `dig AAAA app.example.com`. Let's Encrypt tries IPv6 first, so an `AAAA` pointing
   elsewhere breaks issuance. Point it at this server or delete it.
3. Make sure port **80** reaches the server from the internet (firewall, security group, NAT).
4. Delete any old, conflicting record for the same name.
5. Press **Verify** again. The **Domain DNS verification** job also retries on its own, less often after each
   failure (see [Automatic retries](/docs/guides/https-certificates#automatic-retries)).

After a failed check, Synq also looks up the hostname's DNS. If it has no record, or answers with an address that
isn't the server serving the project, an Issue titled "app.example.com is not pointing at this server" opens on the
**Monitoring** page with the addresses found, the expected server IP and the record to add. It updates on each
automatic re-check and resolves itself once DNS points here and the certificate is issued. The lookup only explains
a failure; it never blocks verification, so a CDN in front of a working domain is unaffected.

<Callout title="Wildcards and DNS-01" type="info">
A wildcard (`*.example.com`) or a domain with **Use DNS-01 challenge** is verified through your DNS provider, not
port 80. "DNS-01 challenge for ... requires a connected DNS provider (e.g. Cloudflare) in Settings → DNS." means
no provider manages that zone: connect Cloudflare under **Settings → Credentials**. "DNS provider credential
rejected" means Cloudflare refused the stored token: replace it there. See
[Wildcard certificates](/docs/guides/https-certificates#wildcard-certificates-with-a-dns-provider).
</Callout>

<Callout title="Already verified?" type="info">
Verifying a domain that already has a valid certificate with more than 14 days left reuses it and requests
nothing new. That's expected, not an error.
</Callout>

## DNS hasn't propagated yet

<Callout type="error" title="Symptom / what you see">
The records look correct at your registrar, but the Domains page shows **No record yet** or **Points
elsewhere**, and Verify still fails, sometimes minutes after you saved them.
</Callout>

**What it means.** DNS isn't instant. After you save a record it has to spread across the internet's caches.
Usually that takes minutes; occasionally hours (the panel warns it can take up to 48 hours).

**How to fix**

1. Double-check the **Host** field for typos and remove leftover records for the same name.
2. Use **Check DNS** on the Domains page to see what public DNS answers now (it asks Google's public resolver).
   If your own `dig` doesn't show the new value yet either, it's a propagation delay.
3. Wait. The **Domain DNS verification** job (on by default) first retries a pending domain once it is 10
   minutes old, then backs off (15 minutes up to every 6 hours), and issues the certificate once DNS is
   right. It only retries domains of projects with a live deployment. Pressing **Verify** is only the faster
   retry.

<Callout title="What the first deployment does" type="info">
A new custom domain starts **Pending**. The project's first deployment creates the HTTP route and prepares the
server's certificate tools, but doesn't request a certificate yet. Issuance starts once the domain verifies,
from the scheduled job or from **Verify**.
</Callout>

## Verified, but no certificate yet

<Callout type="error" title="Symptom / what you see">
The domain is **Verified**, but its **SSL** state stays on **Awaiting certificate** (**Issuing** on the Domains
page) instead of **Active**, so `https://` doesn't work yet.
</Callout>

**What it means.** Issuance is still running, its result hasn't been read yet, or the first attempt failed.

**How to fix**

1. Give it a moment, then open the domain's **⋯** menu and click **Recheck SSL** (**Recheck certificate** on
   the Domains page). It only reads the certificate on the server, so it spends no Let's Encrypt quota, and flips
   the state to **Active** once the certificate is there.
2. If it says no valid certificate was found yet, wait a little longer and recheck.
3. The scheduled domain job retries incomplete issuance. For an immediate retry, click **Renew SSL**; the real
   failure reason (DNS, blocked ports, rate limit, CA outage) appears there.

<Callout title="“Domain must be verified before SSL can be managed”" type="warn">
Renew, recheck and upload need a **verified** domain. Finish verification first, then manage the certificate.
</Callout>

<Callout title="Verify shouldn't need a redeploy" type="warn">
After a successful **Verify**, the existing route is re-registered with TLS, so no redeploy is needed. If HTTPS
only starts working after a redeploy, keep both the deployment log and the Verify log: it points to a separate
route problem, which **Retry routing** on the project's Domains & Routes tab repairs.
</Callout>

## A certificate didn't renew

<Callout type="error" title="Symptom / what you see">
An **SSL cert expiring** notification arrived, or a certificate on the Domains page shows **Failed**, a red
countdown, or **Expired**.
</Callout>

**What it means.** The daily **SSL certificate renewal** job (`ssl:renew`) renews certificates with fewer than 14
days left. It failed for this one, for the same reasons issuance fails (DNS moved, port 80 closed, a CDN
redirecting the challenge), or the job is turned off.

**How to fix**

1. Open the Certificates tab of the [Domains page](/docs/dashboard/domains#the-certificates-tab). The row shows
   the last error, and the **Automatic renewal** card shows whether the job is on and how its last run went.
2. Fix the cause, then press **Renew certificate** on the row, or **Renew \{n\} due** to renew every due
   certificate at once.
3. If the card says the renewal job is turned off, open it with **Open renewal job** and enable it.

Uploaded certificates (**Auto-renew: Manual**) are never renewed by Synq and send no reminder: upload a new one
before the old one expires.

## Uploading a certificate fails

"Invalid certificate: ..." means the certificate didn't pass the checks Synq runs before touching the edge: it
must parse as PEM, the private key must match it, it must cover the hostname, and it must not be expired. Paste
the **full chain** (your certificate first, then the intermediates) and the key that belongs to it. Nothing
changes on the server when an upload is refused.

## DNS and SSL are fine, but the app returns 404 or 502

A certificate proves the name reaches the server; it doesn't prove the route in front of your app is right.

- A **502** page titled "Application unavailable" means the edge couldn't reach the app: it's restarting, it
  crashed, or it doesn't listen on the mapped port. Check the internal target on the Domains page (for example
  `https://app.example.com -> http://web:3000`) against the port your app listens on, and read the app's logs.
- A **404** page titled "Service not found" means no route exists for that hostname on this server. Use **Retry
  routing** on the project's **Domains & Routes** tab.
- A **503** page "... is not deployed yet" or "... is not live" means the domain is ready but the project has no
  live deployment. Deploy it (or read the failed deploy's log).
- For a Compose project, map the domain to the intended service's container port on **Domains & Routes**. A
  project-level domain without a port is skipped with a warning naming the available ports.
- A static route serves the release directory of the active deployment. A warning that no static root exists
  means the deployment's files need a new successful deployment.

If setup recorded `SYNQ_PUBLIC_URL` as **external ingress**, Synq assumes an existing proxy terminates TLS for
the panel and doesn't set up its own edge for it. Use the setup flow to hand ingress to Synq's edge if that
changes.

## External ingress: your proxy terminates TLS

<Callout type="error" title="Symptom / what you see">
Your app sits behind a Cloudflare Tunnel, a load balancer or another reverse proxy that handles HTTPS, but Synq
asks for an `A` record and tries to issue its own certificate, which you don't want.
</Callout>

**How to fix**

1. Add the domain with **External ingress & TLS** turned on. From the API, pass `"externalIngress": true` on
   `POST /api/domains`.
2. No DNS record is requested or checked. Point DNS at your proxy.
3. Press **Verify**. The domain is verified straight away with the SSL state **External TLS**. Your proxy must
   forward requests to the Synq server on port 80 and serve HTTPS itself.

See [Cloudflare Tunnel and external proxies](/docs/guides/cloudflare-tunnel).

<Callout title="Turn it on only if you mean it" type="warn">
If nothing in front of the server handles HTTPS, leave External ingress off; otherwise the domain verifies but
is served over plain HTTP only, with no certificate from anyone.
</Callout>

## Custom config was rolled back

"The edge rejected this config, so the previous config was restored." means your directives passed Synq's checks
but `openresty -t` or the reload failed. The error underneath is nginx's own, for example a duplicate directive
(often one that is also set in the project's **Routing** card) or an unknown directive. Nothing changed on the
server. Fix the snippet and save again; see [Custom proxy config](/docs/guides/custom-proxy-config).

## The hostname is refused

If **Add domain** is rejected before verification, the hostname failed a basic check:
`Domain "app.example.com" is already in use` (it's attached elsewhere) or `"..." is not a valid public hostname.`
(a bare IP, `localhost`, a single-label name, or a label longer than 63 characters). A free subdomain
(`<slug>.<domain>`) is saved as an ordinary custom domain, so it goes through the same checks.

<Cards>
  <Card title="Custom domains guide" href="/docs/guides/custom-domains" description="Add a domain, point DNS, verify, HTTPS." />
  <Card title="HTTPS and certificates" href="/docs/guides/https-certificates" description="How issuance, uploads, redirects and renewal work." />
  <Card title="Domains page" href="/docs/dashboard/domains" description="Live DNS and certificate state for every domain." />
  <Card title="Domains API reference" href="/docs/api/domains" description="Every domain endpoint and response." />
</Cards>
