ilumin-cli 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -17
- package/dist/commands/mcp-add.d.ts.map +1 -1
- package/dist/commands/mcp-add.js +14 -8
- package/dist/commands/mcp-add.js.map +1 -1
- package/dist/commands/skills-install.d.ts +14 -0
- package/dist/commands/skills-install.d.ts.map +1 -0
- package/dist/commands/skills-install.js +40 -0
- package/dist/commands/skills-install.js.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/install-skills.d.ts +15 -0
- package/dist/utils/install-skills.d.ts.map +1 -0
- package/dist/utils/install-skills.js +59 -0
- package/dist/utils/install-skills.js.map +1 -0
- package/package.json +7 -3
- package/skills/ilumin-cloud/SKILL.md +217 -0
- package/skills/ilumin-cloud/references/compose.md +425 -0
- package/skills/ilumin-cloud/references/recommended-stack.md +179 -0
- package/skills/ilumin-security/SKILL.md +355 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# Ilumin Compose Formatter
|
|
2
|
+
|
|
3
|
+
This skill defines the mandatory rules and patterns for writing `docker-compose.yml` files compatible with the Ilumin Cloud platform. Follow every rule — small mistakes like a fixed router name or a wrong network definition are the most common causes of deployment failures.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## How Ilumin's Infrastructure Works
|
|
8
|
+
|
|
9
|
+
Understanding the infrastructure prevents 90% of errors:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
[Internet] → [Traefik v3 on port 80/443] → [Docker containers via labels]
|
|
13
|
+
↑
|
|
14
|
+
Reads routing rules from labels
|
|
15
|
+
Handles SSL via Let's Encrypt
|
|
16
|
+
Routes by hostname or path
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- Traefik runs as a separate container on the `traefik` **external** Docker network.
|
|
20
|
+
- All app containers that need public access must join the `traefik` network.
|
|
21
|
+
- Containers that must stay private (databases, caches) must join only the `internal` network.
|
|
22
|
+
- Containers on the same `internal` network communicate using the **service name** as hostname (e.g., `postgres:5432`).
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Mandatory Formatting Rules
|
|
27
|
+
|
|
28
|
+
### 1. No Comments
|
|
29
|
+
Remove **all** comments (lines starting with `#`) from the final compose file.
|
|
30
|
+
|
|
31
|
+
### 2. No Resource Limits
|
|
32
|
+
Remove any `deploy`, `resources`, `limits`, or `reservations` sections — Ilumin manages these at the infrastructure level.
|
|
33
|
+
|
|
34
|
+
### 3. Image Version Variable
|
|
35
|
+
Replace the image tag of the **main application** with `${APP_VERSION}`.
|
|
36
|
+
|
|
37
|
+
```yml
|
|
38
|
+
# ❌ Wrong
|
|
39
|
+
image: n8n:latest
|
|
40
|
+
image: ghost:5.82.2
|
|
41
|
+
image: myapp:v2.1.0
|
|
42
|
+
|
|
43
|
+
# ✅ Correct
|
|
44
|
+
image: n8n:${APP_VERSION}
|
|
45
|
+
image: ghost:${APP_VERSION}
|
|
46
|
+
image: myapp:${APP_VERSION}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
> **Critical:** `APP_VERSION` replaces the **entire tag** after the colon, including any `v` prefix. Never write `v${APP_VERSION}`.
|
|
50
|
+
|
|
51
|
+
### 4. Database Password Variable
|
|
52
|
+
Replace **all** database passwords with `${DB_PASSWORD}`:
|
|
53
|
+
|
|
54
|
+
```yml
|
|
55
|
+
# Applies to: POSTGRES_PASSWORD, MYSQL_ROOT_PASSWORD, MYSQL_PASSWORD, MARIADB_ROOT_PASSWORD, etc.
|
|
56
|
+
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
|
57
|
+
- MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
|
|
58
|
+
- MYSQL_PASSWORD=${DB_PASSWORD}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 5. Optional Admin Credentials
|
|
62
|
+
If the app supports initial admin setup, use these standard variables:
|
|
63
|
+
- `${APP_USERNAME}` — admin username
|
|
64
|
+
- `${APP_PASSWORD}` — admin password
|
|
65
|
+
- `${APP_EMAIL}` — admin email
|
|
66
|
+
|
|
67
|
+
### 6. No Backslashes in URLs
|
|
68
|
+
Never use `\` before `$` in URLs inside the compose file.
|
|
69
|
+
|
|
70
|
+
```yml
|
|
71
|
+
# ❌ Wrong
|
|
72
|
+
- APP_URL=https://\${BASE_DOMAIN}
|
|
73
|
+
|
|
74
|
+
# ✅ Correct
|
|
75
|
+
- APP_URL=https://${BASE_DOMAIN}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Network Rules (Critical)
|
|
81
|
+
|
|
82
|
+
```yml
|
|
83
|
+
# ✅ Main app service (public-facing) — both networks
|
|
84
|
+
networks:
|
|
85
|
+
- traefik
|
|
86
|
+
- internal
|
|
87
|
+
|
|
88
|
+
# ✅ Dependency services (db, redis, queue) — internal only
|
|
89
|
+
networks:
|
|
90
|
+
- internal
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Always define networks at the bottom of the file exactly like this:**
|
|
94
|
+
|
|
95
|
+
```yml
|
|
96
|
+
networks:
|
|
97
|
+
traefik:
|
|
98
|
+
external: true
|
|
99
|
+
internal:
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
> ⚠️ The `traefik` network must be `external: true`. Forgetting this causes Traefik to be unable to discover the container.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Traefik Labels (Critical)
|
|
107
|
+
|
|
108
|
+
Every public-facing service needs these labels. **Copy this block exactly** and replace `<appname>` with a unique identifier for the app.
|
|
109
|
+
|
|
110
|
+
```yml
|
|
111
|
+
labels:
|
|
112
|
+
- traefik.enable=true
|
|
113
|
+
- traefik.docker.network=traefik
|
|
114
|
+
- traefik.http.routers.<appname>.rule=Host(`${BASE_DOMAIN}`)${CUSTOM_DOMAIN:+ || Host(`${CUSTOM_DOMAIN}`)}
|
|
115
|
+
- traefik.http.routers.<appname>.entrypoints=websecure
|
|
116
|
+
- traefik.http.routers.<appname>.tls=true
|
|
117
|
+
- traefik.http.routers.<appname>.tls.certresolver=letsencrypt
|
|
118
|
+
- traefik.http.services.<appname>.loadbalancer.server.port=<PORT>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Router Naming Rules (Most Common Error)
|
|
122
|
+
|
|
123
|
+
The router/service name in Traefik labels **must be unique across all apps on the server**. Using generic names like `backend` or `frontend` causes conflicts when multiple apps are deployed.
|
|
124
|
+
|
|
125
|
+
**Formula:** `{appname}{role}` — always prefix with the app name.
|
|
126
|
+
|
|
127
|
+
```yml
|
|
128
|
+
# ❌ WRONG — will conflict with any other app using the same generic name
|
|
129
|
+
- traefik.http.routers.backend.rule=...
|
|
130
|
+
- traefik.http.routers.frontend.rule=...
|
|
131
|
+
|
|
132
|
+
# ✅ CORRECT — unique per app
|
|
133
|
+
- traefik.http.routers.n8nmain.rule=...
|
|
134
|
+
- traefik.http.routers.ghostblog.rule=...
|
|
135
|
+
- traefik.http.routers.myappfrontend.rule=...
|
|
136
|
+
- traefik.http.routers.myappbackend.rule=...
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Domain Rule Syntax
|
|
140
|
+
|
|
141
|
+
The domain rule must use this exact syntax — it supports both the Ilumin base domain and an optional custom domain:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
Host(`${BASE_DOMAIN}`)${CUSTOM_DOMAIN:+ || Host(`${CUSTOM_DOMAIN}`)}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
> **Why this syntax?** `${CUSTOM_DOMAIN:+ || Host(...)}` is a shell parameter expansion that only adds the custom domain rule if the variable is set. This allows the platform to manage domain routing dynamically without requiring a compose file change.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Multi-Service Compose Rules
|
|
152
|
+
|
|
153
|
+
### Two Services, One Domain (Path Routing — Frontend + Backend)
|
|
154
|
+
|
|
155
|
+
When your app has a frontend and a backend on the **same domain**:
|
|
156
|
+
|
|
157
|
+
```yml
|
|
158
|
+
# Frontend: answers on / (no path prefix needed)
|
|
159
|
+
labels:
|
|
160
|
+
- traefik.http.routers.myappfrontend.rule=Host(`${BASE_DOMAIN}`)${CUSTOM_DOMAIN:+ || Host(`${CUSTOM_DOMAIN}`)}
|
|
161
|
+
- traefik.http.services.myappfrontend.loadbalancer.server.port=3000
|
|
162
|
+
|
|
163
|
+
# Backend: answers on /api
|
|
164
|
+
labels:
|
|
165
|
+
- traefik.http.routers.myappbackend.rule=(Host(`${BASE_DOMAIN}`)${CUSTOM_DOMAIN:+ || Host(`${CUSTOM_DOMAIN}`)}) && PathPrefix(`/api`)
|
|
166
|
+
- traefik.http.services.myappbackend.loadbalancer.server.port=8000
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Two Services, Two Subdomains
|
|
170
|
+
|
|
171
|
+
When you need separate subdomains for backend and frontend:
|
|
172
|
+
|
|
173
|
+
```yml
|
|
174
|
+
# Frontend — base domain
|
|
175
|
+
labels:
|
|
176
|
+
- traefik.http.routers.myappfrontend.rule=Host(`${BASE_DOMAIN}`)${CUSTOM_DOMAIN:+ || Host(`${CUSTOM_DOMAIN}`)}
|
|
177
|
+
|
|
178
|
+
# Backend — api subdomain prefix
|
|
179
|
+
labels:
|
|
180
|
+
- traefik.http.routers.myappbackend.rule=Host(`api.${BASE_DOMAIN}`)${CUSTOM_DOMAIN:+ || Host(`api.${CUSTOM_DOMAIN}`)}
|
|
181
|
+
|
|
182
|
+
# Other services follow the same prefix pattern:
|
|
183
|
+
# db.${BASE_DOMAIN}, admin.${BASE_DOMAIN}, etc.
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## HTTP → HTTPS Redirect (Optional but Recommended)
|
|
189
|
+
|
|
190
|
+
To redirect HTTP traffic to HTTPS, add these labels alongside the main router:
|
|
191
|
+
|
|
192
|
+
```yml
|
|
193
|
+
labels:
|
|
194
|
+
# ... (main HTTPS router labels above) ...
|
|
195
|
+
|
|
196
|
+
# HTTP redirect router
|
|
197
|
+
- traefik.http.routers.<appname>-http.rule=Host(`${BASE_DOMAIN}`)${CUSTOM_DOMAIN:+ || Host(`${CUSTOM_DOMAIN}`)}
|
|
198
|
+
- traefik.http.routers.<appname>-http.entrypoints=web
|
|
199
|
+
- traefik.http.routers.<appname>-http.middlewares=redirect-to-https
|
|
200
|
+
- traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Architecture Options
|
|
206
|
+
|
|
207
|
+
### Option 1: Unified Build (Recommended) ✅
|
|
208
|
+
|
|
209
|
+
Serve the compiled frontend through the backend. One container, no CORS issues.
|
|
210
|
+
|
|
211
|
+
- Build the frontend in Stage 1 of the Dockerfile.
|
|
212
|
+
- Copy `dist/` into the backend Stage 2.
|
|
213
|
+
- Backend serves static files from `/` and API from `/api`.
|
|
214
|
+
- Only one Traefik router needed.
|
|
215
|
+
|
|
216
|
+
### Option 2: Separate Containers
|
|
217
|
+
|
|
218
|
+
Use path routing or subdomains. Requires proper CORS config on the backend.
|
|
219
|
+
|
|
220
|
+
> The browser **cannot** access `http://backend:8000` — the Docker internal network is not accessible from the user's browser. The frontend must call the public domain (e.g., `/api` relative path or `https://api.domain.com`).
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Common Errors and Fixes
|
|
225
|
+
|
|
226
|
+
| Error | Cause | Fix |
|
|
227
|
+
|---|---|---|
|
|
228
|
+
| App unreachable (502 Bad Gateway) | Wrong port in `loadbalancer.server.port` | Set the port the container actually listens on |
|
|
229
|
+
| App unreachable (no SSL / cert error) | Missing `tls=true` or `certresolver=letsencrypt` | Add both TLS labels |
|
|
230
|
+
| Two apps conflict (first works, second doesn't) | Duplicate router names in Traefik labels | Use unique names: `{appname}{role}` |
|
|
231
|
+
| Database not reachable from app | DB container not on `internal` network | Ensure both app and DB are on `internal` |
|
|
232
|
+
| Traefik can't discover container | `traefik` network not set as `external: true` | Fix the network definition at the bottom |
|
|
233
|
+
| URL contains `v${APP_VERSION}` | Manually adding `v` before the variable | Remove the `v` — `APP_VERSION` already includes it |
|
|
234
|
+
| URL contains backslash `\$` | Escaping `$` in compose | Remove `\` — dollar signs don't need escaping in compose YAML |
|
|
235
|
+
| Custom domain not working | `manage_domain` not called after install | Call `manage_domain` and configure CNAME at DNS provider |
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Reference Examples
|
|
240
|
+
|
|
241
|
+
### Minimal Single App (Uptime Kuma)
|
|
242
|
+
|
|
243
|
+
```yml
|
|
244
|
+
services:
|
|
245
|
+
uptime-kuma:
|
|
246
|
+
image: louislam/uptime-kuma:${APP_VERSION}
|
|
247
|
+
volumes:
|
|
248
|
+
- uptime_data:/app/data
|
|
249
|
+
networks:
|
|
250
|
+
- traefik
|
|
251
|
+
- internal
|
|
252
|
+
labels:
|
|
253
|
+
- traefik.enable=true
|
|
254
|
+
- traefik.docker.network=traefik
|
|
255
|
+
- traefik.http.routers.uptimekuma.rule=Host(`${BASE_DOMAIN}`)${CUSTOM_DOMAIN:+ || Host(`${CUSTOM_DOMAIN}`)}
|
|
256
|
+
- traefik.http.routers.uptimekuma.entrypoints=websecure
|
|
257
|
+
- traefik.http.routers.uptimekuma.tls=true
|
|
258
|
+
- traefik.http.routers.uptimekuma.tls.certresolver=letsencrypt
|
|
259
|
+
- traefik.http.services.uptimekuma.loadbalancer.server.port=3001
|
|
260
|
+
restart: unless-stopped
|
|
261
|
+
|
|
262
|
+
volumes:
|
|
263
|
+
uptime_data:
|
|
264
|
+
|
|
265
|
+
networks:
|
|
266
|
+
traefik:
|
|
267
|
+
external: true
|
|
268
|
+
internal:
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### App + Database (WordPress + MySQL)
|
|
272
|
+
|
|
273
|
+
```yml
|
|
274
|
+
services:
|
|
275
|
+
wordpress:
|
|
276
|
+
image: wordpress:${APP_VERSION}
|
|
277
|
+
depends_on:
|
|
278
|
+
- mysql
|
|
279
|
+
environment:
|
|
280
|
+
- WORDPRESS_DB_HOST=mysql
|
|
281
|
+
- WORDPRESS_DB_USER=wordpress
|
|
282
|
+
- WORDPRESS_DB_PASSWORD=${DB_PASSWORD}
|
|
283
|
+
- WORDPRESS_DB_NAME=wordpress
|
|
284
|
+
volumes:
|
|
285
|
+
- wordpress_data:/var/www/html
|
|
286
|
+
networks:
|
|
287
|
+
- traefik
|
|
288
|
+
- internal
|
|
289
|
+
labels:
|
|
290
|
+
- traefik.enable=true
|
|
291
|
+
- traefik.docker.network=traefik
|
|
292
|
+
- traefik.http.routers.wordpress.rule=Host(`${BASE_DOMAIN}`)${CUSTOM_DOMAIN:+ || Host(`${CUSTOM_DOMAIN}`)}
|
|
293
|
+
- traefik.http.routers.wordpress.entrypoints=websecure
|
|
294
|
+
- traefik.http.routers.wordpress.tls=true
|
|
295
|
+
- traefik.http.routers.wordpress.tls.certresolver=letsencrypt
|
|
296
|
+
- traefik.http.services.wordpress.loadbalancer.server.port=80
|
|
297
|
+
restart: unless-stopped
|
|
298
|
+
|
|
299
|
+
mysql:
|
|
300
|
+
image: mysql:5.7
|
|
301
|
+
environment:
|
|
302
|
+
- MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
|
|
303
|
+
- MYSQL_DATABASE=wordpress
|
|
304
|
+
- MYSQL_USER=wordpress
|
|
305
|
+
- MYSQL_PASSWORD=${DB_PASSWORD}
|
|
306
|
+
volumes:
|
|
307
|
+
- mysql_data:/var/lib/mysql
|
|
308
|
+
networks:
|
|
309
|
+
- internal
|
|
310
|
+
restart: unless-stopped
|
|
311
|
+
|
|
312
|
+
volumes:
|
|
313
|
+
wordpress_data:
|
|
314
|
+
mysql_data:
|
|
315
|
+
|
|
316
|
+
networks:
|
|
317
|
+
traefik:
|
|
318
|
+
external: true
|
|
319
|
+
internal:
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### App + Database (Baserow + PostgreSQL)
|
|
323
|
+
|
|
324
|
+
```yml
|
|
325
|
+
services:
|
|
326
|
+
baserow:
|
|
327
|
+
image: baserow/baserow:${APP_VERSION}
|
|
328
|
+
depends_on:
|
|
329
|
+
- postgres
|
|
330
|
+
volumes:
|
|
331
|
+
- baserow_data:/baserow/data
|
|
332
|
+
environment:
|
|
333
|
+
- BASEROW_PUBLIC_URL=https://${BASE_DOMAIN}
|
|
334
|
+
- DATABASE_HOST=postgres
|
|
335
|
+
- DATABASE_NAME=baserow
|
|
336
|
+
- DATABASE_USER=postgres
|
|
337
|
+
- DATABASE_PASSWORD=${DB_PASSWORD}
|
|
338
|
+
networks:
|
|
339
|
+
- traefik
|
|
340
|
+
- internal
|
|
341
|
+
labels:
|
|
342
|
+
- traefik.enable=true
|
|
343
|
+
- traefik.docker.network=traefik
|
|
344
|
+
- traefik.http.routers.baserow.rule=Host(`${BASE_DOMAIN}`)${CUSTOM_DOMAIN:+ || Host(`${CUSTOM_DOMAIN}`)}
|
|
345
|
+
- traefik.http.routers.baserow.entrypoints=websecure
|
|
346
|
+
- traefik.http.routers.baserow.tls=true
|
|
347
|
+
- traefik.http.routers.baserow.tls.certresolver=letsencrypt
|
|
348
|
+
- traefik.http.routers.baserow.service=baserow
|
|
349
|
+
- traefik.http.services.baserow.loadbalancer.server.port=80
|
|
350
|
+
restart: unless-stopped
|
|
351
|
+
|
|
352
|
+
postgres:
|
|
353
|
+
image: postgres:16
|
|
354
|
+
environment:
|
|
355
|
+
- POSTGRES_USER=postgres
|
|
356
|
+
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
|
357
|
+
- POSTGRES_DB=baserow
|
|
358
|
+
volumes:
|
|
359
|
+
- postgres_data:/var/lib/postgresql/data
|
|
360
|
+
networks:
|
|
361
|
+
- internal
|
|
362
|
+
restart: unless-stopped
|
|
363
|
+
|
|
364
|
+
volumes:
|
|
365
|
+
baserow_data:
|
|
366
|
+
postgres_data:
|
|
367
|
+
|
|
368
|
+
networks:
|
|
369
|
+
traefik:
|
|
370
|
+
external: true
|
|
371
|
+
internal:
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Custom App (Fixed Domain, No Variables)
|
|
375
|
+
|
|
376
|
+
Used when the user wants to hardcode a specific domain. Inform the user that Ilumin's domain management panel will NOT work in this mode — they must manage DNS manually.
|
|
377
|
+
|
|
378
|
+
```yml
|
|
379
|
+
services:
|
|
380
|
+
app:
|
|
381
|
+
image: myapp:${APP_VERSION}
|
|
382
|
+
container_name: myapp
|
|
383
|
+
restart: unless-stopped
|
|
384
|
+
environment:
|
|
385
|
+
- NODE_ENV=production
|
|
386
|
+
networks:
|
|
387
|
+
- traefik
|
|
388
|
+
- internal
|
|
389
|
+
labels:
|
|
390
|
+
- traefik.enable=true
|
|
391
|
+
- traefik.docker.network=traefik
|
|
392
|
+
- traefik.http.routers.myapp.rule=Host(`app.mycompany.com`)
|
|
393
|
+
- traefik.http.routers.myapp.entrypoints=websecure
|
|
394
|
+
- traefik.http.routers.myapp.tls=true
|
|
395
|
+
- traefik.http.routers.myapp.tls.certresolver=letsencrypt
|
|
396
|
+
- traefik.http.routers.myapp-http.rule=Host(`app.mycompany.com`)
|
|
397
|
+
- traefik.http.routers.myapp-http.entrypoints=web
|
|
398
|
+
- traefik.http.routers.myapp-http.middlewares=redirect-to-https
|
|
399
|
+
- traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https
|
|
400
|
+
- traefik.http.services.myapp.loadbalancer.server.port=3000
|
|
401
|
+
|
|
402
|
+
networks:
|
|
403
|
+
traefik:
|
|
404
|
+
external: true
|
|
405
|
+
internal:
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## Pre-Deploy Compose Checklist
|
|
411
|
+
|
|
412
|
+
- [ ] All image versions replaced with `${APP_VERSION}` (no `v` prefix before the variable)
|
|
413
|
+
- [ ] All database passwords replaced with `${DB_PASSWORD}`
|
|
414
|
+
- [ ] No hardcoded secrets or credentials in the file
|
|
415
|
+
- [ ] Main service is on both `traefik` and `internal` networks
|
|
416
|
+
- [ ] Database/cache services are on `internal` network only
|
|
417
|
+
- [ ] Network definitions at the bottom: `traefik` is `external: true`, `internal` has no extra config
|
|
418
|
+
- [ ] Traefik router name is unique (format: `{appname}{role}`)
|
|
419
|
+
- [ ] Domain rule uses exact syntax with `${BASE_DOMAIN}` and `${CUSTOM_DOMAIN:+ ...}` expansion
|
|
420
|
+
- [ ] `loadbalancer.server.port` matches the actual port the container listens on
|
|
421
|
+
- [ ] TLS labels present: `tls=true` + `tls.certresolver=letsencrypt`
|
|
422
|
+
- [ ] No `\` backslashes before `$` in URLs
|
|
423
|
+
- [ ] No comments (`#`) in the final file
|
|
424
|
+
- [ ] No `deploy`, `resources`, `limits`, or `reservations` sections
|
|
425
|
+
- [ ] All volumes declared in the `volumes:` section at the bottom
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Ilumin Recommended Stack
|
|
2
|
+
|
|
3
|
+
When the user hasn't specified which infrastructure services to use, follow this recommended stack. These services are optimized for Ilumin Cloud — they are lightweight, self-hosted, and integrate well with each other via the `internal` Docker network.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## How Internal Communication Works
|
|
8
|
+
|
|
9
|
+
All services in the same `docker-compose.yml` (or on the same `internal` Docker network) can communicate using the **service name as the hostname**.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
App Container → [internal network] → postgres:5432
|
|
13
|
+
App Container → [internal network] → redis:6379
|
|
14
|
+
NocoDB → [internal network] → postgres:5432
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
> The key rule: **both the app and the service it connects to must share the same network** (always `internal`). The public internet never touches these internal services — only the main app container is exposed via Traefik.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 1. 🗄️ Database: NocoDB + PostgreSQL
|
|
22
|
+
|
|
23
|
+
**NocoDB** is the recommended database layer for Ilumin apps. It is a lightweight, self-hosted Airtable alternative that sits on top of PostgreSQL and exposes a full REST + GraphQL API, a visual UI, and programmatic table creation via API — no raw SQL required for most operations.
|
|
24
|
+
|
|
25
|
+
### Why NocoDB?
|
|
26
|
+
- Full REST API to read/write/create tables from any app or AI agent
|
|
27
|
+
- Visual interface for non-developers to manage data
|
|
28
|
+
- Integrates with webhooks, automations, and third-party tools
|
|
29
|
+
- JWT-based API authentication via `NC_AUTH_JWT_SECRET`
|
|
30
|
+
|
|
31
|
+
### Connecting Your App to NocoDB
|
|
32
|
+
|
|
33
|
+
NocoDB exposes an HTTP API. Your app does **not** connect to NocoDB as a database directly — it makes API calls to NocoDB's REST endpoint.
|
|
34
|
+
|
|
35
|
+
If your app is in the **same compose** or **same `internal` network**, use the internal hostname:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Internal (same internal network) — preferred
|
|
39
|
+
NOCODB_URL=http://nocodb:8080
|
|
40
|
+
|
|
41
|
+
# External (different server or no shared network)
|
|
42
|
+
NOCODB_URL=https://your-nocodb-domain.com
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
# Python example — connecting via internal network
|
|
47
|
+
import httpx
|
|
48
|
+
|
|
49
|
+
NOCODB_URL = os.getenv("NOCODB_URL", "http://nocodb:8080")
|
|
50
|
+
NOCODB_API_KEY = os.getenv("NOCODB_API_KEY") # = NC_AUTH_JWT_SECRET value
|
|
51
|
+
|
|
52
|
+
headers = {
|
|
53
|
+
"xc-auth": NOCODB_API_KEY,
|
|
54
|
+
"Content-Type": "application/json"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# List records from a table
|
|
58
|
+
response = httpx.get(f"{NOCODB_URL}/api/v1/db/data/noco/{{TABLE_ID}}/{{VIEW_ID}}", headers=headers)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### If NocoDB Is Already Installed
|
|
62
|
+
|
|
63
|
+
Before installing a new NocoDB, check if one is already running on the server:
|
|
64
|
+
|
|
65
|
+
1. Run `list_servers` to see installed apps.
|
|
66
|
+
2. If NocoDB is already there, retrieve its `NC_AUTH_JWT_SECRET` env var — this is the API key your app uses to authenticate.
|
|
67
|
+
3. Use the internal URL `http://nocodb:8080` if your app is in the same compose, or the public domain if separate.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 2. ⚡ Cache & Sessions: Redis + RedisInsight
|
|
72
|
+
|
|
73
|
+
**Redis** is the recommended solution for caching, session storage, and rate limiting. **RedisInsight** is the official Redis visual UI — install them together for observability.
|
|
74
|
+
|
|
75
|
+
### Connecting Your App to Redis
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Internal connection string (same internal network)
|
|
79
|
+
REDIS_URL=redis://redis:6379
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// Node.js / TypeScript — ioredis
|
|
84
|
+
import Redis from 'ioredis';
|
|
85
|
+
|
|
86
|
+
const redis = new Redis(process.env.REDIS_URL || 'redis://redis:6379');
|
|
87
|
+
|
|
88
|
+
// Cache example
|
|
89
|
+
await redis.set('key', 'value', 'EX', 3600); // expires in 1 hour
|
|
90
|
+
const value = await redis.get('key');
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
# Python — redis-py
|
|
95
|
+
import redis
|
|
96
|
+
import os
|
|
97
|
+
|
|
98
|
+
r = redis.from_url(os.getenv("REDIS_URL", "redis://redis:6379"))
|
|
99
|
+
|
|
100
|
+
r.set("key", "value", ex=3600)
|
|
101
|
+
value = r.get("key")
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
> **Network rule:** Redis is on `internal` only — it is **never** exposed publicly. RedisInsight (the UI) is on `traefik + internal` so it can be accessed via browser. Your app connects to `redis:6379` on the `internal` network.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 3. 🔄 Job Queues: BullMQ (uses Redis)
|
|
109
|
+
|
|
110
|
+
**BullMQ** is the recommended job queue solution. It uses Redis as its backend — so if Redis is already installed (from item 2), BullMQ is ready to use with no extra infrastructure.
|
|
111
|
+
|
|
112
|
+
**BullMQ is a library, not a separate service.** It runs inside your application code.
|
|
113
|
+
|
|
114
|
+
### When to use BullMQ
|
|
115
|
+
- Sending emails asynchronously
|
|
116
|
+
- Processing uploaded files or images
|
|
117
|
+
- Scheduling recurring tasks (cron-like)
|
|
118
|
+
- Handling webhooks with retry logic
|
|
119
|
+
- Any long-running task that shouldn't block an HTTP response
|
|
120
|
+
|
|
121
|
+
### BullMQ Integration (Node.js / TypeScript)
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
import { Queue, Worker } from 'bullmq';
|
|
125
|
+
import IORedis from 'ioredis';
|
|
126
|
+
|
|
127
|
+
const connection = new IORedis(process.env.REDIS_URL || 'redis://redis:6379', {
|
|
128
|
+
maxRetriesPerRequest: null, // required for BullMQ
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// --- Producer (add jobs to the queue) ---
|
|
132
|
+
const emailQueue = new Queue('emails', { connection });
|
|
133
|
+
|
|
134
|
+
await emailQueue.add('send-welcome', {
|
|
135
|
+
to: 'user@example.com',
|
|
136
|
+
subject: 'Welcome!',
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// --- Worker (process jobs from the queue) ---
|
|
140
|
+
const worker = new Worker('emails', async (job) => {
|
|
141
|
+
const { to, subject } = job.data;
|
|
142
|
+
await sendEmail(to, subject); // your email logic
|
|
143
|
+
}, { connection });
|
|
144
|
+
|
|
145
|
+
worker.on('completed', (job) => console.log(`Job ${job.id} completed`));
|
|
146
|
+
worker.on('failed', (job, err) => console.error(`Job ${job?.id} failed:`, err));
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### BullMQ Integration (Python — with rq or celery as alternatives)
|
|
150
|
+
|
|
151
|
+
> BullMQ is Node.js-native. For Python apps, use **Celery + Redis** or **rq + Redis** instead — same Redis instance, same connection string.
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
# Python — Celery with Redis broker
|
|
155
|
+
from celery import Celery
|
|
156
|
+
|
|
157
|
+
app = Celery('tasks', broker=os.getenv('REDIS_URL', 'redis://redis:6379/0'))
|
|
158
|
+
|
|
159
|
+
@app.task
|
|
160
|
+
def send_email(to: str, subject: str):
|
|
161
|
+
# your email logic
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
# Calling the task (async)
|
|
165
|
+
send_email.delay('user@example.com', 'Welcome!')
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Decision Guide
|
|
171
|
+
|
|
172
|
+
| Need | Recommended Solution |
|
|
173
|
+
|---|---|
|
|
174
|
+
| Store and query structured data | NocoDB + PostgreSQL |
|
|
175
|
+
| Cache API responses, store sessions | Redis |
|
|
176
|
+
| Background jobs, async tasks, retries | BullMQ (Node.js) / Celery (Python) — both use Redis |
|
|
177
|
+
| Visual database management | NocoDB UI (built-in) |
|
|
178
|
+
| Visual Redis inspection | RedisInsight |
|
|
179
|
+
| All of the above | Full stack compose above |
|