@geekmidas/cli 0.46.0 → 0.48.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/dist/config.d.cts +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/{dokploy-api-D8a0eQQB.cjs → dokploy-api-BDLu0qWi.cjs} +12 -1
- package/dist/dokploy-api-BDLu0qWi.cjs.map +1 -0
- package/dist/dokploy-api-BN3V57z1.mjs +3 -0
- package/dist/dokploy-api-BdCKjFDA.cjs +3 -0
- package/dist/{dokploy-api-b6usLLKk.mjs → dokploy-api-DvzIDxTj.mjs} +12 -1
- package/dist/dokploy-api-DvzIDxTj.mjs.map +1 -0
- package/dist/{index-BtnjoghR.d.mts → index-A70abJ1m.d.mts} +60 -2
- package/dist/index-A70abJ1m.d.mts.map +1 -0
- package/dist/{index-c89X2mi2.d.cts → index-pOA56MWT.d.cts} +60 -2
- package/dist/index-pOA56MWT.d.cts.map +1 -0
- package/dist/index.cjs +685 -249
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +685 -249
- package/dist/index.mjs.map +1 -1
- package/dist/workspace/index.d.cts +1 -1
- package/dist/workspace/index.d.mts +1 -1
- package/dist/workspace-CaVW6j2q.cjs.map +1 -1
- package/dist/workspace-DLFRaDc-.mjs.map +1 -1
- package/package.json +3 -3
- package/src/auth/credentials.ts +66 -0
- package/src/deploy/dns/hostinger-api.ts +258 -0
- package/src/deploy/dns/index.ts +398 -0
- package/src/deploy/dokploy-api.ts +12 -0
- package/src/deploy/index.ts +108 -35
- package/src/docker/templates.ts +10 -14
- package/src/workspace/types.ts +64 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/dokploy-api-C1JgU9Vr.mjs +0 -3
- package/dist/dokploy-api-Cpq_tLSz.cjs +0 -3
- package/dist/dokploy-api-D8a0eQQB.cjs.map +0 -1
- package/dist/dokploy-api-b6usLLKk.mjs.map +0 -1
- package/dist/index-BtnjoghR.d.mts.map +0 -1
- package/dist/index-c89X2mi2.d.cts.map +0 -1
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DNS orchestration for deployments
|
|
3
|
+
*
|
|
4
|
+
* Handles automatic DNS record creation for deployed applications.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { lookup } from 'node:dns/promises';
|
|
8
|
+
import { getHostingerToken, storeHostingerToken } from '../../auth/credentials';
|
|
9
|
+
import type { DnsConfig } from '../../workspace/types';
|
|
10
|
+
import { HostingerApi } from './hostinger-api';
|
|
11
|
+
|
|
12
|
+
const logger = console;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Required DNS record for an app
|
|
16
|
+
*/
|
|
17
|
+
export interface RequiredDnsRecord {
|
|
18
|
+
/** Full hostname (e.g., 'api.joemoer.traflabs.io') */
|
|
19
|
+
hostname: string;
|
|
20
|
+
/** Subdomain part for the DNS provider (e.g., 'api.joemoer') */
|
|
21
|
+
subdomain: string;
|
|
22
|
+
/** Record type */
|
|
23
|
+
type: 'A' | 'CNAME';
|
|
24
|
+
/** Target value (IP or hostname) */
|
|
25
|
+
value: string;
|
|
26
|
+
/** App name */
|
|
27
|
+
appName: string;
|
|
28
|
+
/** Whether the record was created */
|
|
29
|
+
created?: boolean;
|
|
30
|
+
/** Whether the record already existed */
|
|
31
|
+
existed?: boolean;
|
|
32
|
+
/** Error if creation failed */
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Result of DNS record creation
|
|
38
|
+
*/
|
|
39
|
+
export interface DnsCreationResult {
|
|
40
|
+
records: RequiredDnsRecord[];
|
|
41
|
+
success: boolean;
|
|
42
|
+
serverIp: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve IP address from a hostname
|
|
47
|
+
*/
|
|
48
|
+
export async function resolveHostnameToIp(hostname: string): Promise<string> {
|
|
49
|
+
try {
|
|
50
|
+
const addresses = await lookup(hostname, { family: 4 });
|
|
51
|
+
return addresses.address;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Failed to resolve IP for ${hostname}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Extract subdomain from full hostname relative to root domain
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* extractSubdomain('api.joemoer.traflabs.io', 'traflabs.io') => 'api.joemoer'
|
|
64
|
+
* extractSubdomain('joemoer.traflabs.io', 'traflabs.io') => 'joemoer'
|
|
65
|
+
*/
|
|
66
|
+
export function extractSubdomain(hostname: string, rootDomain: string): string {
|
|
67
|
+
if (!hostname.endsWith(rootDomain)) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Hostname ${hostname} is not under root domain ${rootDomain}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const subdomain = hostname.slice(0, -(rootDomain.length + 1)); // +1 for the dot
|
|
74
|
+
return subdomain || '@'; // '@' represents the root domain itself
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Generate required DNS records for a deployment
|
|
79
|
+
*/
|
|
80
|
+
export function generateRequiredRecords(
|
|
81
|
+
appHostnames: Map<string, string>, // appName -> hostname
|
|
82
|
+
rootDomain: string,
|
|
83
|
+
serverIp: string,
|
|
84
|
+
): RequiredDnsRecord[] {
|
|
85
|
+
const records: RequiredDnsRecord[] = [];
|
|
86
|
+
|
|
87
|
+
for (const [appName, hostname] of appHostnames) {
|
|
88
|
+
const subdomain = extractSubdomain(hostname, rootDomain);
|
|
89
|
+
records.push({
|
|
90
|
+
hostname,
|
|
91
|
+
subdomain,
|
|
92
|
+
type: 'A',
|
|
93
|
+
value: serverIp,
|
|
94
|
+
appName,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return records;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Print DNS records table
|
|
103
|
+
*/
|
|
104
|
+
export function printDnsRecordsTable(
|
|
105
|
+
records: RequiredDnsRecord[],
|
|
106
|
+
rootDomain: string,
|
|
107
|
+
): void {
|
|
108
|
+
logger.log('\n 📋 DNS Records for ' + rootDomain + ':');
|
|
109
|
+
logger.log(
|
|
110
|
+
' ┌─────────────────────────────────────┬──────┬─────────────────┬────────┐',
|
|
111
|
+
);
|
|
112
|
+
logger.log(
|
|
113
|
+
' │ Subdomain │ Type │ Value │ Status │',
|
|
114
|
+
);
|
|
115
|
+
logger.log(
|
|
116
|
+
' ├─────────────────────────────────────┼──────┼─────────────────┼────────┤',
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
for (const record of records) {
|
|
120
|
+
const subdomain = record.subdomain.padEnd(35);
|
|
121
|
+
const type = record.type.padEnd(4);
|
|
122
|
+
const value = record.value.padEnd(15);
|
|
123
|
+
let status: string;
|
|
124
|
+
|
|
125
|
+
if (record.error) {
|
|
126
|
+
status = '✗';
|
|
127
|
+
} else if (record.created) {
|
|
128
|
+
status = '✓ new';
|
|
129
|
+
} else if (record.existed) {
|
|
130
|
+
status = '✓';
|
|
131
|
+
} else {
|
|
132
|
+
status = '?';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
logger.log(
|
|
136
|
+
` │ ${subdomain} │ ${type} │ ${value} │ ${status.padEnd(6)} │`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
logger.log(
|
|
141
|
+
' └─────────────────────────────────────┴──────┴─────────────────┴────────┘',
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Print DNS records in a simple format for manual setup
|
|
147
|
+
*/
|
|
148
|
+
export function printDnsRecordsSimple(
|
|
149
|
+
records: RequiredDnsRecord[],
|
|
150
|
+
rootDomain: string,
|
|
151
|
+
): void {
|
|
152
|
+
logger.log('\n 📋 Required DNS Records:');
|
|
153
|
+
logger.log(` Add these A records to your DNS provider (${rootDomain}):\n`);
|
|
154
|
+
|
|
155
|
+
for (const record of records) {
|
|
156
|
+
logger.log(` ${record.subdomain} → ${record.value} (A record)`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
logger.log('');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Prompt for input (reuse from deploy/index.ts pattern)
|
|
164
|
+
*/
|
|
165
|
+
async function promptForToken(message: string): Promise<string> {
|
|
166
|
+
const { stdin, stdout } = await import('node:process');
|
|
167
|
+
|
|
168
|
+
if (!stdin.isTTY) {
|
|
169
|
+
throw new Error('Interactive input required for Hostinger token.');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Hidden input for token
|
|
173
|
+
stdout.write(message);
|
|
174
|
+
return new Promise((resolve) => {
|
|
175
|
+
let value = '';
|
|
176
|
+
const onData = (char: Buffer) => {
|
|
177
|
+
const c = char.toString();
|
|
178
|
+
if (c === '\n' || c === '\r') {
|
|
179
|
+
stdin.setRawMode(false);
|
|
180
|
+
stdin.pause();
|
|
181
|
+
stdin.removeListener('data', onData);
|
|
182
|
+
stdout.write('\n');
|
|
183
|
+
resolve(value);
|
|
184
|
+
} else if (c === '\u0003') {
|
|
185
|
+
stdin.setRawMode(false);
|
|
186
|
+
stdin.pause();
|
|
187
|
+
stdout.write('\n');
|
|
188
|
+
process.exit(1);
|
|
189
|
+
} else if (c === '\u007F' || c === '\b') {
|
|
190
|
+
if (value.length > 0) value = value.slice(0, -1);
|
|
191
|
+
} else {
|
|
192
|
+
value += c;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
stdin.setRawMode(true);
|
|
196
|
+
stdin.resume();
|
|
197
|
+
stdin.on('data', onData);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Create DNS records using the configured provider
|
|
203
|
+
*/
|
|
204
|
+
export async function createDnsRecords(
|
|
205
|
+
records: RequiredDnsRecord[],
|
|
206
|
+
dnsConfig: DnsConfig,
|
|
207
|
+
): Promise<RequiredDnsRecord[]> {
|
|
208
|
+
const { provider, domain: rootDomain, ttl = 300 } = dnsConfig;
|
|
209
|
+
|
|
210
|
+
if (provider === 'manual') {
|
|
211
|
+
// Just mark all records as needing manual creation
|
|
212
|
+
return records.map((r) => ({ ...r, created: false, existed: false }));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (provider === 'hostinger') {
|
|
216
|
+
return createHostingerRecords(records, rootDomain, ttl);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (provider === 'cloudflare') {
|
|
220
|
+
logger.log(' ⚠ Cloudflare DNS integration not yet implemented');
|
|
221
|
+
return records.map((r) => ({
|
|
222
|
+
...r,
|
|
223
|
+
error: 'Cloudflare not implemented',
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return records;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Create DNS records at Hostinger
|
|
232
|
+
*/
|
|
233
|
+
async function createHostingerRecords(
|
|
234
|
+
records: RequiredDnsRecord[],
|
|
235
|
+
rootDomain: string,
|
|
236
|
+
ttl: number,
|
|
237
|
+
): Promise<RequiredDnsRecord[]> {
|
|
238
|
+
// Get or prompt for Hostinger token
|
|
239
|
+
let token = await getHostingerToken();
|
|
240
|
+
|
|
241
|
+
if (!token) {
|
|
242
|
+
logger.log('\n 📋 Hostinger API token not found.');
|
|
243
|
+
logger.log(
|
|
244
|
+
' Get your token from: https://hpanel.hostinger.com/profile/api\n',
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
token = await promptForToken(' Hostinger API Token: ');
|
|
249
|
+
await storeHostingerToken(token);
|
|
250
|
+
logger.log(' ✓ Token saved');
|
|
251
|
+
} catch {
|
|
252
|
+
logger.log(' ⚠ Could not get token, skipping DNS creation');
|
|
253
|
+
return records.map((r) => ({
|
|
254
|
+
...r,
|
|
255
|
+
error: 'No API token',
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const api = new HostingerApi(token);
|
|
261
|
+
const results: RequiredDnsRecord[] = [];
|
|
262
|
+
|
|
263
|
+
// Get existing records to check what already exists
|
|
264
|
+
let existingRecords: Awaited<ReturnType<typeof api.getRecords>> = [];
|
|
265
|
+
try {
|
|
266
|
+
existingRecords = await api.getRecords(rootDomain);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
269
|
+
logger.log(` ⚠ Failed to fetch existing DNS records: ${message}`);
|
|
270
|
+
return records.map((r) => ({ ...r, error: message }));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Process each record
|
|
274
|
+
for (const record of records) {
|
|
275
|
+
const existing = existingRecords.find(
|
|
276
|
+
(r) => r.name === record.subdomain && r.type === 'A',
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
if (existing) {
|
|
280
|
+
// Record already exists
|
|
281
|
+
results.push({
|
|
282
|
+
...record,
|
|
283
|
+
existed: true,
|
|
284
|
+
created: false,
|
|
285
|
+
});
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Create the record
|
|
290
|
+
try {
|
|
291
|
+
await api.upsertRecords(rootDomain, [
|
|
292
|
+
{
|
|
293
|
+
name: record.subdomain,
|
|
294
|
+
type: 'A',
|
|
295
|
+
ttl,
|
|
296
|
+
records: [{ content: record.value }],
|
|
297
|
+
},
|
|
298
|
+
]);
|
|
299
|
+
|
|
300
|
+
results.push({
|
|
301
|
+
...record,
|
|
302
|
+
created: true,
|
|
303
|
+
existed: false,
|
|
304
|
+
});
|
|
305
|
+
} catch (error) {
|
|
306
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
307
|
+
results.push({
|
|
308
|
+
...record,
|
|
309
|
+
error: message,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return results;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Main DNS orchestration function for deployments
|
|
319
|
+
*/
|
|
320
|
+
export async function orchestrateDns(
|
|
321
|
+
appHostnames: Map<string, string>, // appName -> hostname
|
|
322
|
+
dnsConfig: DnsConfig | undefined,
|
|
323
|
+
dokployEndpoint: string,
|
|
324
|
+
): Promise<DnsCreationResult | null> {
|
|
325
|
+
if (!dnsConfig) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const { domain: rootDomain, autoCreate = true } = dnsConfig;
|
|
330
|
+
|
|
331
|
+
// Resolve Dokploy server IP from endpoint
|
|
332
|
+
logger.log('\n🌐 Setting up DNS records...');
|
|
333
|
+
let serverIp: string;
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const endpointUrl = new URL(dokployEndpoint);
|
|
337
|
+
serverIp = await resolveHostnameToIp(endpointUrl.hostname);
|
|
338
|
+
logger.log(` Server IP: ${serverIp} (from ${endpointUrl.hostname})`);
|
|
339
|
+
} catch (error) {
|
|
340
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
341
|
+
logger.log(` ⚠ Failed to resolve server IP: ${message}`);
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Generate required records
|
|
346
|
+
const requiredRecords = generateRequiredRecords(
|
|
347
|
+
appHostnames,
|
|
348
|
+
rootDomain,
|
|
349
|
+
serverIp,
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
if (requiredRecords.length === 0) {
|
|
353
|
+
logger.log(' No DNS records needed');
|
|
354
|
+
return { records: [], success: true, serverIp };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Create records if auto-create is enabled
|
|
358
|
+
let finalRecords: RequiredDnsRecord[];
|
|
359
|
+
|
|
360
|
+
if (autoCreate && dnsConfig.provider !== 'manual') {
|
|
361
|
+
logger.log(` Creating DNS records at ${dnsConfig.provider}...`);
|
|
362
|
+
finalRecords = await createDnsRecords(requiredRecords, dnsConfig);
|
|
363
|
+
|
|
364
|
+
const created = finalRecords.filter((r) => r.created).length;
|
|
365
|
+
const existed = finalRecords.filter((r) => r.existed).length;
|
|
366
|
+
const failed = finalRecords.filter((r) => r.error).length;
|
|
367
|
+
|
|
368
|
+
if (created > 0) {
|
|
369
|
+
logger.log(` ✓ Created ${created} DNS record(s)`);
|
|
370
|
+
}
|
|
371
|
+
if (existed > 0) {
|
|
372
|
+
logger.log(` ✓ ${existed} record(s) already exist`);
|
|
373
|
+
}
|
|
374
|
+
if (failed > 0) {
|
|
375
|
+
logger.log(` ⚠ ${failed} record(s) failed`);
|
|
376
|
+
}
|
|
377
|
+
} else {
|
|
378
|
+
finalRecords = requiredRecords;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Print summary table
|
|
382
|
+
printDnsRecordsTable(finalRecords, rootDomain);
|
|
383
|
+
|
|
384
|
+
// If manual mode or some failed, print simple instructions
|
|
385
|
+
const hasFailures = finalRecords.some((r) => r.error);
|
|
386
|
+
if (dnsConfig.provider === 'manual' || hasFailures) {
|
|
387
|
+
printDnsRecordsSimple(
|
|
388
|
+
finalRecords.filter((r) => !r.created && !r.existed),
|
|
389
|
+
rootDomain,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
records: finalRecords,
|
|
395
|
+
success: !hasFailures,
|
|
396
|
+
serverIp,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
@@ -671,6 +671,18 @@ export class DokployApi {
|
|
|
671
671
|
);
|
|
672
672
|
}
|
|
673
673
|
|
|
674
|
+
/**
|
|
675
|
+
* Validate a domain and trigger SSL certificate generation
|
|
676
|
+
*
|
|
677
|
+
* This should be called after DNS records are created and propagated.
|
|
678
|
+
* It triggers Let's Encrypt certificate generation for HTTPS domains.
|
|
679
|
+
*
|
|
680
|
+
* @param domain - The domain hostname to validate (e.g., 'api.example.com')
|
|
681
|
+
*/
|
|
682
|
+
async validateDomain(domain: string): Promise<{ isValid: boolean; resolvedIp: string }> {
|
|
683
|
+
return this.post<{ isValid: boolean; resolvedIp: string }>('domain.validateDomain', { domain });
|
|
684
|
+
}
|
|
685
|
+
|
|
674
686
|
/**
|
|
675
687
|
* Auto-generate a domain name for an application
|
|
676
688
|
*/
|
package/src/deploy/index.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
setRedisId,
|
|
36
36
|
writeStageState,
|
|
37
37
|
} from './state.js';
|
|
38
|
+
import { orchestrateDns } from './dns/index.js';
|
|
38
39
|
import {
|
|
39
40
|
generatePublicUrlBuildArgs,
|
|
40
41
|
getPublicUrlArgNames,
|
|
@@ -925,6 +926,10 @@ export async function workspaceDeployCommand(
|
|
|
925
926
|
const results: AppDeployResult[] = [];
|
|
926
927
|
const dokployConfig = workspace.deploy.dokploy;
|
|
927
928
|
|
|
929
|
+
// Track domain IDs and hostnames for DNS orchestration
|
|
930
|
+
const appHostnames = new Map<string, string>(); // appName -> hostname
|
|
931
|
+
const appDomainIds = new Map<string, string>(); // appName -> domainId
|
|
932
|
+
|
|
928
933
|
// ==================================================================
|
|
929
934
|
// PHASE 1: Deploy backend apps (with encrypted secrets)
|
|
930
935
|
// ==================================================================
|
|
@@ -1027,31 +1032,51 @@ export async function workspaceDeployCommand(
|
|
|
1027
1032
|
await api.deployApplication(application.applicationId);
|
|
1028
1033
|
|
|
1029
1034
|
// Create domain for this app
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
);
|
|
1035
|
+
const backendHost = resolveHost(
|
|
1036
|
+
appName,
|
|
1037
|
+
app,
|
|
1038
|
+
stage,
|
|
1039
|
+
dokployConfig,
|
|
1040
|
+
false, // Backend apps are not main frontend
|
|
1041
|
+
);
|
|
1038
1042
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1043
|
+
try {
|
|
1044
|
+
const domain = await api.createDomain({
|
|
1045
|
+
host: backendHost,
|
|
1041
1046
|
port: app.port,
|
|
1042
1047
|
https: true,
|
|
1043
1048
|
certificateType: 'letsencrypt',
|
|
1044
1049
|
applicationId: application.applicationId,
|
|
1045
1050
|
});
|
|
1046
1051
|
|
|
1047
|
-
|
|
1052
|
+
// Track for DNS orchestration
|
|
1053
|
+
appHostnames.set(appName, backendHost);
|
|
1054
|
+
appDomainIds.set(appName, domain.domainId);
|
|
1055
|
+
|
|
1056
|
+
const publicUrl = `https://${backendHost}`;
|
|
1048
1057
|
publicUrls[appName] = publicUrl;
|
|
1049
1058
|
logger.log(` ✓ Domain: ${publicUrl}`);
|
|
1050
1059
|
} catch (domainError) {
|
|
1051
|
-
// Domain might already exist, try to get
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1060
|
+
// Domain might already exist, try to get the existing domain
|
|
1061
|
+
appHostnames.set(appName, backendHost);
|
|
1062
|
+
|
|
1063
|
+
// Try to get existing domain ID for validation
|
|
1064
|
+
try {
|
|
1065
|
+
const existingDomains = await api.getDomainsByApplicationId(
|
|
1066
|
+
application.applicationId,
|
|
1067
|
+
);
|
|
1068
|
+
const matchingDomain = existingDomains.find(
|
|
1069
|
+
(d) => d.host === backendHost,
|
|
1070
|
+
);
|
|
1071
|
+
if (matchingDomain) {
|
|
1072
|
+
appDomainIds.set(appName, matchingDomain.domainId);
|
|
1073
|
+
}
|
|
1074
|
+
} catch {
|
|
1075
|
+
// Ignore - we'll just skip validation for this domain
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
publicUrls[appName] = `https://${backendHost}`;
|
|
1079
|
+
logger.log(` ℹ Domain already configured: https://${backendHost}`);
|
|
1055
1080
|
}
|
|
1056
1081
|
|
|
1057
1082
|
results.push({
|
|
@@ -1176,36 +1201,51 @@ export async function workspaceDeployCommand(
|
|
|
1176
1201
|
|
|
1177
1202
|
// Create domain for this app
|
|
1178
1203
|
const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
);
|
|
1204
|
+
const frontendHost = resolveHost(
|
|
1205
|
+
appName,
|
|
1206
|
+
app,
|
|
1207
|
+
stage,
|
|
1208
|
+
dokployConfig,
|
|
1209
|
+
isMainFrontend,
|
|
1210
|
+
);
|
|
1187
1211
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1212
|
+
try {
|
|
1213
|
+
const domain = await api.createDomain({
|
|
1214
|
+
host: frontendHost,
|
|
1190
1215
|
port: app.port,
|
|
1191
1216
|
https: true,
|
|
1192
1217
|
certificateType: 'letsencrypt',
|
|
1193
1218
|
applicationId: application.applicationId,
|
|
1194
1219
|
});
|
|
1195
1220
|
|
|
1196
|
-
|
|
1221
|
+
// Track for DNS orchestration
|
|
1222
|
+
appHostnames.set(appName, frontendHost);
|
|
1223
|
+
appDomainIds.set(appName, domain.domainId);
|
|
1224
|
+
|
|
1225
|
+
const publicUrl = `https://${frontendHost}`;
|
|
1197
1226
|
publicUrls[appName] = publicUrl;
|
|
1198
1227
|
logger.log(` ✓ Domain: ${publicUrl}`);
|
|
1199
1228
|
} catch (domainError) {
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1229
|
+
// Domain might already exist, try to get the existing domain
|
|
1230
|
+
appHostnames.set(appName, frontendHost);
|
|
1231
|
+
|
|
1232
|
+
// Try to get existing domain ID for validation
|
|
1233
|
+
try {
|
|
1234
|
+
const existingDomains = await api.getDomainsByApplicationId(
|
|
1235
|
+
application.applicationId,
|
|
1236
|
+
);
|
|
1237
|
+
const matchingDomain = existingDomains.find(
|
|
1238
|
+
(d) => d.host === frontendHost,
|
|
1239
|
+
);
|
|
1240
|
+
if (matchingDomain) {
|
|
1241
|
+
appDomainIds.set(appName, matchingDomain.domainId);
|
|
1242
|
+
}
|
|
1243
|
+
} catch {
|
|
1244
|
+
// Ignore - we'll just skip validation for this domain
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
publicUrls[appName] = `https://${frontendHost}`;
|
|
1248
|
+
logger.log(` ℹ Domain already configured: https://${frontendHost}`);
|
|
1209
1249
|
}
|
|
1210
1250
|
|
|
1211
1251
|
results.push({
|
|
@@ -1239,6 +1279,39 @@ export async function workspaceDeployCommand(
|
|
|
1239
1279
|
await writeStageState(workspace.root, stage, state);
|
|
1240
1280
|
logger.log(` ✓ State saved to .gkm/deploy-${stage}.json`);
|
|
1241
1281
|
|
|
1282
|
+
// ==================================================================
|
|
1283
|
+
// DNS: Create DNS records and validate domains for SSL
|
|
1284
|
+
// ==================================================================
|
|
1285
|
+
const dnsConfig = workspace.deploy.dns;
|
|
1286
|
+
if (dnsConfig && appHostnames.size > 0) {
|
|
1287
|
+
const dnsResult = await orchestrateDns(
|
|
1288
|
+
appHostnames,
|
|
1289
|
+
dnsConfig,
|
|
1290
|
+
creds.endpoint,
|
|
1291
|
+
);
|
|
1292
|
+
|
|
1293
|
+
// Validate domains to trigger SSL certificate generation
|
|
1294
|
+
if (dnsResult?.success && appHostnames.size > 0) {
|
|
1295
|
+
logger.log('\n🔒 Validating domains for SSL certificates...');
|
|
1296
|
+
for (const [appName, hostname] of appHostnames) {
|
|
1297
|
+
try {
|
|
1298
|
+
const result = await api.validateDomain(hostname);
|
|
1299
|
+
if (result.isValid) {
|
|
1300
|
+
logger.log(` ✓ ${appName}: ${hostname} → ${result.resolvedIp}`);
|
|
1301
|
+
} else {
|
|
1302
|
+
logger.log(` ⚠ ${appName}: ${hostname} not valid`);
|
|
1303
|
+
}
|
|
1304
|
+
} catch (validationError) {
|
|
1305
|
+
const message =
|
|
1306
|
+
validationError instanceof Error
|
|
1307
|
+
? validationError.message
|
|
1308
|
+
: 'Unknown error';
|
|
1309
|
+
logger.log(` ⚠ ${appName}: validation failed - ${message}`);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1242
1315
|
// ==================================================================
|
|
1243
1316
|
// Summary
|
|
1244
1317
|
// ==================================================================
|
package/src/docker/templates.ts
CHANGED
|
@@ -321,8 +321,8 @@ ENV NODE_ENV=production
|
|
|
321
321
|
ENV PORT=${port}
|
|
322
322
|
|
|
323
323
|
# Health check
|
|
324
|
-
HEALTHCHECK --interval=30s --timeout=
|
|
325
|
-
CMD wget -
|
|
324
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
|
|
325
|
+
CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
|
|
326
326
|
|
|
327
327
|
# Switch to non-root user
|
|
328
328
|
USER hono
|
|
@@ -413,8 +413,8 @@ COPY --from=builder --chown=hono:nodejs /app/.gkm/server/dist/server.mjs ./
|
|
|
413
413
|
ENV NODE_ENV=production
|
|
414
414
|
ENV PORT=${port}
|
|
415
415
|
|
|
416
|
-
HEALTHCHECK --interval=30s --timeout=
|
|
417
|
-
CMD wget -
|
|
416
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
|
|
417
|
+
CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
|
|
418
418
|
|
|
419
419
|
USER hono
|
|
420
420
|
|
|
@@ -452,8 +452,8 @@ ENV NODE_ENV=production
|
|
|
452
452
|
ENV PORT=${port}
|
|
453
453
|
|
|
454
454
|
# Health check
|
|
455
|
-
HEALTHCHECK --interval=30s --timeout=
|
|
456
|
-
CMD wget -
|
|
455
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
|
|
456
|
+
CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
|
|
457
457
|
|
|
458
458
|
# Switch to non-root user
|
|
459
459
|
USER hono
|
|
@@ -682,10 +682,6 @@ COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/.next/standalone ./
|
|
|
682
682
|
COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/.next/static ./${appPath}/.next/static
|
|
683
683
|
COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/public ./${appPath}/public
|
|
684
684
|
|
|
685
|
-
# Health check
|
|
686
|
-
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \\
|
|
687
|
-
CMD wget -q --spider http://localhost:${port}/ || exit 1
|
|
688
|
-
|
|
689
685
|
USER nextjs
|
|
690
686
|
|
|
691
687
|
EXPOSE ${port}
|
|
@@ -790,8 +786,8 @@ COPY --from=builder --chown=hono:nodejs /app/${appPath}/.gkm/server/dist/server.
|
|
|
790
786
|
ENV NODE_ENV=production
|
|
791
787
|
ENV PORT=${port}
|
|
792
788
|
|
|
793
|
-
HEALTHCHECK --interval=30s --timeout=
|
|
794
|
-
CMD wget -
|
|
789
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
|
|
790
|
+
CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
|
|
795
791
|
|
|
796
792
|
USER hono
|
|
797
793
|
|
|
@@ -930,8 +926,8 @@ COPY --from=builder --chown=app:nodejs /app/${appPath}/dist/index.mjs ./
|
|
|
930
926
|
ENV NODE_ENV=production
|
|
931
927
|
ENV PORT=${port}
|
|
932
928
|
|
|
933
|
-
HEALTHCHECK --interval=30s --timeout=
|
|
934
|
-
CMD wget -
|
|
929
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
|
|
930
|
+
CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
|
|
935
931
|
|
|
936
932
|
USER app
|
|
937
933
|
|