@brewnet/cli 0.0.1
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/LICENSE +184 -0
- package/dist/admin-server-DQVIEHV3.js +14 -0
- package/dist/admin-server-DQVIEHV3.js.map +1 -0
- package/dist/boilerplate-manager-P6QYUU7Q.js +29 -0
- package/dist/boilerplate-manager-P6QYUU7Q.js.map +1 -0
- package/dist/chunk-2VWMDHGI.js +1393 -0
- package/dist/chunk-2VWMDHGI.js.map +1 -0
- package/dist/chunk-4TJMJZMO.js +1173 -0
- package/dist/chunk-4TJMJZMO.js.map +1 -0
- package/dist/chunk-BAVGYMGA.js +114 -0
- package/dist/chunk-BAVGYMGA.js.map +1 -0
- package/dist/chunk-DH2VK3YI.js +293 -0
- package/dist/chunk-DH2VK3YI.js.map +1 -0
- package/dist/chunk-HCHY5UIQ.js +301 -0
- package/dist/chunk-HCHY5UIQ.js.map +1 -0
- package/dist/chunk-JFPHGZ6Z.js +254 -0
- package/dist/chunk-JFPHGZ6Z.js.map +1 -0
- package/dist/chunk-SIXBB6JU.js +2973 -0
- package/dist/chunk-SIXBB6JU.js.map +1 -0
- package/dist/chunk-SYV6PK3R.js +181 -0
- package/dist/chunk-SYV6PK3R.js.map +1 -0
- package/dist/chunk-ZKMWE5AH.js +444 -0
- package/dist/chunk-ZKMWE5AH.js.map +1 -0
- package/dist/cloudflare-client-TFT6VCXF.js +32 -0
- package/dist/cloudflare-client-TFT6VCXF.js.map +1 -0
- package/dist/compose-generator-O7GSIJ2S.js +19 -0
- package/dist/compose-generator-O7GSIJ2S.js.map +1 -0
- package/dist/frameworks-Z7VXDGP4.js +18 -0
- package/dist/frameworks-Z7VXDGP4.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +7897 -0
- package/dist/index.js.map +1 -0
- package/dist/services/admin-daemon.d.ts +2 -0
- package/dist/services/admin-daemon.js +33 -0
- package/dist/services/admin-daemon.js.map +1 -0
- package/dist/stacks-M4FBTVO5.js +16 -0
- package/dist/stacks-M4FBTVO5.js.map +1 -0
- package/dist/state-2SI3P4JG.js +27 -0
- package/dist/state-2SI3P4JG.js.map +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1,1173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DOCKER_LOG_MAX_FILES,
|
|
4
|
+
DOCKER_LOG_MAX_SIZE
|
|
5
|
+
} from "./chunk-HCHY5UIQ.js";
|
|
6
|
+
|
|
7
|
+
// src/services/compose-generator.ts
|
|
8
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
9
|
+
import yaml from "js-yaml";
|
|
10
|
+
|
|
11
|
+
// src/config/services.ts
|
|
12
|
+
function traefikRouterLabels(serviceId, subdomain, port) {
|
|
13
|
+
return {
|
|
14
|
+
"traefik.enable": "true",
|
|
15
|
+
[`traefik.http.routers.${serviceId}.rule`]: `Host(\`${subdomain}.{{DOMAIN}}\`)`,
|
|
16
|
+
[`traefik.http.routers.${serviceId}.entrypoints`]: "websecure",
|
|
17
|
+
[`traefik.http.routers.${serviceId}.tls.certresolver`]: "letsencrypt",
|
|
18
|
+
[`traefik.http.services.${serviceId}.loadbalancer.server.port`]: String(port)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
var SERVICE_REGISTRY = /* @__PURE__ */ new Map([
|
|
22
|
+
// -- Web servers ----------------------------------------------------------
|
|
23
|
+
[
|
|
24
|
+
"traefik",
|
|
25
|
+
{
|
|
26
|
+
id: "traefik",
|
|
27
|
+
name: "Traefik",
|
|
28
|
+
image: "traefik:v2.11",
|
|
29
|
+
ports: [80, 443, 8080],
|
|
30
|
+
subdomain: "traefik",
|
|
31
|
+
ramMB: 64,
|
|
32
|
+
diskGB: 0.1,
|
|
33
|
+
networks: ["brewnet"],
|
|
34
|
+
healthCheck: {
|
|
35
|
+
endpoint: "/api/health",
|
|
36
|
+
interval: 30,
|
|
37
|
+
timeout: 5,
|
|
38
|
+
retries: 3
|
|
39
|
+
},
|
|
40
|
+
requiredEnvVars: [],
|
|
41
|
+
traefikLabels: {
|
|
42
|
+
"traefik.enable": "true",
|
|
43
|
+
"traefik.http.routers.traefik-dashboard.rule": "Host(`traefik.{{DOMAIN}}`)",
|
|
44
|
+
"traefik.http.routers.traefik-dashboard.entrypoints": "websecure",
|
|
45
|
+
"traefik.http.routers.traefik-dashboard.tls.certresolver": "letsencrypt",
|
|
46
|
+
"traefik.http.routers.traefik-dashboard.service": "api@internal"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
"nginx",
|
|
52
|
+
{
|
|
53
|
+
id: "nginx",
|
|
54
|
+
name: "Nginx",
|
|
55
|
+
image: "nginx:1.25-alpine",
|
|
56
|
+
ports: [80, 443],
|
|
57
|
+
subdomain: "",
|
|
58
|
+
ramMB: 32,
|
|
59
|
+
diskGB: 0.1,
|
|
60
|
+
networks: ["brewnet"],
|
|
61
|
+
healthCheck: {
|
|
62
|
+
endpoint: "/",
|
|
63
|
+
interval: 30,
|
|
64
|
+
timeout: 5,
|
|
65
|
+
retries: 3
|
|
66
|
+
},
|
|
67
|
+
requiredEnvVars: []
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
[
|
|
71
|
+
"caddy",
|
|
72
|
+
{
|
|
73
|
+
id: "caddy",
|
|
74
|
+
name: "Caddy",
|
|
75
|
+
image: "caddy:2-alpine",
|
|
76
|
+
ports: [80, 443],
|
|
77
|
+
subdomain: "",
|
|
78
|
+
ramMB: 32,
|
|
79
|
+
diskGB: 0.1,
|
|
80
|
+
networks: ["brewnet"],
|
|
81
|
+
healthCheck: {
|
|
82
|
+
endpoint: "/",
|
|
83
|
+
interval: 30,
|
|
84
|
+
timeout: 5,
|
|
85
|
+
retries: 3
|
|
86
|
+
},
|
|
87
|
+
requiredEnvVars: []
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
// -- Git server -----------------------------------------------------------
|
|
91
|
+
[
|
|
92
|
+
"gitea",
|
|
93
|
+
{
|
|
94
|
+
id: "gitea",
|
|
95
|
+
name: "Gitea",
|
|
96
|
+
image: "gitea/gitea:latest",
|
|
97
|
+
ports: [3e3, 3022],
|
|
98
|
+
subdomain: "git",
|
|
99
|
+
ramMB: 256,
|
|
100
|
+
diskGB: 1,
|
|
101
|
+
networks: ["brewnet", "brewnet-internal"],
|
|
102
|
+
healthCheck: {
|
|
103
|
+
endpoint: "/api/healthz",
|
|
104
|
+
interval: 30,
|
|
105
|
+
timeout: 10,
|
|
106
|
+
retries: 3
|
|
107
|
+
},
|
|
108
|
+
requiredEnvVars: [
|
|
109
|
+
"GITEA__database__DB_TYPE",
|
|
110
|
+
"GITEA__database__HOST",
|
|
111
|
+
"GITEA__database__NAME",
|
|
112
|
+
"GITEA__database__USER",
|
|
113
|
+
"GITEA__database__PASSWD"
|
|
114
|
+
],
|
|
115
|
+
traefikLabels: traefikRouterLabels("gitea", "git", 3e3)
|
|
116
|
+
}
|
|
117
|
+
],
|
|
118
|
+
// -- File browser ---------------------------------------------------------
|
|
119
|
+
[
|
|
120
|
+
"filebrowser",
|
|
121
|
+
{
|
|
122
|
+
id: "filebrowser",
|
|
123
|
+
name: "FileBrowser",
|
|
124
|
+
image: "filebrowser/filebrowser:latest",
|
|
125
|
+
ports: [80],
|
|
126
|
+
subdomain: "files",
|
|
127
|
+
ramMB: 50,
|
|
128
|
+
diskGB: 0.1,
|
|
129
|
+
networks: ["brewnet"],
|
|
130
|
+
healthCheck: {
|
|
131
|
+
endpoint: "/health",
|
|
132
|
+
interval: 30,
|
|
133
|
+
timeout: 5,
|
|
134
|
+
retries: 3
|
|
135
|
+
},
|
|
136
|
+
requiredEnvVars: [],
|
|
137
|
+
traefikLabels: traefikRouterLabels("filebrowser", "files", 80)
|
|
138
|
+
}
|
|
139
|
+
],
|
|
140
|
+
// -- File servers ---------------------------------------------------------
|
|
141
|
+
[
|
|
142
|
+
"nextcloud",
|
|
143
|
+
{
|
|
144
|
+
id: "nextcloud",
|
|
145
|
+
name: "Nextcloud",
|
|
146
|
+
image: "nextcloud:29-apache",
|
|
147
|
+
ports: [80],
|
|
148
|
+
subdomain: "cloud",
|
|
149
|
+
ramMB: 256,
|
|
150
|
+
diskGB: 2,
|
|
151
|
+
networks: ["brewnet", "brewnet-internal"],
|
|
152
|
+
healthCheck: {
|
|
153
|
+
endpoint: "/status.php",
|
|
154
|
+
interval: 30,
|
|
155
|
+
timeout: 10,
|
|
156
|
+
retries: 5
|
|
157
|
+
},
|
|
158
|
+
requiredEnvVars: [
|
|
159
|
+
"NEXTCLOUD_ADMIN_USER",
|
|
160
|
+
"NEXTCLOUD_ADMIN_PASSWORD",
|
|
161
|
+
"NEXTCLOUD_TRUSTED_DOMAINS"
|
|
162
|
+
],
|
|
163
|
+
traefikLabels: traefikRouterLabels("nextcloud", "cloud", 80)
|
|
164
|
+
}
|
|
165
|
+
],
|
|
166
|
+
[
|
|
167
|
+
"minio",
|
|
168
|
+
{
|
|
169
|
+
id: "minio",
|
|
170
|
+
name: "MinIO",
|
|
171
|
+
image: "minio/minio:latest",
|
|
172
|
+
ports: [9e3],
|
|
173
|
+
subdomain: "minio",
|
|
174
|
+
ramMB: 128,
|
|
175
|
+
diskGB: 1,
|
|
176
|
+
networks: ["brewnet", "brewnet-internal"],
|
|
177
|
+
healthCheck: {
|
|
178
|
+
endpoint: "/minio/health/live",
|
|
179
|
+
interval: 30,
|
|
180
|
+
timeout: 5,
|
|
181
|
+
retries: 3
|
|
182
|
+
},
|
|
183
|
+
requiredEnvVars: ["MINIO_ROOT_USER", "MINIO_ROOT_PASSWORD"],
|
|
184
|
+
traefikLabels: traefikRouterLabels("minio", "minio", 9001)
|
|
185
|
+
}
|
|
186
|
+
],
|
|
187
|
+
// -- Media ----------------------------------------------------------------
|
|
188
|
+
[
|
|
189
|
+
"jellyfin",
|
|
190
|
+
{
|
|
191
|
+
id: "jellyfin",
|
|
192
|
+
name: "Jellyfin",
|
|
193
|
+
image: "jellyfin/jellyfin:latest",
|
|
194
|
+
ports: [8096],
|
|
195
|
+
subdomain: "jellyfin",
|
|
196
|
+
ramMB: 256,
|
|
197
|
+
diskGB: 2,
|
|
198
|
+
networks: ["brewnet"],
|
|
199
|
+
healthCheck: {
|
|
200
|
+
endpoint: "/health",
|
|
201
|
+
interval: 30,
|
|
202
|
+
timeout: 10,
|
|
203
|
+
retries: 3
|
|
204
|
+
},
|
|
205
|
+
requiredEnvVars: [],
|
|
206
|
+
traefikLabels: traefikRouterLabels("jellyfin", "jellyfin", 8096)
|
|
207
|
+
}
|
|
208
|
+
],
|
|
209
|
+
// -- Databases ------------------------------------------------------------
|
|
210
|
+
[
|
|
211
|
+
"postgresql",
|
|
212
|
+
{
|
|
213
|
+
id: "postgresql",
|
|
214
|
+
name: "PostgreSQL",
|
|
215
|
+
image: "postgres:18.3-alpine",
|
|
216
|
+
ports: [5432],
|
|
217
|
+
subdomain: "",
|
|
218
|
+
ramMB: 120,
|
|
219
|
+
diskGB: 1,
|
|
220
|
+
networks: ["brewnet", "brewnet-internal"],
|
|
221
|
+
healthCheck: {
|
|
222
|
+
endpoint: "",
|
|
223
|
+
interval: 10,
|
|
224
|
+
timeout: 5,
|
|
225
|
+
retries: 5
|
|
226
|
+
},
|
|
227
|
+
requiredEnvVars: ["POSTGRES_PASSWORD", "POSTGRES_USER", "POSTGRES_DB"]
|
|
228
|
+
}
|
|
229
|
+
],
|
|
230
|
+
[
|
|
231
|
+
"mysql",
|
|
232
|
+
{
|
|
233
|
+
id: "mysql",
|
|
234
|
+
name: "MySQL",
|
|
235
|
+
image: "mysql:8.4",
|
|
236
|
+
ports: [3306],
|
|
237
|
+
subdomain: "",
|
|
238
|
+
ramMB: 256,
|
|
239
|
+
diskGB: 1,
|
|
240
|
+
networks: ["brewnet", "brewnet-internal"],
|
|
241
|
+
healthCheck: {
|
|
242
|
+
endpoint: "",
|
|
243
|
+
interval: 10,
|
|
244
|
+
timeout: 5,
|
|
245
|
+
retries: 5
|
|
246
|
+
},
|
|
247
|
+
requiredEnvVars: [
|
|
248
|
+
"MYSQL_ROOT_PASSWORD",
|
|
249
|
+
"MYSQL_DATABASE",
|
|
250
|
+
"MYSQL_USER",
|
|
251
|
+
"MYSQL_PASSWORD"
|
|
252
|
+
]
|
|
253
|
+
}
|
|
254
|
+
],
|
|
255
|
+
// -- Admin UI -------------------------------------------------------------
|
|
256
|
+
[
|
|
257
|
+
"pgadmin",
|
|
258
|
+
{
|
|
259
|
+
id: "pgadmin",
|
|
260
|
+
name: "pgAdmin",
|
|
261
|
+
image: "dpage/pgadmin4:latest",
|
|
262
|
+
ports: [5050],
|
|
263
|
+
subdomain: "pgadmin",
|
|
264
|
+
ramMB: 128,
|
|
265
|
+
diskGB: 0.5,
|
|
266
|
+
networks: ["brewnet", "brewnet-internal"],
|
|
267
|
+
healthCheck: {
|
|
268
|
+
endpoint: "/misc/ping",
|
|
269
|
+
interval: 30,
|
|
270
|
+
timeout: 5,
|
|
271
|
+
retries: 3
|
|
272
|
+
},
|
|
273
|
+
requiredEnvVars: [
|
|
274
|
+
"PGADMIN_DEFAULT_EMAIL",
|
|
275
|
+
"PGADMIN_DEFAULT_PASSWORD"
|
|
276
|
+
],
|
|
277
|
+
traefikLabels: traefikRouterLabels("pgadmin", "pgadmin", 80)
|
|
278
|
+
}
|
|
279
|
+
],
|
|
280
|
+
// -- SSH ------------------------------------------------------------------
|
|
281
|
+
[
|
|
282
|
+
"openssh-server",
|
|
283
|
+
{
|
|
284
|
+
id: "openssh-server",
|
|
285
|
+
name: "OpenSSH Server",
|
|
286
|
+
image: "linuxserver/openssh-server:latest",
|
|
287
|
+
ports: [2222],
|
|
288
|
+
subdomain: "",
|
|
289
|
+
ramMB: 16,
|
|
290
|
+
diskGB: 0.1,
|
|
291
|
+
networks: ["brewnet"],
|
|
292
|
+
healthCheck: {
|
|
293
|
+
endpoint: "",
|
|
294
|
+
interval: 30,
|
|
295
|
+
timeout: 5,
|
|
296
|
+
retries: 3
|
|
297
|
+
},
|
|
298
|
+
requiredEnvVars: ["PUID", "PGID", "TZ", "PASSWORD_ACCESS", "USER_NAME"]
|
|
299
|
+
}
|
|
300
|
+
],
|
|
301
|
+
// -- Tunnel ---------------------------------------------------------------
|
|
302
|
+
[
|
|
303
|
+
"cloudflared",
|
|
304
|
+
{
|
|
305
|
+
id: "cloudflared",
|
|
306
|
+
name: "Cloudflare Tunnel",
|
|
307
|
+
image: "cloudflare/cloudflared:latest",
|
|
308
|
+
ports: [],
|
|
309
|
+
subdomain: "",
|
|
310
|
+
ramMB: 32,
|
|
311
|
+
diskGB: 0.1,
|
|
312
|
+
networks: ["brewnet"],
|
|
313
|
+
requiredEnvVars: ["TUNNEL_TOKEN"]
|
|
314
|
+
}
|
|
315
|
+
]
|
|
316
|
+
]);
|
|
317
|
+
function getServiceDefinition(id) {
|
|
318
|
+
return SERVICE_REGISTRY.get(id);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/services/compose-generator.ts
|
|
322
|
+
var BREWNET_PREFIX = "brewnet";
|
|
323
|
+
function getLoggingConfig() {
|
|
324
|
+
return {
|
|
325
|
+
driver: "json-file",
|
|
326
|
+
options: {
|
|
327
|
+
"max-size": DOCKER_LOG_MAX_SIZE,
|
|
328
|
+
"max-file": DOCKER_LOG_MAX_FILES,
|
|
329
|
+
tag: "{{.Name}}"
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function getServiceVolumes(serviceId) {
|
|
334
|
+
switch (serviceId) {
|
|
335
|
+
case "traefik":
|
|
336
|
+
return [
|
|
337
|
+
"/var/run/docker.sock:/var/run/docker.sock",
|
|
338
|
+
`${BREWNET_PREFIX}_traefik_certs:/letsencrypt`,
|
|
339
|
+
"./logs:/logs"
|
|
340
|
+
];
|
|
341
|
+
case "gitea":
|
|
342
|
+
return [`${BREWNET_PREFIX}_gitea_data:/data`];
|
|
343
|
+
case "postgresql":
|
|
344
|
+
return [`${BREWNET_PREFIX}_postgres_data:/var/lib/postgresql/data`];
|
|
345
|
+
case "mysql":
|
|
346
|
+
return [`${BREWNET_PREFIX}_mysql_data:/var/lib/mysql`];
|
|
347
|
+
case "nextcloud":
|
|
348
|
+
return [
|
|
349
|
+
`${BREWNET_PREFIX}_nextcloud_data:/var/www/html`
|
|
350
|
+
];
|
|
351
|
+
case "minio":
|
|
352
|
+
return [`${BREWNET_PREFIX}_minio_data:/data`];
|
|
353
|
+
case "jellyfin":
|
|
354
|
+
return [
|
|
355
|
+
`${BREWNET_PREFIX}_jellyfin_config:/config`,
|
|
356
|
+
`${BREWNET_PREFIX}_jellyfin_media:/media`
|
|
357
|
+
];
|
|
358
|
+
case "openssh-server":
|
|
359
|
+
return [`${BREWNET_PREFIX}_ssh_config:/config`];
|
|
360
|
+
case "pgadmin":
|
|
361
|
+
return [`${BREWNET_PREFIX}_pgadmin_data:/var/lib/pgadmin`];
|
|
362
|
+
case "filebrowser":
|
|
363
|
+
return [
|
|
364
|
+
`${BREWNET_PREFIX}_filebrowser_data:/srv`,
|
|
365
|
+
`${BREWNET_PREFIX}_filebrowser_db:/database`,
|
|
366
|
+
`${BREWNET_PREFIX}_filebrowser_config:/config`
|
|
367
|
+
];
|
|
368
|
+
case "cloudflared":
|
|
369
|
+
return [];
|
|
370
|
+
default:
|
|
371
|
+
return [];
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function getHealthcheck(serviceId, state) {
|
|
375
|
+
switch (serviceId) {
|
|
376
|
+
case "postgresql":
|
|
377
|
+
return {
|
|
378
|
+
test: ["CMD-SHELL", `pg_isready -U ${state.servers.dbServer.dbUser || "brewnet"}`],
|
|
379
|
+
interval: "10s",
|
|
380
|
+
timeout: "5s",
|
|
381
|
+
retries: 5
|
|
382
|
+
};
|
|
383
|
+
case "mysql":
|
|
384
|
+
return {
|
|
385
|
+
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"],
|
|
386
|
+
interval: "10s",
|
|
387
|
+
timeout: "5s",
|
|
388
|
+
retries: 5
|
|
389
|
+
};
|
|
390
|
+
case "gitea":
|
|
391
|
+
return {
|
|
392
|
+
test: ["CMD-SHELL", "curl -fsSL http://localhost:3000/api/healthz || exit 1"],
|
|
393
|
+
interval: "30s",
|
|
394
|
+
timeout: "10s",
|
|
395
|
+
retries: 3
|
|
396
|
+
};
|
|
397
|
+
case "traefik":
|
|
398
|
+
return {
|
|
399
|
+
test: ["CMD-SHELL", "wget --spider -q http://localhost:8080/api/overview || exit 1"],
|
|
400
|
+
interval: "30s",
|
|
401
|
+
timeout: "5s",
|
|
402
|
+
retries: 3
|
|
403
|
+
};
|
|
404
|
+
case "nextcloud":
|
|
405
|
+
return {
|
|
406
|
+
test: ["CMD-SHELL", "curl -fsSL http://localhost/status.php || exit 1"],
|
|
407
|
+
interval: "30s",
|
|
408
|
+
timeout: "10s",
|
|
409
|
+
retries: 5
|
|
410
|
+
};
|
|
411
|
+
case "jellyfin":
|
|
412
|
+
return {
|
|
413
|
+
test: ["CMD-SHELL", "curl -fsSL http://localhost:8096/health || exit 1"],
|
|
414
|
+
interval: "30s",
|
|
415
|
+
timeout: "10s",
|
|
416
|
+
retries: 3
|
|
417
|
+
};
|
|
418
|
+
default:
|
|
419
|
+
return void 0;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function getPostgresqlEnv(state) {
|
|
423
|
+
const db = state.servers.dbServer;
|
|
424
|
+
return {
|
|
425
|
+
POSTGRES_USER: db.dbUser || "brewnet",
|
|
426
|
+
POSTGRES_PASSWORD: db.dbPassword || "${DB_PASSWORD}",
|
|
427
|
+
POSTGRES_DB: db.dbName || "brewnet_db"
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
function getMysqlEnv(state) {
|
|
431
|
+
const db = state.servers.dbServer;
|
|
432
|
+
return {
|
|
433
|
+
MYSQL_ROOT_PASSWORD: db.dbPassword || "${DB_PASSWORD}",
|
|
434
|
+
MYSQL_DATABASE: db.dbName || "brewnet_db",
|
|
435
|
+
MYSQL_USER: db.dbUser || "brewnet",
|
|
436
|
+
MYSQL_PASSWORD: db.dbPassword || "${DB_PASSWORD}"
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function getGiteaEnv(state) {
|
|
440
|
+
const env = {
|
|
441
|
+
USER_UID: "1000",
|
|
442
|
+
USER_GID: "1000",
|
|
443
|
+
// Lock the web installer — prevents the setup wizard from running on first access.
|
|
444
|
+
// Without this, visiting the Gitea URL triggers the installer which writes app.ini
|
|
445
|
+
// with whatever credentials the user enters (often wrong defaults).
|
|
446
|
+
// Once app.ini exists, Gitea ignores ALL env vars → DB password mismatch on restart.
|
|
447
|
+
// With INSTALL_LOCK=true, Gitea reads DB credentials exclusively from GITEA__database__* env vars.
|
|
448
|
+
"GITEA__security__INSTALL_LOCK": "true"
|
|
449
|
+
};
|
|
450
|
+
const giteaTunnelMode = state.domain.cloudflare.tunnelMode;
|
|
451
|
+
const giteaDomain = state.domain.name;
|
|
452
|
+
if (giteaTunnelMode === "quick") {
|
|
453
|
+
env["GITEA__server__ROOT_URL"] = "http://localhost/git/";
|
|
454
|
+
} else if (giteaDomain) {
|
|
455
|
+
env["GITEA__server__ROOT_URL"] = `https://git.${giteaDomain}/`;
|
|
456
|
+
env["GITEA__server__DOMAIN"] = `git.${giteaDomain}`;
|
|
457
|
+
env["GITEA__server__SSH_DOMAIN"] = `git.${giteaDomain}`;
|
|
458
|
+
}
|
|
459
|
+
if (state.servers.dbServer.enabled && state.servers.dbServer.primary) {
|
|
460
|
+
const dbType = state.servers.dbServer.primary === "postgresql" ? "postgres" : "mysql";
|
|
461
|
+
const dbHost = state.servers.dbServer.primary === "postgresql" ? "postgresql" : "mysql";
|
|
462
|
+
const dbPort = state.servers.dbServer.primary === "postgresql" ? "5432" : "3306";
|
|
463
|
+
env["GITEA__database__DB_TYPE"] = dbType;
|
|
464
|
+
env["GITEA__database__HOST"] = `${dbHost}:${dbPort}`;
|
|
465
|
+
env["GITEA__database__NAME"] = "gitea_db";
|
|
466
|
+
env["GITEA__database__USER"] = state.servers.dbServer.dbUser || "brewnet";
|
|
467
|
+
env["GITEA__database__PASSWD"] = state.servers.dbServer.dbPassword || "${DB_PASSWORD}";
|
|
468
|
+
}
|
|
469
|
+
return env;
|
|
470
|
+
}
|
|
471
|
+
function getNextcloudEnv(state) {
|
|
472
|
+
const env = {
|
|
473
|
+
NEXTCLOUD_ADMIN_USER: state.admin.username || "admin",
|
|
474
|
+
NEXTCLOUD_ADMIN_PASSWORD: state.admin.password || "${ADMIN_PASSWORD}",
|
|
475
|
+
NEXTCLOUD_TRUSTED_DOMAINS: state.domain.name
|
|
476
|
+
};
|
|
477
|
+
const ncTunnelMode = state.domain.cloudflare.tunnelMode;
|
|
478
|
+
const ncDomain = state.domain.name;
|
|
479
|
+
if (ncTunnelMode === "quick") {
|
|
480
|
+
env["OVERWRITEWEBROOT"] = "/cloud";
|
|
481
|
+
env["NEXTCLOUD_TRUSTED_PROXIES"] = "traefik";
|
|
482
|
+
const portMap = state.portRemapping ?? {};
|
|
483
|
+
const ncPort = portMap[8443] ?? 8443;
|
|
484
|
+
env["NEXTCLOUD_TRUSTED_DOMAINS"] = `${ncDomain} localhost localhost:${ncPort} *.trycloudflare.com`;
|
|
485
|
+
} else if (ncDomain) {
|
|
486
|
+
env["OVERWRITEHOST"] = `cloud.${ncDomain}`;
|
|
487
|
+
env["OVERWRITEPROTOCOL"] = "https";
|
|
488
|
+
env["NEXTCLOUD_TRUSTED_PROXIES"] = "traefik";
|
|
489
|
+
env["NEXTCLOUD_TRUSTED_DOMAINS"] = `cloud.${ncDomain} localhost`;
|
|
490
|
+
}
|
|
491
|
+
if (state.servers.dbServer.enabled && state.servers.dbServer.primary) {
|
|
492
|
+
if (state.servers.dbServer.primary === "postgresql") {
|
|
493
|
+
env["POSTGRES_HOST"] = "postgresql";
|
|
494
|
+
env["POSTGRES_DB"] = state.servers.dbServer.dbName || "brewnet_db";
|
|
495
|
+
env["POSTGRES_USER"] = state.servers.dbServer.dbUser || "brewnet";
|
|
496
|
+
env["POSTGRES_PASSWORD"] = state.servers.dbServer.dbPassword || "${DB_PASSWORD}";
|
|
497
|
+
} else if (state.servers.dbServer.primary === "mysql") {
|
|
498
|
+
env["MYSQL_HOST"] = "mysql";
|
|
499
|
+
env["MYSQL_DATABASE"] = state.servers.dbServer.dbName || "brewnet_db";
|
|
500
|
+
env["MYSQL_USER"] = state.servers.dbServer.dbUser || "brewnet";
|
|
501
|
+
env["MYSQL_PASSWORD"] = state.servers.dbServer.dbPassword || "${DB_PASSWORD}";
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return env;
|
|
505
|
+
}
|
|
506
|
+
function getMinioEnv(state) {
|
|
507
|
+
return {
|
|
508
|
+
MINIO_ROOT_USER: state.admin.username || "admin",
|
|
509
|
+
MINIO_ROOT_PASSWORD: state.admin.password || "${ADMIN_PASSWORD}"
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function getSshEnv(state) {
|
|
513
|
+
return {
|
|
514
|
+
PUID: "1000",
|
|
515
|
+
PGID: "1000",
|
|
516
|
+
TZ: "UTC",
|
|
517
|
+
PASSWORD_ACCESS: state.servers.sshServer.passwordAuth ? "true" : "false",
|
|
518
|
+
USER_NAME: state.admin.username || "admin"
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
function getPgadminEnv(state) {
|
|
522
|
+
const env = {
|
|
523
|
+
PGADMIN_DEFAULT_EMAIL: state.servers.dbServer.pgadminEmail || `${state.admin.username || "admin"}@brewnet.dev`,
|
|
524
|
+
PGADMIN_DEFAULT_PASSWORD: state.admin.password || "${ADMIN_PASSWORD}"
|
|
525
|
+
};
|
|
526
|
+
if (state.domain.cloudflare.tunnelMode === "quick") {
|
|
527
|
+
env["SCRIPT_NAME"] = "/pgadmin";
|
|
528
|
+
}
|
|
529
|
+
return env;
|
|
530
|
+
}
|
|
531
|
+
function getFilebrowserEnv(state) {
|
|
532
|
+
const env = {
|
|
533
|
+
FB_USERNAME: state.admin.username || "admin",
|
|
534
|
+
FB_PASSWORD: state.admin.password || "${ADMIN_PASSWORD}"
|
|
535
|
+
};
|
|
536
|
+
if (state.domain.cloudflare.tunnelMode === "quick") {
|
|
537
|
+
env["FB_BASEURL"] = "/files";
|
|
538
|
+
}
|
|
539
|
+
return env;
|
|
540
|
+
}
|
|
541
|
+
function getCloudflaredEnv(state) {
|
|
542
|
+
if (state.domain.cloudflare.tunnelMode === "quick") {
|
|
543
|
+
return void 0;
|
|
544
|
+
}
|
|
545
|
+
return {
|
|
546
|
+
TUNNEL_TOKEN: state.domain.cloudflare.tunnelToken || "${TUNNEL_TOKEN}"
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
function resolveTraefikLabels(def, domain) {
|
|
550
|
+
if (!def.traefikLabels) return {};
|
|
551
|
+
const resolved = {};
|
|
552
|
+
for (const [key, value] of Object.entries(def.traefikLabels)) {
|
|
553
|
+
resolved[key] = value.replace(/\{\{DOMAIN\}\}/g, domain);
|
|
554
|
+
}
|
|
555
|
+
return resolved;
|
|
556
|
+
}
|
|
557
|
+
function buildQuickTunnelPathLabels(serviceId, path, port, noStrip = false) {
|
|
558
|
+
const name = `quicktunnel-${serviceId}`;
|
|
559
|
+
const labels = {
|
|
560
|
+
"traefik.enable": "true",
|
|
561
|
+
[`traefik.http.routers.${name}.rule`]: `PathPrefix(\`${path}\`)`,
|
|
562
|
+
[`traefik.http.routers.${name}.entrypoints`]: "web",
|
|
563
|
+
[`traefik.http.services.${name}.loadbalancer.server.port`]: String(port)
|
|
564
|
+
};
|
|
565
|
+
if (!noStrip) {
|
|
566
|
+
labels[`traefik.http.middlewares.${name}-strip.stripprefix.prefixes`] = path;
|
|
567
|
+
labels[`traefik.http.routers.${name}.middlewares`] = `${name}-strip`;
|
|
568
|
+
}
|
|
569
|
+
return labels;
|
|
570
|
+
}
|
|
571
|
+
function buildQuickTunnelExtraPathLabels(serviceId, extraPath, port) {
|
|
572
|
+
const slug = extraPath.replace(/\//g, "").replace(/[^a-z0-9]/gi, "");
|
|
573
|
+
const routerName = `quicktunnel-${serviceId}-${slug}`;
|
|
574
|
+
const serviceName = `quicktunnel-${serviceId}-${slug}`;
|
|
575
|
+
return {
|
|
576
|
+
// Explicit service link required when the container has multiple service definitions
|
|
577
|
+
[`traefik.http.routers.quicktunnel-${serviceId}.service`]: `quicktunnel-${serviceId}`,
|
|
578
|
+
[`traefik.http.routers.${routerName}.rule`]: `PathPrefix(\`${extraPath}\`)`,
|
|
579
|
+
[`traefik.http.routers.${routerName}.entrypoints`]: "web",
|
|
580
|
+
// Priority 10 > landing-page explicit priority 1 so this route takes precedence
|
|
581
|
+
[`traefik.http.routers.${routerName}.priority`]: "10",
|
|
582
|
+
[`traefik.http.routers.${routerName}.service`]: serviceName,
|
|
583
|
+
[`traefik.http.services.${serviceName}.loadbalancer.server.port`]: String(port)
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
var QUICK_TUNNEL_PATH_MAP = {
|
|
587
|
+
// Gitea: Traefik strips /git before forwarding to Gitea (strip-prefix).
|
|
588
|
+
// ROOT_URL must include /git/ sub-path so Gitea generates /git/assets/... links.
|
|
589
|
+
// ROOT_URL host must be Traefik's port (80) not Gitea's port (3000):
|
|
590
|
+
// - Gitea generates root-relative links: /git/assets/...
|
|
591
|
+
// - Browser resolves them against current origin (both local :80 and external tunnel)
|
|
592
|
+
// - Traefik receives /git/assets/... → strips /git → sends /assets/... to Gitea ✓
|
|
593
|
+
gitea: { path: "/git", port: 3e3 },
|
|
594
|
+
// FileBrowser: Vite build emits /static/... asset paths regardless of FB_BASEURL.
|
|
595
|
+
// A companion route for /static is required so browsers can load CSS/JS.
|
|
596
|
+
// See: buildQuickTunnelExtraPathLabels for the /static → filebrowser routing.
|
|
597
|
+
filebrowser: { path: "/files", port: 80, extraPaths: ["/static"] },
|
|
598
|
+
"uptime-kuma": { path: "/status", port: 3001 },
|
|
599
|
+
grafana: { path: "/grafana", port: 3e3 },
|
|
600
|
+
// pgadmin: WSGI app — SCRIPT_NAME handles path; no strip needed
|
|
601
|
+
pgadmin: { path: "/pgadmin", port: 80, noStrip: true },
|
|
602
|
+
// Nextcloud: OVERWRITEWEBROOT env makes NC generate prefixed URLs; strip prefix so NC gets clean paths
|
|
603
|
+
// Ref: https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/reverse_proxy_configuration.html
|
|
604
|
+
nextcloud: { path: "/cloud", port: 80 },
|
|
605
|
+
// Jellyfin: Base URL setting handles path natively; no strip needed
|
|
606
|
+
// Ref: https://jellyfin.org/docs/general/post-install/networking — "Base URL"
|
|
607
|
+
jellyfin: { path: "/jellyfin", port: 8096, noStrip: true },
|
|
608
|
+
// MinIO Console: pre-built React SPA without sub-path awareness; use noStrip so the app
|
|
609
|
+
// receives the full path and Traefik does not alter the prefix.
|
|
610
|
+
minio: { path: "/minio", port: 9001, noStrip: true }
|
|
611
|
+
};
|
|
612
|
+
function getServicePorts(serviceId, state) {
|
|
613
|
+
const portMap = state.portRemapping ?? {};
|
|
614
|
+
const remap = (hostPort) => portMap[hostPort] ?? hostPort;
|
|
615
|
+
switch (serviceId) {
|
|
616
|
+
case "traefik": {
|
|
617
|
+
const httpHost = remap(80);
|
|
618
|
+
const isQuickTunnel = state.domain.cloudflare.tunnelMode === "quick";
|
|
619
|
+
if (isQuickTunnel) {
|
|
620
|
+
return [`${httpHost}:80`];
|
|
621
|
+
}
|
|
622
|
+
const httpsHost = remap(443);
|
|
623
|
+
return [`${httpHost}:80`, `${httpsHost}:443`];
|
|
624
|
+
}
|
|
625
|
+
case "nginx":
|
|
626
|
+
return [`${remap(80)}:80`, `${remap(443)}:443`];
|
|
627
|
+
case "caddy":
|
|
628
|
+
return [`${remap(80)}:80`, `${remap(443)}:443`];
|
|
629
|
+
case "gitea": {
|
|
630
|
+
const ports = [`${remap(state.servers.gitServer.sshPort)}:22`];
|
|
631
|
+
if (state.domain.cloudflare.tunnelMode !== "quick") {
|
|
632
|
+
ports.unshift(`${remap(state.servers.gitServer.port)}:3000`);
|
|
633
|
+
}
|
|
634
|
+
return ports;
|
|
635
|
+
}
|
|
636
|
+
case "openssh-server":
|
|
637
|
+
return [`${remap(state.servers.sshServer.port)}:2222`];
|
|
638
|
+
case "jellyfin":
|
|
639
|
+
return [`${remap(8096)}:8096`];
|
|
640
|
+
case "nextcloud": {
|
|
641
|
+
if (state.domain.cloudflare.tunnelMode === "quick") {
|
|
642
|
+
return [];
|
|
643
|
+
}
|
|
644
|
+
return [`${remap(8443)}:80`];
|
|
645
|
+
}
|
|
646
|
+
case "minio":
|
|
647
|
+
return [`${remap(9e3)}:9000`, `${remap(9001)}:9001`];
|
|
648
|
+
case "filebrowser":
|
|
649
|
+
return [`${remap(8085)}:80`];
|
|
650
|
+
case "pgadmin":
|
|
651
|
+
return [`${remap(5050)}:80`];
|
|
652
|
+
case "cloudflared":
|
|
653
|
+
return [];
|
|
654
|
+
// DB ports are NOT exposed externally
|
|
655
|
+
case "postgresql":
|
|
656
|
+
case "mysql":
|
|
657
|
+
return [];
|
|
658
|
+
default:
|
|
659
|
+
return [];
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
function getDependsOn(serviceId, state) {
|
|
663
|
+
const deps = [];
|
|
664
|
+
const dbEnabled = state.servers.dbServer.enabled && state.servers.dbServer.primary;
|
|
665
|
+
const primaryId = state.servers.dbServer.primary;
|
|
666
|
+
switch (serviceId) {
|
|
667
|
+
case "gitea":
|
|
668
|
+
if (dbEnabled) deps.push(primaryId);
|
|
669
|
+
break;
|
|
670
|
+
case "nextcloud":
|
|
671
|
+
if (dbEnabled) deps.push(primaryId);
|
|
672
|
+
break;
|
|
673
|
+
case "pgadmin":
|
|
674
|
+
deps.push("postgresql");
|
|
675
|
+
break;
|
|
676
|
+
default:
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
return deps;
|
|
680
|
+
}
|
|
681
|
+
function getServiceEnvironment(serviceId, state) {
|
|
682
|
+
switch (serviceId) {
|
|
683
|
+
case "postgresql":
|
|
684
|
+
return getPostgresqlEnv(state);
|
|
685
|
+
case "mysql":
|
|
686
|
+
return getMysqlEnv(state);
|
|
687
|
+
case "gitea":
|
|
688
|
+
return getGiteaEnv(state);
|
|
689
|
+
case "nextcloud":
|
|
690
|
+
return getNextcloudEnv(state);
|
|
691
|
+
case "minio":
|
|
692
|
+
return getMinioEnv(state);
|
|
693
|
+
case "openssh-server":
|
|
694
|
+
return getSshEnv(state);
|
|
695
|
+
case "pgadmin":
|
|
696
|
+
return getPgadminEnv(state);
|
|
697
|
+
case "filebrowser":
|
|
698
|
+
return getFilebrowserEnv(state);
|
|
699
|
+
case "cloudflared":
|
|
700
|
+
return getCloudflaredEnv(state);
|
|
701
|
+
default:
|
|
702
|
+
return void 0;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
function buildComposeService(def, state) {
|
|
706
|
+
const domain = state.domain.name;
|
|
707
|
+
const webService = state.servers.webServer.service;
|
|
708
|
+
const svc = {
|
|
709
|
+
image: def.image,
|
|
710
|
+
container_name: `${BREWNET_PREFIX}-${def.id}`,
|
|
711
|
+
restart: "unless-stopped",
|
|
712
|
+
security_opt: ["no-new-privileges:true"],
|
|
713
|
+
networks: [...def.networks]
|
|
714
|
+
};
|
|
715
|
+
const ports = getServicePorts(def.id, state);
|
|
716
|
+
if (ports.length > 0) {
|
|
717
|
+
svc.ports = ports;
|
|
718
|
+
}
|
|
719
|
+
const volumes = getServiceVolumes(def.id);
|
|
720
|
+
if (volumes.length > 0) {
|
|
721
|
+
svc.volumes = volumes;
|
|
722
|
+
}
|
|
723
|
+
svc.logging = getLoggingConfig();
|
|
724
|
+
const environment = getServiceEnvironment(def.id, state);
|
|
725
|
+
if (environment) {
|
|
726
|
+
svc.environment = environment;
|
|
727
|
+
}
|
|
728
|
+
if (webService === "traefik" && def.traefikLabels && state.domain.cloudflare.tunnelMode !== "quick") {
|
|
729
|
+
svc.labels = resolveTraefikLabels(def, domain);
|
|
730
|
+
}
|
|
731
|
+
if (webService === "traefik" && state.domain.cloudflare.tunnelMode === "quick") {
|
|
732
|
+
const entry = QUICK_TUNNEL_PATH_MAP[def.id];
|
|
733
|
+
if (entry) {
|
|
734
|
+
svc.labels = buildQuickTunnelPathLabels(def.id, entry.path, entry.port, entry.noStrip);
|
|
735
|
+
for (const extraPath of entry.extraPaths ?? []) {
|
|
736
|
+
Object.assign(svc.labels, buildQuickTunnelExtraPathLabels(def.id, extraPath, entry.port));
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
const deps = getDependsOn(def.id, state);
|
|
741
|
+
if (deps.length > 0) {
|
|
742
|
+
svc.depends_on = deps;
|
|
743
|
+
}
|
|
744
|
+
const hc = getHealthcheck(def.id, state);
|
|
745
|
+
if (hc) {
|
|
746
|
+
svc.healthcheck = hc;
|
|
747
|
+
}
|
|
748
|
+
if (def.id === "traefik") {
|
|
749
|
+
const isQuickTunnel = state.domain.cloudflare.tunnelMode === "quick";
|
|
750
|
+
const cmds = [
|
|
751
|
+
"--providers.docker=true",
|
|
752
|
+
"--providers.docker.exposedbydefault=false",
|
|
753
|
+
"--providers.docker.network=brewnet",
|
|
754
|
+
"--entrypoints.web.address=:80"
|
|
755
|
+
];
|
|
756
|
+
if (!isQuickTunnel) {
|
|
757
|
+
cmds.push("--entrypoints.websecure.address=:443");
|
|
758
|
+
}
|
|
759
|
+
if (isQuickTunnel) {
|
|
760
|
+
cmds.push("--api.insecure=true");
|
|
761
|
+
cmds.push("--entrypoints.web.forwardedHeaders.insecure=true");
|
|
762
|
+
}
|
|
763
|
+
cmds.push(
|
|
764
|
+
"--accesslog=true",
|
|
765
|
+
"--accesslog.filepath=/logs/access.log",
|
|
766
|
+
"--accesslog.format=json",
|
|
767
|
+
"--accesslog.bufferingsize=100",
|
|
768
|
+
"--accesslog.fields.headers.defaultmode=drop",
|
|
769
|
+
"--accesslog.fields.headers.names.User-Agent=keep",
|
|
770
|
+
"--accesslog.fields.headers.names.X-Forwarded-For=keep"
|
|
771
|
+
);
|
|
772
|
+
svc.command = cmds;
|
|
773
|
+
svc.labels = {
|
|
774
|
+
"traefik.enable": "true",
|
|
775
|
+
"traefik.http.routers.brewnet-dashboard.rule": "PathPrefix(`/dashboard`) || PathPrefix(`/api`)",
|
|
776
|
+
"traefik.http.routers.brewnet-dashboard.entrypoints": "web",
|
|
777
|
+
"traefik.http.routers.brewnet-dashboard.service": "api@internal",
|
|
778
|
+
// Redirect /dashboard (no trailing slash) → /dashboard/ before auth.
|
|
779
|
+
// api@internal returns 404 for paths without trailing slash.
|
|
780
|
+
"traefik.http.routers.brewnet-dashboard.middlewares": "dashboard-slash-redirect@docker,dashboard-auth@docker",
|
|
781
|
+
"traefik.http.middlewares.dashboard-slash-redirect.redirectregex.regex": "^(https?://[^/]+)/dashboard$",
|
|
782
|
+
"traefik.http.middlewares.dashboard-slash-redirect.redirectregex.replacement": "$${1}/dashboard/",
|
|
783
|
+
"traefik.http.middlewares.dashboard-slash-redirect.redirectregex.permanent": "false",
|
|
784
|
+
"traefik.http.middlewares.dashboard-auth.basicauth.users": "${TRAEFIK_DASHBOARD_AUTH}"
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
if (def.id === "cloudflared") {
|
|
788
|
+
if (state.domain.cloudflare.tunnelMode === "quick") {
|
|
789
|
+
svc.command = ["tunnel", "--no-autoupdate", "--url", "http://traefik:80"];
|
|
790
|
+
} else {
|
|
791
|
+
svc.command = ["tunnel", "--no-autoupdate", "run"];
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (def.id === "jellyfin") {
|
|
795
|
+
svc.entrypoint = [
|
|
796
|
+
"/bin/sh",
|
|
797
|
+
"-c",
|
|
798
|
+
`mkdir -p /config/config && if [ ! -f /config/config/network.xml ]; then echo '<?xml version="1.0" encoding="utf-8"?><NetworkConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><BaseUrl>/jellyfin</BaseUrl></NetworkConfiguration>' > /config/config/network.xml; fi && exec /jellyfin/jellyfin`
|
|
799
|
+
];
|
|
800
|
+
}
|
|
801
|
+
if (def.id === "minio") {
|
|
802
|
+
svc.command = ["server", "/data", "--console-address", ":9001"];
|
|
803
|
+
}
|
|
804
|
+
return svc;
|
|
805
|
+
}
|
|
806
|
+
function applySecretsMigration(serviceId, svc, _state) {
|
|
807
|
+
const env = svc.environment ?? {};
|
|
808
|
+
const secrets = [];
|
|
809
|
+
switch (serviceId) {
|
|
810
|
+
// --- PostgreSQL: POSTGRES_PASSWORD → POSTGRES_PASSWORD_FILE ---
|
|
811
|
+
case "postgresql": {
|
|
812
|
+
delete env["POSTGRES_PASSWORD"];
|
|
813
|
+
env["POSTGRES_PASSWORD_FILE"] = "/run/secrets/db_password";
|
|
814
|
+
secrets.push("db_password");
|
|
815
|
+
break;
|
|
816
|
+
}
|
|
817
|
+
// --- MySQL: MYSQL_ROOT_PASSWORD + MYSQL_PASSWORD → _FILE variants ---
|
|
818
|
+
case "mysql": {
|
|
819
|
+
delete env["MYSQL_ROOT_PASSWORD"];
|
|
820
|
+
delete env["MYSQL_PASSWORD"];
|
|
821
|
+
env["MYSQL_ROOT_PASSWORD_FILE"] = "/run/secrets/db_password";
|
|
822
|
+
env["MYSQL_PASSWORD_FILE"] = "/run/secrets/db_password";
|
|
823
|
+
secrets.push("db_password");
|
|
824
|
+
break;
|
|
825
|
+
}
|
|
826
|
+
// --- Gitea: database PASSWD, security SECRET_KEY → __FILE variants ---
|
|
827
|
+
case "gitea": {
|
|
828
|
+
if (env["GITEA__database__PASSWD"]) {
|
|
829
|
+
delete env["GITEA__database__PASSWD"];
|
|
830
|
+
env["GITEA__database__PASSWD__FILE"] = "/run/secrets/db_password";
|
|
831
|
+
secrets.push("db_password");
|
|
832
|
+
}
|
|
833
|
+
delete env["GITEA__security__SECRET_KEY"];
|
|
834
|
+
secrets.push("gitea_secret_key");
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
// --- Nextcloud: NEXTCLOUD_ADMIN_PASSWORD, DB passwords → _FILE ---
|
|
838
|
+
case "nextcloud": {
|
|
839
|
+
delete env["NEXTCLOUD_ADMIN_PASSWORD"];
|
|
840
|
+
env["NEXTCLOUD_ADMIN_PASSWORD_FILE"] = "/run/secrets/admin_password";
|
|
841
|
+
secrets.push("admin_password");
|
|
842
|
+
if (env["POSTGRES_PASSWORD"]) {
|
|
843
|
+
delete env["POSTGRES_PASSWORD"];
|
|
844
|
+
env["POSTGRES_PASSWORD_FILE"] = "/run/secrets/db_password";
|
|
845
|
+
secrets.push("db_password");
|
|
846
|
+
}
|
|
847
|
+
if (env["MYSQL_PASSWORD"]) {
|
|
848
|
+
delete env["MYSQL_PASSWORD"];
|
|
849
|
+
env["MYSQL_PASSWORD_FILE"] = "/run/secrets/db_password";
|
|
850
|
+
secrets.push("db_password");
|
|
851
|
+
}
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
// --- pgAdmin: PGADMIN_DEFAULT_PASSWORD → _FILE ---
|
|
855
|
+
case "pgadmin": {
|
|
856
|
+
delete env["PGADMIN_DEFAULT_PASSWORD"];
|
|
857
|
+
env["PGADMIN_DEFAULT_PASSWORD_FILE"] = "/run/secrets/admin_password";
|
|
858
|
+
secrets.push("admin_password");
|
|
859
|
+
break;
|
|
860
|
+
}
|
|
861
|
+
// --- Traefik: basicauth.users → basicauth.usersfile (BasicAuth bug fix) ---
|
|
862
|
+
case "traefik": {
|
|
863
|
+
secrets.push("traefik_dashboard_auth");
|
|
864
|
+
if (svc.labels) {
|
|
865
|
+
delete svc.labels["traefik.http.middlewares.dashboard-auth.basicauth.users"];
|
|
866
|
+
svc.labels["traefik.http.middlewares.dashboard-auth.basicauth.usersfile"] = "/run/secrets/traefik_dashboard_auth";
|
|
867
|
+
}
|
|
868
|
+
break;
|
|
869
|
+
}
|
|
870
|
+
// minio, cloudflared: no changes (stay in .env)
|
|
871
|
+
default:
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
const uniqueSecrets = [...new Set(secrets)];
|
|
875
|
+
if (uniqueSecrets.length > 0) {
|
|
876
|
+
svc.secrets = uniqueSecrets;
|
|
877
|
+
}
|
|
878
|
+
if (Object.keys(env).length > 0) {
|
|
879
|
+
svc.environment = env;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
function collectTopLevelSecrets(services) {
|
|
883
|
+
const allSecrets = /* @__PURE__ */ new Set();
|
|
884
|
+
for (const svc of Object.values(services)) {
|
|
885
|
+
for (const s of svc.secrets ?? []) {
|
|
886
|
+
allSecrets.add(s);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
if (allSecrets.size === 0) return void 0;
|
|
890
|
+
const result = {};
|
|
891
|
+
for (const name of allSecrets) {
|
|
892
|
+
result[name] = { file: `./secrets/${name}` };
|
|
893
|
+
}
|
|
894
|
+
return result;
|
|
895
|
+
}
|
|
896
|
+
function collectNamedVolumes(services) {
|
|
897
|
+
const volumes = {};
|
|
898
|
+
for (const svc of Object.values(services)) {
|
|
899
|
+
for (const vol of svc.volumes ?? []) {
|
|
900
|
+
const hostPart = vol.split(":")[0];
|
|
901
|
+
if (!hostPart.startsWith("/") && !hostPart.startsWith(".")) {
|
|
902
|
+
volumes[hostPart] = null;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return volumes;
|
|
907
|
+
}
|
|
908
|
+
function generateComposeConfig(state) {
|
|
909
|
+
const services = {};
|
|
910
|
+
const webId = state.servers.webServer.service;
|
|
911
|
+
const webDef = SERVICE_REGISTRY.get(webId);
|
|
912
|
+
if (webDef) {
|
|
913
|
+
services[webId] = buildComposeService(webDef, state);
|
|
914
|
+
}
|
|
915
|
+
if (webId === "traefik") {
|
|
916
|
+
const isQuickTunnel = state.domain.cloudflare.tunnelMode === "quick";
|
|
917
|
+
const landingLabels = {
|
|
918
|
+
"traefik.enable": "true",
|
|
919
|
+
"traefik.http.services.brewnet-landing.loadbalancer.server.port": "80",
|
|
920
|
+
// Security headers middleware
|
|
921
|
+
"traefik.http.middlewares.landing-headers.headers.customResponseHeaders.Server": "Brewnet",
|
|
922
|
+
"traefik.http.middlewares.landing-headers.headers.frameDeny": "true",
|
|
923
|
+
"traefik.http.middlewares.landing-headers.headers.contentTypeNosniff": "true",
|
|
924
|
+
"traefik.http.middlewares.landing-headers.headers.browserXssFilter": "true"
|
|
925
|
+
};
|
|
926
|
+
if (isQuickTunnel) {
|
|
927
|
+
landingLabels["traefik.http.routers.brewnet-landing.rule"] = "PathPrefix(`/`)";
|
|
928
|
+
landingLabels["traefik.http.routers.brewnet-landing.entrypoints"] = "web";
|
|
929
|
+
landingLabels["traefik.http.routers.brewnet-landing.priority"] = "1";
|
|
930
|
+
landingLabels["traefik.http.routers.brewnet-landing.middlewares"] = "landing-headers@docker";
|
|
931
|
+
} else {
|
|
932
|
+
landingLabels["traefik.http.routers.brewnet-landing.rule"] = "Host(`localhost`)";
|
|
933
|
+
landingLabels["traefik.http.routers.brewnet-landing.entrypoints"] = "web";
|
|
934
|
+
landingLabels["traefik.http.routers.brewnet-landing.middlewares"] = "landing-headers@docker";
|
|
935
|
+
}
|
|
936
|
+
services["brewnet-landing"] = {
|
|
937
|
+
build: "./landing",
|
|
938
|
+
container_name: "brewnet-landing",
|
|
939
|
+
restart: "unless-stopped",
|
|
940
|
+
security_opt: ["no-new-privileges:true"],
|
|
941
|
+
networks: ["brewnet"],
|
|
942
|
+
labels: landingLabels,
|
|
943
|
+
logging: getLoggingConfig()
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
const giteaDef = SERVICE_REGISTRY.get("gitea");
|
|
947
|
+
if (giteaDef) {
|
|
948
|
+
services["gitea"] = buildComposeService(giteaDef, state);
|
|
949
|
+
}
|
|
950
|
+
if (state.servers.dbServer.enabled && state.servers.dbServer.primary) {
|
|
951
|
+
const dbId = state.servers.dbServer.primary;
|
|
952
|
+
const dbDef = SERVICE_REGISTRY.get(dbId);
|
|
953
|
+
if (dbDef) {
|
|
954
|
+
const ver = state.servers.dbServer.primaryVersion;
|
|
955
|
+
const versionedImage = ver && dbId === "postgresql" ? `postgres:${ver}-alpine` : ver && dbId === "mysql" ? `mysql:${ver}` : dbDef.image;
|
|
956
|
+
services[dbId] = buildComposeService({ ...dbDef, image: versionedImage }, state);
|
|
957
|
+
}
|
|
958
|
+
if (state.servers.dbServer.adminUI && dbId === "postgresql") {
|
|
959
|
+
const pgadminDef = SERVICE_REGISTRY.get("pgadmin");
|
|
960
|
+
if (pgadminDef) {
|
|
961
|
+
services["pgadmin"] = buildComposeService(pgadminDef, state);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
if (state.servers.fileServer.enabled && state.servers.fileServer.service) {
|
|
966
|
+
const fileId = state.servers.fileServer.service;
|
|
967
|
+
const fileDef = SERVICE_REGISTRY.get(fileId);
|
|
968
|
+
if (fileDef) {
|
|
969
|
+
services[fileId] = buildComposeService(fileDef, state);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (state.servers.media.enabled && state.servers.media.services.length > 0) {
|
|
973
|
+
for (const mediaId of state.servers.media.services) {
|
|
974
|
+
const mediaDef = SERVICE_REGISTRY.get(mediaId);
|
|
975
|
+
if (mediaDef) {
|
|
976
|
+
services[mediaId] = buildComposeService(mediaDef, state);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
if (state.servers.sshServer.enabled) {
|
|
981
|
+
const sshDef = SERVICE_REGISTRY.get("openssh-server");
|
|
982
|
+
if (sshDef) {
|
|
983
|
+
services["openssh-server"] = buildComposeService(sshDef, state);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
if (state.domain.cloudflare.enabled) {
|
|
987
|
+
const cfDef = SERVICE_REGISTRY.get("cloudflared");
|
|
988
|
+
if (cfDef) {
|
|
989
|
+
services["cloudflared"] = buildComposeService(cfDef, state);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
if (state.servers.fileBrowser.enabled) {
|
|
993
|
+
const fbDef = SERVICE_REGISTRY.get("filebrowser");
|
|
994
|
+
if (fbDef) {
|
|
995
|
+
services["filebrowser"] = buildComposeService(fbDef, state);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
for (const [id, svc] of Object.entries(services)) {
|
|
999
|
+
applySecretsMigration(id, svc, state);
|
|
1000
|
+
}
|
|
1001
|
+
const namedVolumes = collectNamedVolumes(services);
|
|
1002
|
+
const topSecrets = collectTopLevelSecrets(services);
|
|
1003
|
+
return {
|
|
1004
|
+
name: state.projectName || "brewnet",
|
|
1005
|
+
services,
|
|
1006
|
+
networks: {
|
|
1007
|
+
brewnet: { external: true },
|
|
1008
|
+
"brewnet-internal": { internal: true }
|
|
1009
|
+
},
|
|
1010
|
+
...Object.keys(namedVolumes).length > 0 ? { volumes: namedVolumes } : {},
|
|
1011
|
+
...topSecrets ? { secrets: topSecrets } : {}
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
function addExternalLabels(composePath, appName, hostname, port) {
|
|
1015
|
+
const raw = readFileSync(composePath, "utf-8");
|
|
1016
|
+
const doc = yaml.load(raw);
|
|
1017
|
+
const services = doc["services"];
|
|
1018
|
+
if (!services) return;
|
|
1019
|
+
const serviceKey = services[appName] ? appName : services[`brewnet-${appName}`] ? `brewnet-${appName}` : Object.keys(services).find((k) => {
|
|
1020
|
+
const cn = services[k]?.["container_name"];
|
|
1021
|
+
return cn === appName || cn === `brewnet-${appName}`;
|
|
1022
|
+
});
|
|
1023
|
+
if (!serviceKey) {
|
|
1024
|
+
throw new Error(`Service "${appName}" not found in docker-compose.yml`);
|
|
1025
|
+
}
|
|
1026
|
+
const svc = services[serviceKey];
|
|
1027
|
+
const labels = svc["labels"] ?? {};
|
|
1028
|
+
const routerName = `${appName}-external`;
|
|
1029
|
+
labels["traefik.enable"] = "true";
|
|
1030
|
+
labels[`traefik.http.routers.${routerName}.rule`] = `Host(\`${hostname}\`)`;
|
|
1031
|
+
labels[`traefik.http.routers.${routerName}.entrypoints`] = "web";
|
|
1032
|
+
labels[`traefik.http.routers.${routerName}.service`] = routerName;
|
|
1033
|
+
labels[`traefik.http.services.${routerName}.loadbalancer.server.port`] = String(port);
|
|
1034
|
+
svc["labels"] = labels;
|
|
1035
|
+
const output = yaml.dump(doc, {
|
|
1036
|
+
indent: 2,
|
|
1037
|
+
lineWidth: 120,
|
|
1038
|
+
noRefs: true,
|
|
1039
|
+
sortKeys: false,
|
|
1040
|
+
quotingType: '"',
|
|
1041
|
+
forceQuotes: false
|
|
1042
|
+
});
|
|
1043
|
+
writeFileSync(composePath, output, "utf-8");
|
|
1044
|
+
}
|
|
1045
|
+
function removeExternalLabels(composePath, appName) {
|
|
1046
|
+
const raw = readFileSync(composePath, "utf-8");
|
|
1047
|
+
const doc = yaml.load(raw);
|
|
1048
|
+
const services = doc["services"];
|
|
1049
|
+
if (!services) return;
|
|
1050
|
+
const serviceKey = services[appName] ? appName : services[`brewnet-${appName}`] ? `brewnet-${appName}` : Object.keys(services).find((k) => {
|
|
1051
|
+
const cn = services[k]?.["container_name"];
|
|
1052
|
+
return cn === appName || cn === `brewnet-${appName}`;
|
|
1053
|
+
});
|
|
1054
|
+
if (!serviceKey) return;
|
|
1055
|
+
const svc = services[serviceKey];
|
|
1056
|
+
const labels = svc["labels"];
|
|
1057
|
+
if (!labels) return;
|
|
1058
|
+
const routerName = `${appName}-external`;
|
|
1059
|
+
const prefix = `traefik.http.routers.${routerName}`;
|
|
1060
|
+
const svcPrefix = `traefik.http.services.${routerName}`;
|
|
1061
|
+
for (const key of Object.keys(labels)) {
|
|
1062
|
+
if (key.startsWith(prefix) || key.startsWith(svcPrefix)) {
|
|
1063
|
+
delete labels[key];
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
const output = yaml.dump(doc, {
|
|
1067
|
+
indent: 2,
|
|
1068
|
+
lineWidth: 120,
|
|
1069
|
+
noRefs: true,
|
|
1070
|
+
sortKeys: false,
|
|
1071
|
+
quotingType: '"',
|
|
1072
|
+
forceQuotes: false
|
|
1073
|
+
});
|
|
1074
|
+
writeFileSync(composePath, output, "utf-8");
|
|
1075
|
+
}
|
|
1076
|
+
function addQuickTunnelAppLabels(composePath, appName, serviceName, port, noStrip) {
|
|
1077
|
+
const raw = readFileSync(composePath, "utf-8");
|
|
1078
|
+
const doc = yaml.load(raw);
|
|
1079
|
+
const services = doc["services"];
|
|
1080
|
+
if (!services) return;
|
|
1081
|
+
const svc = services[serviceName];
|
|
1082
|
+
if (!svc) return;
|
|
1083
|
+
const routerName = `app-${appName}`;
|
|
1084
|
+
const pathPrefix = `/apps/${appName}`;
|
|
1085
|
+
let labels;
|
|
1086
|
+
const rawLabels = svc["labels"];
|
|
1087
|
+
if (Array.isArray(rawLabels)) {
|
|
1088
|
+
labels = {};
|
|
1089
|
+
for (const l of rawLabels) {
|
|
1090
|
+
const s = String(l);
|
|
1091
|
+
const idx = s.indexOf("=");
|
|
1092
|
+
if (idx > 0) labels[s.slice(0, idx)] = s.slice(idx + 1);
|
|
1093
|
+
}
|
|
1094
|
+
} else {
|
|
1095
|
+
labels = rawLabels ?? {};
|
|
1096
|
+
}
|
|
1097
|
+
labels["traefik.enable"] = "true";
|
|
1098
|
+
labels[`traefik.http.routers.${routerName}.rule`] = `PathPrefix(\`${pathPrefix}\`)`;
|
|
1099
|
+
labels[`traefik.http.routers.${routerName}.entrypoints`] = "web";
|
|
1100
|
+
labels[`traefik.http.routers.${routerName}.priority`] = String(10 + pathPrefix.length);
|
|
1101
|
+
labels[`traefik.http.routers.${routerName}.service`] = routerName;
|
|
1102
|
+
labels[`traefik.http.services.${routerName}.loadbalancer.server.port`] = String(port);
|
|
1103
|
+
labels[`traefik.http.middlewares.${routerName}-slash.redirectregex.regex`] = `^(.*${pathPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})$$`;
|
|
1104
|
+
labels[`traefik.http.middlewares.${routerName}-slash.redirectregex.replacement`] = "$${1}/";
|
|
1105
|
+
labels[`traefik.http.middlewares.${routerName}-slash.redirectregex.permanent`] = "false";
|
|
1106
|
+
if (noStrip) {
|
|
1107
|
+
} else {
|
|
1108
|
+
labels[`traefik.http.middlewares.${routerName}-strip.stripprefix.prefixes`] = pathPrefix;
|
|
1109
|
+
labels[`traefik.http.routers.${routerName}.middlewares`] = `${routerName}-slash,${routerName}-strip`;
|
|
1110
|
+
}
|
|
1111
|
+
svc["labels"] = labels;
|
|
1112
|
+
const svcNetworks = svc["networks"] ?? [];
|
|
1113
|
+
if (Array.isArray(svcNetworks)) {
|
|
1114
|
+
if (!svcNetworks.includes("brewnet")) svcNetworks.push("brewnet");
|
|
1115
|
+
svc["networks"] = svcNetworks;
|
|
1116
|
+
} else {
|
|
1117
|
+
if (!svcNetworks["brewnet"]) svcNetworks["brewnet"] = {};
|
|
1118
|
+
svc["networks"] = svcNetworks;
|
|
1119
|
+
}
|
|
1120
|
+
const topNetworks = doc["networks"] ?? {};
|
|
1121
|
+
topNetworks["brewnet"] = { external: true };
|
|
1122
|
+
doc["networks"] = topNetworks;
|
|
1123
|
+
const output = yaml.dump(doc, {
|
|
1124
|
+
indent: 2,
|
|
1125
|
+
lineWidth: 120,
|
|
1126
|
+
noRefs: true,
|
|
1127
|
+
sortKeys: false,
|
|
1128
|
+
quotingType: '"',
|
|
1129
|
+
forceQuotes: false
|
|
1130
|
+
});
|
|
1131
|
+
writeFileSync(composePath, output, "utf-8");
|
|
1132
|
+
}
|
|
1133
|
+
function patchCloudflaredToNamedTunnel(composePath, tunnelToken) {
|
|
1134
|
+
const raw = readFileSync(composePath, "utf-8");
|
|
1135
|
+
const doc = yaml.load(raw);
|
|
1136
|
+
const services = doc["services"];
|
|
1137
|
+
if (!services?.["cloudflared"]) return false;
|
|
1138
|
+
const svc = services["cloudflared"];
|
|
1139
|
+
svc["command"] = ["tunnel", "--no-autoupdate", "run"];
|
|
1140
|
+
const env = svc["environment"] ?? {};
|
|
1141
|
+
env["TUNNEL_TOKEN"] = tunnelToken;
|
|
1142
|
+
svc["environment"] = env;
|
|
1143
|
+
const output = yaml.dump(doc, {
|
|
1144
|
+
indent: 2,
|
|
1145
|
+
lineWidth: 120,
|
|
1146
|
+
noRefs: true,
|
|
1147
|
+
sortKeys: false
|
|
1148
|
+
});
|
|
1149
|
+
writeFileSync(composePath, output, "utf-8");
|
|
1150
|
+
return true;
|
|
1151
|
+
}
|
|
1152
|
+
function composeConfigToYaml(config) {
|
|
1153
|
+
return yaml.dump(config, {
|
|
1154
|
+
indent: 2,
|
|
1155
|
+
lineWidth: 120,
|
|
1156
|
+
noRefs: true,
|
|
1157
|
+
sortKeys: false,
|
|
1158
|
+
quotingType: '"',
|
|
1159
|
+
forceQuotes: false
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
export {
|
|
1164
|
+
SERVICE_REGISTRY,
|
|
1165
|
+
getServiceDefinition,
|
|
1166
|
+
generateComposeConfig,
|
|
1167
|
+
addExternalLabels,
|
|
1168
|
+
removeExternalLabels,
|
|
1169
|
+
addQuickTunnelAppLabels,
|
|
1170
|
+
patchCloudflaredToNamedTunnel,
|
|
1171
|
+
composeConfigToYaml
|
|
1172
|
+
};
|
|
1173
|
+
//# sourceMappingURL=chunk-4TJMJZMO.js.map
|