# Custom domains, DNS & SSL
URL: https://synquic.tech/docs/guides/custom-domains.md

Put your app on your own address like app.example.com, or on a free subdomain of your install, with automatic HTTPS. Add the domain, point one DNS record at your server, verify, done.

import { Step, Steps } from 'fumadocs-ui/components/steps';

This guide puts an app on a name **you** own, like `app.example.com`, with the padlock (HTTPS) turned on
automatically.

A **domain** is the name people type to reach your app. **DNS** is the internet's phone book: it connects a
name like `app.example.com` to the server running your app. **HTTPS** is what makes the address start with
`https://` and keeps traffic private. Synq handles HTTPS for you; your job is to add one record to your
domain's DNS.

<Callout title="Free subdomains need no DNS work per app" type="info">
Any app can also get a free `<app>.<apps domain>` address. With an apps domain (or the panel's own domain),
one wildcard record `*.<domain>` covers every app; an install reached only by its IP address uses
`<app>.<ip-with-dashes>.sslip.io`, which needs no DNS at all. See [Free subdomains](#free-subdomains). The rest
of this guide is for names outside your apps domain.
</Callout>

<Callout title="What you need first">
- A project in Synq. It doesn't have to be deployed yet, but HTTPS only starts once it is.
- For your own domain: a domain you own, and access to its **DNS settings** (at your registrar, or at
  Cloudflare, Route 53 and so on).
- Ports **80 and 443** on the server open to the internet. Port 80 is how Let's Encrypt checks the domain.
</Callout>

You can add domains in two places that do the same thing:

- the project's **Domains & Routes** tab (this guide), which also offers **Include www**, **External ingress &
  TLS** and **Use DNS-01 challenge**;
- the **Domains** page in the sidebar, whose **Add domain** wizard walks through route, DNS and HTTPS for any
  project (see [Domains page](/docs/dashboard/domains#add-a-domain-with-the-wizard)).

## Free subdomains

Every app can get a free address `<app>.<apps domain>` without any DNS work per app. A new project deploys on one
by default (`synq deploy`, the deploy wizard, one-click apps), so the first deploy on a fresh install already has
an address. Synq picks the **apps domain** in this order:

| | Apps domain | Apps get | DNS you need |
| --- | --- | --- | --- |
| 1 | The one set on the server: `--apps-domain` when installing, or `sudo synq apps-domain set <domain>` (`SYNQ_APPS_BASE_DOMAIN`) | `my-app.apps.example.com` | `*.apps.example.com` pointing at the server |
| 2 | The one set in the panel: **Settings**, **Instance**, **Apps domain** (used while the server sets none) | `my-app.apps.example.com` | `*.apps.example.com` pointing at the server |
| 3 | The panel's own domain (installed with `--domain panel.example.com`) | `my-app.panel.example.com` | `*.panel.example.com` pointing at the server |
| 4 | The server's public IPv4 address through [sslip.io](https://sslip.io) | `my-app.203-0-113-7.sslip.io` | None: sslip.io answers every such name with the IP inside it |
| 5 | None (the server has only an IPv6 or a private address, and no apps domain) | No free subdomain | Set an apps domain, or use a custom domain |

When adding a domain, choose **Free subdomain** and type the label (`my-app`); Synq shows the full name and,
when the apps domain needs one, the wildcard record. The label may contain lowercase letters, digits and
hyphens, up to 63 characters. On `deploy.sh` installs the apps domain is your `DOMAIN`, so apps get
`my-app.example.com` while the panel is on `app.example.com`. See also
[your apps' domains](/docs/getting-started/installation#your-apps-domains).

A free subdomain is saved as an ordinary custom domain, so it gets its own Let's Encrypt certificate over
HTTP-01 on its first deploy, sslip.io names included. Until a certificate is issued, or when issuance fails (for
example on Let's Encrypt's rate limits), the app is served over plain HTTP at `http://<name>`; the deploy log and
the domain's card say so, and **Verify** on the domain retries.

With no apps domain and no public IPv4 address (case 5), an app deploys without a public address of its own and
the deploy log says why. Reach it through a custom domain or a published port, or set an apps domain.

### Set or change the apps domain

- **When installing**: `curl -sSL https://synquic.tech/install.sh | sh -s -- --key <KEY> --apps-domain apps.example.com`.
  The installer prints the wildcard record and warns (without failing) if it doesn't resolve yet.
- **On the server**: `sudo synq apps-domain` shows the domain in use, the record it needs and whether that record
  points at the server; `sudo synq apps-domain set apps.example.com` and `sudo synq apps-domain clear` change it
  and re-apply Synq.
- **In the panel**: instance administrators set it in **Settings**, **Instance**, **Apps domain**, which shows the
  domain in use, where it comes from, the record to add and whether it points here (**Check DNS**). While the
  server sets one (`SYNQ_APPS_BASE_DOMAIN`), the field is read-only and names the command to change it.

Changing the apps domain doesn't move existing apps. Their free subdomains are ordinary domains, so
`my-app.<old domain>` keeps its route and certificate for as long as its DNS points at the server. New free
subdomains use the new domain. To move an app, add a free subdomain on the new domain on its **Domains & Routes**
tab, make it primary, then remove the old one.

An app still carrying a free subdomain this server can't serve (saved by an older version) has it moved onto
the apps domain on its next deploy, or removed when there is no apps domain; the deploy log says which.

<Callout title="A wildcard DNS record covers one level" type="info">
`*.apps.example.com` covers `my-app.apps.example.com` but not `a.b.apps.example.com`. When a free subdomain
doesn't resolve, the domain's issue names the missing wildcard record rather than a record per app.
</Callout>

## Add your own domain

<Steps>

<Step>

### Open the project's Domains & Routes tab

Open the project and choose **Domains & Routes** in the project menu. Each existing domain is a card showing
**Primary**, **Mapped to** (where the edge sends the traffic inside the project, for example
`http://web:3000`), any **Redirect**, the **Status** and the **SSL** state.

</Step>

<Step>

### Add the domain

Press **Add domain**. In the **Add domain** form ("Add a route: pick a free or custom domain and the port it
maps to."):

- choose **Custom domain** (or **Free subdomain**, above) and type the **Domain name**, for example
  `app.example.com`;
- **Maps to port**: the port your app listens on inside its container. Synq prefills the project's port.
  A static site shows **Serves path** instead: the folder of your build output to serve for this domain
  (`/` for the root). A new path takes effect on the next deploy.

Three switches follow for a custom domain:

- **Include www**: also add `www.app.example.com`. It becomes a domain of its own, with its own DNS record,
  verification and certificate, and by default it sends visitors to the bare domain with a `301`
  (see [Redirect one domain to another](#redirect-one-domain-to-another)).
- **Use DNS-01 challenge**: issue and renew the certificate through a connected DNS provider instead of over
  port 80. Required for a wildcard like `*.example.com`. See
  [Wildcard certificates](/docs/guides/https-certificates#wildcard-certificates-with-a-dns-provider).
- **External ingress & TLS**: turn this on **only** if your own proxy already handles HTTPS for this name
  (a Cloudflare Tunnel, Nginx Proxy Manager, a load balancer). See [DNS setup, two ways](#dns-setup-two-ways).

Press the **Add domain** button at the bottom of the form to save it.

<Callout title="www needs its own DNS record" type="warn">
Because `www.example.com` is a separate domain, it needs a separate DNS record; Synq lists it next to the bare
domain's. Skip it and `www` stays **Pending**: it can't get a certificate.
</Callout>

</Step>

<Step>

### Create the DNS record

A **DNS Records** panel lists what to create at your DNS provider. Each row has a **Type**, a **Name / Host**
and a **Value / Target**, with a copy button. On a Synq server it's a single record per hostname:

| Type | Name / Host | Value / Target |
| --- | --- | --- |
| `A` | `app` (the part before your domain), or `@` for the bare domain | your server's public IP |

There is no `TXT` ownership record on a Synq server: you own the server, so issuing the certificate is the
proof (see [Verify](#verify)).

Registrars word things differently; you may see "Host", "Name" or "@". `@` means the domain itself. If the
panel shows no IP ("Synq couldn't detect this server's public IP"), use the address you connect to the server
with.

<Callout title="Let Synq create the record" type="info">
If a DNS provider is connected (Cloudflare, under **Settings → Credentials**) and it manages this domain's zone,
the panel offers **Auto-configure DNS**. It shows what it will **Add**, **Update** or **Repoint**, and writes the
records only when you press it. Records it didn't create show as **Conflict**: remove them or set the record by
hand.
</Callout>

</Step>

<Step>

### Verify

Once the record exists, press **Verify \{hostname\}** under the records (or the **Verify** button on the domain's card). A
log window streams the certificate request live. Verifying a domain on a Synq server means getting its
certificate from Let's Encrypt: if Let's Encrypt can reach the name on port 80, the certificate is issued, the
domain turns **Verified**, and **SSL** turns **Active**. The log stays on screen whatever the outcome; use
**Copy log** to keep it and **Try again** to re-run.

If the project hasn't deployed yet, the first deploy creates the route; verification and the certificate
follow. Until the project has a live deployment, a verified domain shows a "not deployed yet" page (see
[What visitors see](/docs/guides/https-certificates#what-visitors-see-before-an-app-is-live)).

<Callout title="DNS is not instant" type="info">
New records usually resolve within minutes, occasionally hours. You don't have to keep clicking: the **Domain
DNS verification** job retries pending domains on its own, first after 10 minutes and then less often after
each failure (up to every 6 hours), once the project has a live deployment. **Verify** is the immediate,
explicit retry. See [Automatic retries](/docs/guides/https-certificates#automatic-retries).
</Callout>

</Step>

<Step>

### HTTPS is on

Your app is now at `https://app.example.com`. Plain `http://` requests are redirected to HTTPS from now on,
and the certificate renews itself 14 days before it expires. See
[HTTPS and certificates](/docs/guides/https-certificates).

</Step>

</Steps>

## Domain details and retries

A domain that isn't healthy shows its status pill with **Show why**. The **Domain details** panel explains the
reason in plain words (for example "HTTPS needs checking on the serving server." or "This route is waiting for a
deployment. Deploy the project to apply it."), the number of failed attempts, **Last check**, **Next scheduled
check**, and a **Retry now** button.

## Retry routing with logs

If the app is live but the routes in front of it didn't sync, a **Domain routing needs attention** banner
appears with **Retry routing**. It is also in the page actions and in a domain's menu; a domain card whose
backing route is missing offers it directly. It repairs the project's saved routes on the server and then
checks pending domains and HTTPS again.

The log viewer shows progress and the reason for any failure, including SSH and certificate errors. Working
domains keep their state when another domain fails, and existing valid certificates are reused. Closing the
viewer stops watching; an accepted repair continues on the server.

## DNS setup, two ways

<Tabs items={['Pointing at your server (A record)', 'External ingress & TLS']}>

<Tab value="Pointing at your server (A record)">

The normal case. The domain points straight at the server that runs the project.

| Type | Host | Value |
| --- | --- | --- |
| `A` | `@` or the subdomain | your server's public IP |

The `A` record is guidance: Synq doesn't compare public DNS with the server IP. What matters is that Let's
Encrypt can reach the name on port 80 (or, with DNS-01, that your DNS provider can publish the challenge).
A CDN proxy in front is fine as long as it forwards port 80; the simplest path is to keep the proxy off until
the certificate is issued.

</Tab>

<Tab value="External ingress & TLS">

For setups where **your own proxy already terminates HTTPS**: a Cloudflare Tunnel, Nginx Proxy Manager, a load
balancer or another reverse proxy in front of Synq. Turn on **External ingress & TLS** when adding the domain.

Synq requests and checks no DNS record, and issues no certificate. The edge serves the domain as plain HTTP on
port 80 for your proxy to forward to. **Verify** marks the domain verified straight away and says that TLS is
handled by your external ingress; the SSL state reads **External TLS**.

Your proxy must forward requests for the hostname to the Synq server (port 80) with the original `Host` header.
For a homelab behind Nginx Proxy Manager, point your internal DNS name at NPM and let NPM own HTTPS. Visitors
outside your network still need a public address, VPN or tunnel. See
[Cloudflare Tunnel and external proxies](/docs/guides/cloudflare-tunnel).

</Tab>

</Tabs>

## Redirect one domain to another

Any domain on a project can send its visitors to another domain on the same project instead of serving the app.
Synq answers a `301` (or `302`, if you pick it) and keeps the rest of the URL, so
`https://www.example.com/pricing?ref=x` lands on `https://example.com/pricing?ref=x`.

On the project's **Domains & Routes** tab press **Edit domains**, and set **Redirect** on the domain you want
to send *away*. Pick the destination (**→ \{hostname\}**, only this project's own domains are offered) and
**301 permanent** or **302 temporary**. Choose **Serve the app here** to turn a redirect off again.

The two common uses:

- **Pick a canonical hostname.** `www.example.com` → `example.com`, or the reverse. **Include www** sets up
  `www` → bare domain for you, and you can flip it. Without a redirect both names serve the same app, which
  search engines treat as duplicate content.
- **Move to a new domain.** Keep the old one verified and redirect it to the new one, so links and bookmarks
  keep working.

<Callout title="A redirecting domain still needs DNS and a certificate" type="info">
It has to answer `https://` before it can redirect anyone, so it goes through the same setup as any other
domain and keeps its own **Status** and **SSL**.
</Callout>

Synq refuses a redirect to a domain outside the project (that would turn your domain into an open redirect)
and a loop (`A` → `B` → `A`). For redirecting **paths** (for example `/old-blog` → `/blog`), use the project's
**Redirects** entry; see [Redirects, basic auth and security rules](/docs/guides/edge-rules).

## Bring your own certificate

For a certificate from somewhere else, most often a Cloudflare Origin CA certificate for **Full (strict)**
mode, open the verified domain's menu and choose **Upload certificate**. Synq then serves that certificate and
stops running Let's Encrypt for the domain; renew it by uploading a new one. Full details:
[Upload your own certificate](/docs/guides/https-certificates#upload-your-own-certificate).

## Other domain actions

The domain card's **⋯** menu holds **Edit route** (on a service's route), **Set as primary** (the address the
project card and **Visit** link use), **Renew SSL**, **Recheck SSL** (read the certificate on the server without
requesting a new one), **Upload certificate** and **Remove route**. Which of them appear depends on the domain's
state; an unverified domain shows **Verify** on the card instead. **Edit domains** at the top changes the port
(or static path) and redirect of every project-level domain at once.

Advanced per-domain settings live on the Domains page: **Custom config** adds your own OpenResty directives
to one domain (see [Custom proxy config](/docs/guides/custom-proxy-config)).

## CLI

Every step above has a CLI equivalent. `add` prints the DNS records and the domain's ID; use that ID to verify.

```bash
# See exactly which records a domain will need (nothing is saved)
synq domain preview app.example.com

# Add the domain to a project (prints the records and the domain ID)
synq domain add app.example.com --project <projectId>

# Once your DNS is in place, verify it (this requests the certificate)
synq domain verify <domainId>

# List a project's domains and their status
synq domain list --project <projectId>
```

`synq domain records <domainId>` reprints the records for an existing domain, `synq domain primary <domainId>`
makes a domain the project's main address, and `synq domain renew <domainId>` / `synq domain verify-ssl
<domainId>` manage the certificate. See the [`synq domain` reference](/docs/cli/reference/domain) and the
[Domains API](/docs/api/domains).

## Troubleshooting

<Callout title="Verify keeps failing" type="warn">
The log names the real cause: the name doesn't resolve to this server, port 80 is blocked, or another proxy
answered the challenge. Check that the `A` record points at the server (`dig A app.example.com`), that port 80
is open in the firewall and any cloud security group, that no `AAAA` record points elsewhere, and that a CDN
proxy in front is off or forwards port 80. Then press **Verify** again. The
[troubleshooting page](/docs/troubleshooting/domains-ssl) explains each message.
</Callout>

<Callout title="DNS is not propagating" type="error">
Usually it's just time. A frequent cause of "never propagates" is a typo in the host, or an old conflicting
record left in place. Delete any stale record for the same name before adding the new one. The **Check DNS**
action on the Domains page shows what public DNS answers right now.
</Callout>

<Callout title="Verified, but SSL says “Awaiting certificate”" type="warn">
The certificate is still being issued, or its result hasn't been read yet. Give it a minute, then open the
domain's menu and click **Recheck SSL**: a read-only check that flips the state to **Active** once the
certificate is in place. If it never arrives, use **Renew SSL** to try again; the error names the reason.
</Callout>

<Callout title="The bare domain works but www doesn't (or the other way round)" type="warn">
They're two separate domains with separate records, verification and certificates. Whichever one is still
**Pending** is missing its DNS record: add it, then press that domain's own **Verify**.
</Callout>

<Callout title="An Issue says the domain isn't pointing at this server" type="warn">
When a check fails because the domain's DNS doesn't reach the server that serves the project (no `A`/`AAAA`/`CNAME`
at all, or an address that isn't this server's), Synq opens one Issue for that domain on the **Monitoring** page:
"app.example.com is not pointing at this server". It shows what DNS answered, the server IP the project is served
from, and the exact record to add, and it notifies through your **Domain verification failed** notification
setting. It's refreshed by the automatic background checks (no extra polling) and closes itself, with an all-clear,
once DNS points here and the certificate is issued. Click the Issue to jump to the domain on the **Domains** page, or
**Check again** after fixing the record. A CDN such as Cloudflare in front is fine: a domain that already serves
HTTPS never gets this Issue.
</Callout>

## Related guides

<Cards>
  <Card title="Domains page" href="/docs/dashboard/domains" description="Every domain across projects, with live DNS and certificate state." />
  <Card title="HTTPS and certificates" href="/docs/guides/https-certificates" description="HTTP-01, wildcards, uploads, redirects and renewal." />
  <Card title="Redirects, basic auth and security rules" href="/docs/guides/edge-rules" description="Protect or redirect paths at the edge." />
  <Card title="Domains API reference" href="/docs/api/domains" description="Every domain endpoint with request shapes." />
</Cards>
