Skip to content

Commit cd1eb06

Browse files
docs: Enhance the deployment guide with detailed Cloudflare DNS, Traefik wildcard SSL configuration, and a new troubleshooting section.
1 parent c45073f commit cd1eb06

1 file changed

Lines changed: 186 additions & 29 deletions

File tree

docs/DEPLOYMENT.md

Lines changed: 186 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,54 +23,107 @@ Access the dashboard at `http://your-server-ip:3000`.
2323

2424
### 1.2 DNS Configuration (Cloudflare)
2525

26+
We recommend **Cloudflare** (free tier) for DNS because Traefik's wildcard SSL requires DNS-01 challenge, and Cloudflare has first-class integration. If you're currently on GoDaddy or another registrar, migrate your nameservers to Cloudflare:
27+
28+
1. Create a free account at [dash.cloudflare.com](https://dash.cloudflare.com)
29+
2. **Add your domain** — Cloudflare will scan existing DNS records
30+
3. Cloudflare gives you two nameservers (e.g., `karina.ns.cloudflare.com`, `karl.ns.cloudflare.com`)
31+
4. **Update nameservers at your registrar** (e.g., GoDaddy > My Domains > DNS > Nameservers > Custom) to point to Cloudflare's nameservers
32+
5. Wait for propagation (usually 5-30 minutes)
33+
34+
Once active, configure these DNS records:
35+
2636
| Type | Name | Value | Proxy |
2737
|-------|------|------------------|----------------------|
28-
| A | `@` | `your-server-ip` | Proxied (orange) |
38+
| A | `@` | `your-server-ip` | **DNS only (grey)** |
2939
| A | `*` | `your-server-ip` | **DNS only (grey)** |
30-
| CNAME | `www`| `lmsplatform.com`| Proxied |
40+
| CNAME | `www`| `lmsplatform.com`| DNS only (grey) |
3141

32-
**Critical:** The wildcard `*` record MUST be **DNS only (grey cloud)** so Traefik handles SSL directly. If proxied through Cloudflare, the wildcard cert won't work with Traefik's DNS challenge.
42+
**Critical:** Both `@` and `*` records MUST be **DNS only (grey cloud)** so Traefik handles SSL directly. If proxied through Cloudflare, the wildcard cert won't work with Traefik's DNS challenge. You can enable Cloudflare proxy on the `@` record later once everything is working.
43+
44+
**Clean up old records:** If you migrated from another provider, delete any leftover A records pointing to old IPs (e.g., GoDaddy forwarding IPs). You should have exactly ONE A record per name.
3345

3446
### 1.3 Cloudflare API Token
3547

36-
1. Cloudflare Dashboard > My Profile > API Tokens
37-
2. Create Token > **Edit zone DNS** template
38-
3. Zone Resources: Include your domain
39-
4. Save the token for Traefik configuration
48+
1. Go to [dash.cloudflare.com/profile/api-tokens](https://dash.cloudflare.com/profile/api-tokens)
49+
2. Click **"Create Token"**
50+
3. Use the **"Edit zone DNS"** template
51+
4. Zone Resources: **Include > Specific zone > your domain**
52+
5. Click **Continue to summary > Create Token**
53+
6. **Copy and save the token** — you'll need it for Traefik configuration
54+
55+
> **Security:** Never share this token publicly. If compromised, rotate it immediately from the same page.
4056
4157
---
4258

4359
## 2. Traefik Wildcard SSL
4460

4561
This is what makes multi-tenant subdomains work with HTTPS.
4662

47-
### 2.1 Add Cloudflare Token
63+
### 2.1 Add Cloudflare Token to Traefik
4864

49-
In Dokploy: **Web Server > Traefik > Environment**
65+
In Dokploy: **Web Server** (left sidebar) > scroll to **Web Server** section > click **Traefik** button > select **"Modify Environment"** from the dropdown:
5066

5167
```
5268
CF_DNS_API_TOKEN=your-cloudflare-api-token
5369
```
5470

71+
Save the environment.
72+
5573
### 2.2 Add DNS Challenge Resolver
5674

57-
In Dokploy: **Web Server > Traefik > traefik.yml** — add a DNS challenge resolver alongside the existing one:
75+
In Dokploy: **Traefik File System** (left sidebar) > open the `traefik.yml` file.
76+
77+
Add a `letsencrypt-wildcard` DNS challenge resolver **alongside** the existing `letsencrypt` HTTP challenge resolver. Your full `traefik.yml` should look like:
5878

5979
```yaml
80+
global:
81+
sendAnonymousUsage: false
82+
providers:
83+
swarm:
84+
exposedByDefault: false
85+
watch: true
86+
docker:
87+
exposedByDefault: false
88+
watch: true
89+
network: dokploy-network
90+
file:
91+
directory: /etc/dokploy/traefik/dynamic
92+
watch: true
93+
entryPoints:
94+
web:
95+
address: :80
96+
websecure:
97+
address: :443
98+
http3:
99+
advertisedPort: 443
100+
http:
101+
tls:
102+
certResolver: letsencrypt
103+
api:
104+
insecure: true
60105
certificatesResolvers:
61106
letsencrypt:
62-
# ... existing HTTP challenge config (keep it)
63-
letsencrypt-dns:
64107
acme:
65108
email: you@yourdomain.com
66-
storage: /etc/dokploy/traefik/dynamic/acme-dns.json
109+
storage: /etc/dokploy/traefik/dynamic/acme.json
110+
httpChallenge:
111+
entryPoint: web
112+
letsencrypt-wildcard:
113+
acme:
114+
email: you@yourdomain.com
115+
storage: /etc/dokploy/traefik/dynamic/acme-wildcard.json
67116
dnsChallenge:
68117
provider: cloudflare
69118
resolvers:
70119
- "1.1.1.1:53"
71120
- "8.8.8.8:53"
72121
```
73122
123+
Save the file, then go back to **Web Server** and click **Traefik > Reload** to apply changes.
124+
125+
> **Note:** The `letsencrypt` resolver (HTTP challenge) is used for the root domain. The `letsencrypt-wildcard` resolver (DNS challenge via Cloudflare) is used for `*.yourdomain.com`. Both coexist.
126+
74127
---
75128

76129
## 3. Deploy the Next.js App
@@ -135,31 +188,73 @@ CRON_SECRET=your-random-secret-here
135188
In the app's **Domains** tab:
136189

137190
**Domain 1 — Platform root:**
191+
192+
In the app's **Domains** tab, click **Add Domain**:
138193
- Host: `lmsplatform.com`
194+
- Port: `3000`
139195
- HTTPS: Yes
140-
- Certificate resolver: `letsencrypt` (standard HTTP challenge)
196+
- Certificate Provider: `Let's Encrypt` (standard HTTP challenge)
141197

142198
**Domain 2 — Wildcard subdomains:**
143199

144-
In the app's **Advanced > Traefik** section, add custom labels:
200+
> **Important:** Do NOT add `*.lmsplatform.com` as a regular domain in Dokploy — Traefik does not support literal `*` in `Host()` rules and will error with: `"*.lmsplatform.com" is not a valid hostname`. You must use `HostRegexp` via the Advanced config.
201+
202+
In the app's **Advanced** tab, find the Traefik dynamic configuration. You'll see the existing router config for your root domain. Add wildcard routers alongside it.
203+
204+
Here's what the full Advanced config should look like (replace `YOUR-SERVICE-NAME` with the Dokploy-generated service name visible in the existing config, and `YOUR-APP-CONTAINER` with the container hostname):
145205

146206
```yaml
147-
# HTTP wildcard router
148-
- "traefik.http.routers.lms-wildcard.rule=HostRegexp(`^[a-z0-9-]+\\.lmsplatform\\.com$`)"
149-
- "traefik.http.routers.lms-wildcard.entrypoints=web"
150-
- "traefik.http.routers.lms-wildcard.service=lms-<SERVICE_HASH>-web"
151-
- "traefik.http.routers.lms-wildcard.middlewares=redirect-to-https@docker"
152-
153-
# HTTPS wildcard router
154-
- "traefik.http.routers.lms-wildcard-secure.rule=HostRegexp(`^[a-z0-9-]+\\.lmsplatform\\.com$`)"
155-
- "traefik.http.routers.lms-wildcard-secure.entrypoints=websecure"
156-
- "traefik.http.routers.lms-wildcard-secure.service=lms-<SERVICE_HASH>-web"
157-
- "traefik.http.routers.lms-wildcard-secure.tls.certresolver=letsencrypt-dns"
158-
- "traefik.http.routers.lms-wildcard-secure.tls.domains[0].main=lmsplatform.com"
159-
- "traefik.http.routers.lms-wildcard-secure.tls.domains[0].sans=*.lmsplatform.com"
207+
http:
208+
routers:
209+
# --- Root domain (auto-generated by Dokploy) ---
210+
your-app-router:
211+
rule: Host(`lmsplatform.com`)
212+
service: YOUR-SERVICE-NAME
213+
middlewares:
214+
- redirect-to-https
215+
entryPoints:
216+
- web
217+
your-app-router-websecure:
218+
rule: Host(`lmsplatform.com`)
219+
service: YOUR-SERVICE-NAME
220+
middlewares: []
221+
entryPoints:
222+
- websecure
223+
tls:
224+
certResolver: letsencrypt
225+
226+
# --- Wildcard subdomains (add these manually) ---
227+
lms-wildcard:
228+
rule: HostRegexp(`^[a-z0-9-]+\.lmsplatform\.com$`)
229+
service: YOUR-SERVICE-NAME
230+
middlewares:
231+
- redirect-to-https
232+
entryPoints:
233+
- web
234+
lms-wildcard-secure:
235+
rule: HostRegexp(`^[a-z0-9-]+\.lmsplatform\.com$`)
236+
service: YOUR-SERVICE-NAME
237+
middlewares: []
238+
entryPoints:
239+
- websecure
240+
tls:
241+
certResolver: letsencrypt-wildcard
242+
domains:
243+
- main: lmsplatform.com
244+
sans:
245+
- "*.lmsplatform.com"
246+
247+
services:
248+
YOUR-SERVICE-NAME:
249+
loadBalancer:
250+
servers:
251+
- url: http://YOUR-APP-CONTAINER:3000
252+
passHostHeader: true
160253
```
161254
162-
Replace `<SERVICE_HASH>` with the Dokploy-generated service name (visible in the Traefik dashboard or via `docker inspect`).
255+
**How to find `YOUR-SERVICE-NAME` and `YOUR-APP-CONTAINER`:** Look at the existing config in the Advanced tab — Dokploy auto-generates these when you add the root domain. They follow the pattern `project-name-hash-service-N` (e.g., `guille-personal-lms-lj9e61-service-15`).
256+
257+
After saving, **redeploy** the app. Allow 1-2 minutes for Let's Encrypt to issue the wildcard certificate via DNS challenge.
163258

164259
### 3.5 Cron Jobs
165260

@@ -313,8 +408,70 @@ After deployment, verify:
313408

314409
---
315410

411+
## 8. Troubleshooting
412+
413+
### Wildcard subdomain returns 404
414+
415+
**Symptom:** `school.lmsplatform.com` returns a Traefik 404 page, but `lmsplatform.com` works.
416+
417+
**Causes and fixes:**
418+
419+
1. **Missing wildcard router:** Check that the `lms-wildcard` and `lms-wildcard-secure` routers exist in the Advanced config. Dokploy's regular domain UI does not support wildcards.
420+
421+
2. **Traefik error `"*.domain.com" is not a valid hostname`:** You added `*.domain.com` as a regular domain in Dokploy's Domains tab. Delete it — wildcards must use `HostRegexp` in the Advanced config.
422+
423+
3. **DNS not resolving:** Run `dig +short school.lmsplatform.com` — it should return your server IP. If not, check that the `*` A record exists in Cloudflare and nameservers have propagated.
424+
425+
4. **Wildcard cert not issued:** Check Traefik logs (**Web Server > Traefik > View Logs**) for ACME errors. Common causes:
426+
- `CF_DNS_API_TOKEN` not set in Traefik environment
427+
- Cloudflare API token doesn't have `DNS:Edit` permission on the correct zone
428+
- `letsencrypt-wildcard` resolver not defined in `traefik.yml`
429+
- `acme-wildcard.json` storage path conflict — ensure it's different from the HTTP challenge storage
430+
431+
5. **Cloudflare proxy (orange cloud) enabled on wildcard:** The `*` A record must be **DNS only (grey cloud)**. Cloudflare proxy intercepts TLS and breaks Traefik's DNS challenge.
432+
433+
### Tenant not created after onboarding
434+
435+
**Symptom:** User completes the create-school flow but the subdomain shows "Invalid tenant."
436+
437+
**Cause:** The `create_school` RPC requires an authenticated user (`auth.uid()` must not be null). If the user's session expired or cookies weren't sent, the RPC fails silently on the client.
438+
439+
**Fix:** Check the browser console for RPC errors. The user should log in again and retry the create-school flow. Alternatively, create the tenant manually via SQL:
440+
441+
```sql
442+
-- Create tenant
443+
INSERT INTO tenants (name, slug, status)
444+
VALUES ('School Name', 'school-slug', 'active')
445+
RETURNING id;
446+
447+
-- Add admin user (replace UUIDs)
448+
INSERT INTO tenant_users (tenant_id, user_id, role, status)
449+
VALUES ('tenant-uuid-from-above', 'user-uuid', 'admin', 'active');
450+
```
451+
452+
### SSL certificate not renewing
453+
454+
Traefik auto-renews Let's Encrypt certificates. If renewal fails:
455+
456+
1. Check Traefik logs for ACME errors
457+
2. Verify the Cloudflare API token hasn't been revoked
458+
3. Ensure the `acme-wildcard.json` file is writable
459+
4. Try deleting the `acme-wildcard.json` file and reloading Traefik to force re-issuance
460+
461+
---
462+
316463
## Recommendation
317464

318465
**Start with Supabase Cloud** to avoid managing Postgres backups, Auth configuration, and the custom access token hook. Migrate to self-hosted later once traffic justifies it.
319466

320467
The critical work is in **Sections 2-3**: getting Traefik wildcard SSL + DNS challenge working. Once that's done, subdomain routing works automatically via `proxy.ts`.
468+
469+
---
470+
471+
## References
472+
473+
- [Dokploy Domains Documentation](https://docs.dokploy.com/docs/core/domains)
474+
- [Wildcard SSL in Dokploy — naps62](https://www.naps62.com/posts/wildcard-ssl-in-dokploy)
475+
- [Dokploy Wildcard Subdomain Discussion #3089](https://github.com/Dokploy/dokploy/discussions/3089)
476+
- [Traefik DNS Challenge Providers](https://doc.traefik.io/traefik/https/acme/#providers)
477+
- [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens)

0 commit comments

Comments
 (0)