@@ -10,6 +10,9 @@ import { URL_PROTOCOL_PREFIX } from '@/utils/const';
10
10
const CONTAINER_STATE_FILE = path . join ( process . cwd ( ) , 'container-state.json' ) ;
11
11
const PORT_STATE_FILE = path . join ( process . cwd ( ) , 'port-state.json' ) ;
12
12
13
+ // Base image name - this is the single image we'll use for all containers
14
+ const BASE_IMAGE_NAME = 'frontend-base-image' ;
15
+
13
16
// In-memory container and port state
14
17
let runningContainers = new Map <
15
18
string ,
@@ -23,6 +26,15 @@ const processingRequests = new Set<string>();
23
26
// State lock to prevent concurrent reads/writes to state files
24
27
let isUpdatingState = false ;
25
28
29
+ // Flag to track if base image has been built
30
+ let baseImageBuilt = false ;
31
+
32
+ // limit memory usage for a container
33
+ const memoryLimit = '400m' ;
34
+
35
+ // limit cpu usage for a container
36
+ const cpusLimit = 1 ;
37
+
26
38
/**
27
39
* Initialize function, loads persisted state when service starts
28
40
*/
@@ -75,6 +87,9 @@ async function initializeState() {
75
87
// Save cleaned-up state
76
88
await saveState ( ) ;
77
89
90
+ // Check if base image exists
91
+ baseImageBuilt = await checkBaseImageExists ( ) ;
92
+
78
93
console . log (
79
94
'State initialization complete, cleaned up non-running containers and expired port allocations'
80
95
) ;
@@ -180,6 +195,21 @@ function checkContainerRunning(containerId: string): Promise<boolean> {
180
195
} ) ;
181
196
}
182
197
198
+ /**
199
+ * Check if base image exists
200
+ */
201
+ function checkBaseImageExists ( ) : Promise < boolean > {
202
+ return new Promise ( ( resolve ) => {
203
+ exec ( `docker image inspect ${ BASE_IMAGE_NAME } ` , ( err ) => {
204
+ if ( err ) {
205
+ resolve ( false ) ;
206
+ } else {
207
+ resolve ( true ) ;
208
+ }
209
+ } ) ;
210
+ } ) ;
211
+ }
212
+
183
213
/**
184
214
* Check if there's already a container running with the specified label
185
215
*/
@@ -203,27 +233,42 @@ async function checkExistingContainer(
203
233
}
204
234
205
235
/**
206
- * Remove node_modules and lock files
236
+ * Build base image if it doesn't exist
207
237
*/
208
- async function removeNodeModulesAndLockFiles ( directory : string ) {
209
- return new Promise < void > ( ( resolve , reject ) => {
210
- const removeCmd = `rm -rf "${ path . join ( directory , 'node_modules' ) } " \
211
- "${ path . join ( directory , 'yarn.lock' ) } " \
212
- "${ path . join ( directory , 'package-lock.json' ) } " \
213
- "${ path . join ( directory , 'pnpm-lock.yaml' ) } "` ;
214
-
215
- console . log ( `Cleaning up node_modules and lock files in: ${ directory } ` ) ;
216
- exec ( removeCmd , { timeout : 30000 } , ( err , stdout , stderr ) => {
217
- if ( err ) {
218
- console . error ( 'Error removing node_modules or lock files:' , stderr ) ;
219
- // Don't block the process, continue even if cleanup fails
220
- resolve ( ) ;
221
- return ;
222
- }
223
- console . log ( `Cleanup done: ${ stdout } ` ) ;
224
- resolve ( ) ;
225
- } ) ;
226
- } ) ;
238
+ async function ensureBaseImageExists ( ) : Promise < void > {
239
+ if ( baseImageBuilt ) {
240
+ return ;
241
+ }
242
+
243
+ try {
244
+ // Path to the base image Dockerfile
245
+ const dockerfilePath = path . join (
246
+ process . cwd ( ) ,
247
+ '../docker' ,
248
+ 'project-base-image'
249
+ ) ;
250
+
251
+ // Check if base Dockerfile exists
252
+ if ( ! fs . existsSync ( path . join ( dockerfilePath , 'Dockerfile' ) ) ) {
253
+ console . error ( 'Base Dockerfile not found at:' , dockerfilePath ) ;
254
+ throw new Error ( 'Base Dockerfile not found' ) ;
255
+ }
256
+
257
+ // Build the base image
258
+ console . log (
259
+ `Building base image ${ BASE_IMAGE_NAME } from ${ dockerfilePath } ...`
260
+ ) ;
261
+ await execWithTimeout (
262
+ `docker build -t ${ BASE_IMAGE_NAME } ${ dockerfilePath } ` ,
263
+ { timeout : 300000 , retries : 1 } // 5 minutes timeout, 1 retry
264
+ ) ;
265
+
266
+ baseImageBuilt = true ;
267
+ console . log ( `Base image ${ BASE_IMAGE_NAME } built successfully` ) ;
268
+ } catch ( error ) {
269
+ console . error ( 'Error building base image:' , error ) ;
270
+ throw new Error ( 'Failed to build base image' ) ;
271
+ }
227
272
}
228
273
229
274
/**
@@ -265,9 +310,9 @@ function execWithTimeout(
265
310
}
266
311
267
312
/**
268
- * Build and run Docker container
313
+ * Run Docker container using the base image
269
314
*/
270
- async function buildAndRunDocker (
315
+ async function runDockerContainer (
271
316
projectPath : string
272
317
) : Promise < { domain : string ; containerId : string ; port : number } > {
273
318
const traefikDomain = process . env . TRAEFIK_DOMAIN || 'docker.localhost' ;
@@ -307,25 +352,17 @@ async function buildAndRunDocker(
307
352
}
308
353
}
309
354
355
+ // Ensure base image exists
356
+ await ensureBaseImageExists ( ) ;
357
+
310
358
const directory = path . join ( getProjectPath ( projectPath ) , 'frontend' ) ;
311
359
const subdomain = projectPath . replace ( / [ ^ \w - ] / g, '' ) . toLowerCase ( ) ;
312
- const imageName = subdomain ;
313
360
const containerName = `container-${ subdomain } ` ;
314
361
const domain = `${ subdomain } .${ traefikDomain } ` ;
315
362
316
363
// Allocate port
317
364
const exposedPort = await findAvailablePort ( ) ;
318
365
319
- // Remove node_modules and lock files
320
- try {
321
- await removeNodeModulesAndLockFiles ( directory ) ;
322
- } catch ( error ) {
323
- console . error (
324
- 'Error during cleanup phase, but will continue with build:' ,
325
- error
326
- ) ;
327
- }
328
-
329
366
try {
330
367
// Check if a container with the same name already exists, remove it if found
331
368
try {
@@ -342,22 +379,15 @@ async function buildAndRunDocker(
342
379
// If container doesn't exist, this will error out which is expected
343
380
}
344
381
345
- // Build Docker image
346
- console . log (
347
- `Starting Docker build for image: ${ imageName } in directory: ${ directory } `
348
- ) ;
349
- await execWithTimeout (
350
- `docker build -t ${ imageName } ${ directory } ` ,
351
- { timeout : 300000 , retries : 1 } // 5 minutes timeout, 1 retry
352
- ) ;
353
-
354
382
// Determine whether to use TLS or non-TLS configuration
355
383
const TLS = process . env . TLS === 'true' ;
356
384
357
385
// Configure Docker run command
358
386
let runCommand ;
359
387
if ( TLS ) {
360
388
runCommand = `docker run -d --name ${ containerName } -l "traefik.enable=true" \
389
+ --memory=${ memoryLimit } --memory-swap=${ memoryLimit } \
390
+ --cpus=${ cpusLimit } \
361
391
-l "traefik.http.routers.${ subdomain } .rule=Host(\\"${ domain } \\")" \
362
392
-l "traefik.http.routers.${ subdomain } .entrypoints=websecure" \
363
393
-l "traefik.http.routers.${ subdomain } .tls=true" \
@@ -368,9 +398,11 @@ async function buildAndRunDocker(
368
398
-l "traefik.http.routers.${ subdomain } .middlewares=${ subdomain } -cors" \
369
399
--network=docker_traefik_network -p ${ exposedPort } :5173 \
370
400
-v "${ directory } :/app" \
371
- ${ imageName } ` ;
401
+ ${ BASE_IMAGE_NAME } ` ;
372
402
} else {
373
403
runCommand = `docker run -d --name ${ containerName } -l "traefik.enable=true" \
404
+ --memory=${ memoryLimit } --memory-swap=${ memoryLimit } \
405
+ --cpus=${ cpusLimit } \
374
406
-l "traefik.http.routers.${ subdomain } .rule=Host(\\"${ domain } \\")" \
375
407
-l "traefik.http.routers.${ subdomain } .entrypoints=web" \
376
408
-l "traefik.http.services.${ subdomain } .loadbalancer.server.port=5173" \
@@ -380,7 +412,7 @@ async function buildAndRunDocker(
380
412
-l "traefik.http.routers.${ subdomain } .middlewares=${ subdomain } -cors" \
381
413
--network=docker_traefik_network -p ${ exposedPort } :5173 \
382
414
-v "${ directory } :/app" \
383
- ${ imageName } ` ;
415
+ ${ BASE_IMAGE_NAME } ` ;
384
416
}
385
417
386
418
// Run container
@@ -414,7 +446,7 @@ async function buildAndRunDocker(
414
446
) ;
415
447
return { domain, containerId : containerActualId , port : exposedPort } ;
416
448
} catch ( error : any ) {
417
- console . error ( `Error building or running container:` , error ) ;
449
+ console . error ( `Error running container:` , error ) ;
418
450
419
451
// Clean up allocated port
420
452
allocatedPorts . delete ( exposedPort ) ;
@@ -499,15 +531,15 @@ export async function GET(req: Request) {
499
531
// Prevent duplicate builds
500
532
if ( processingRequests . has ( projectPath ) ) {
501
533
return NextResponse . json ( {
502
- message : 'Build in progress' ,
534
+ message : 'Container creation in progress' ,
503
535
status : 'pending' ,
504
536
} ) ;
505
537
}
506
538
507
539
processingRequests . add ( projectPath ) ;
508
540
509
541
try {
510
- const { domain, containerId } = await buildAndRunDocker ( projectPath ) ;
542
+ const { domain, containerId } = await runDockerContainer ( projectPath ) ;
511
543
512
544
return NextResponse . json ( {
513
545
message : 'Docker container started' ,
0 commit comments