@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.
Files changed (35) hide show
  1. package/dist/config.d.cts +1 -1
  2. package/dist/config.d.mts +1 -1
  3. package/dist/{dokploy-api-D8a0eQQB.cjs → dokploy-api-BDLu0qWi.cjs} +12 -1
  4. package/dist/dokploy-api-BDLu0qWi.cjs.map +1 -0
  5. package/dist/dokploy-api-BN3V57z1.mjs +3 -0
  6. package/dist/dokploy-api-BdCKjFDA.cjs +3 -0
  7. package/dist/{dokploy-api-b6usLLKk.mjs → dokploy-api-DvzIDxTj.mjs} +12 -1
  8. package/dist/dokploy-api-DvzIDxTj.mjs.map +1 -0
  9. package/dist/{index-BtnjoghR.d.mts → index-A70abJ1m.d.mts} +60 -2
  10. package/dist/index-A70abJ1m.d.mts.map +1 -0
  11. package/dist/{index-c89X2mi2.d.cts → index-pOA56MWT.d.cts} +60 -2
  12. package/dist/index-pOA56MWT.d.cts.map +1 -0
  13. package/dist/index.cjs +685 -249
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.mjs +685 -249
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/workspace/index.d.cts +1 -1
  18. package/dist/workspace/index.d.mts +1 -1
  19. package/dist/workspace-CaVW6j2q.cjs.map +1 -1
  20. package/dist/workspace-DLFRaDc-.mjs.map +1 -1
  21. package/package.json +3 -3
  22. package/src/auth/credentials.ts +66 -0
  23. package/src/deploy/dns/hostinger-api.ts +258 -0
  24. package/src/deploy/dns/index.ts +398 -0
  25. package/src/deploy/dokploy-api.ts +12 -0
  26. package/src/deploy/index.ts +108 -35
  27. package/src/docker/templates.ts +10 -14
  28. package/src/workspace/types.ts +64 -1
  29. package/tsconfig.tsbuildinfo +1 -1
  30. package/dist/dokploy-api-C1JgU9Vr.mjs +0 -3
  31. package/dist/dokploy-api-Cpq_tLSz.cjs +0 -3
  32. package/dist/dokploy-api-D8a0eQQB.cjs.map +0 -1
  33. package/dist/dokploy-api-b6usLLKk.mjs.map +0 -1
  34. package/dist/index-BtnjoghR.d.mts.map +0 -1
  35. 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
  */
@@ -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
- try {
1031
- const host = resolveHost(
1032
- appName,
1033
- app,
1034
- stage,
1035
- dokployConfig,
1036
- false, // Backend apps are not main frontend
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
- await api.createDomain({
1040
- host,
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
- const publicUrl = `https://${host}`;
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 public URL anyway
1052
- const host = resolveHost(appName, app, stage, dokployConfig, false);
1053
- publicUrls[appName] = `https://${host}`;
1054
- logger.log(` ℹ Domain already configured: https://${host}`);
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
- try {
1180
- const host = resolveHost(
1181
- appName,
1182
- app,
1183
- stage,
1184
- dokployConfig,
1185
- isMainFrontend,
1186
- );
1204
+ const frontendHost = resolveHost(
1205
+ appName,
1206
+ app,
1207
+ stage,
1208
+ dokployConfig,
1209
+ isMainFrontend,
1210
+ );
1187
1211
 
1188
- await api.createDomain({
1189
- host,
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
- const publicUrl = `https://${host}`;
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
- const host = resolveHost(
1201
- appName,
1202
- app,
1203
- stage,
1204
- dokployConfig,
1205
- isMainFrontend,
1206
- );
1207
- publicUrls[appName] = `https://${host}`;
1208
- logger.log(` ℹ Domain already configured: https://${host}`);
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
  // ==================================================================
@@ -321,8 +321,8 @@ ENV NODE_ENV=production
321
321
  ENV PORT=${port}
322
322
 
323
323
  # Health check
324
- HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
325
- CMD wget -q --spider http://localhost:${port}${healthCheckPath} || exit 1
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=3s --start-period=5s --retries=3 \\
417
- CMD wget -q --spider http://localhost:${port}${healthCheckPath} || exit 1
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=3s --start-period=5s --retries=3 \\
456
- CMD wget -q --spider http://localhost:${port}${healthCheckPath} || exit 1
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=3s --start-period=5s --retries=3 \\
794
- CMD wget -q --spider http://localhost:${port}${healthCheckPath} || exit 1
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=3s --start-period=5s --retries=3 \\
934
- CMD wget -q --spider http://localhost:${port}${healthCheckPath} || exit 1
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