Skip to content

Commit ef243ae

Browse files
feat(frontend): frontend docker improving (#175)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced an updated container configuration to optimize startup speed, dependency setup, and security. - Enhanced container management with an automatic check and build process for a required base image along with improved resource allocation. - Upgraded the preview experience with a dynamic service readiness monitor that provides live feedback and a manual retry option. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent bf82dbf commit ef243ae

File tree

3 files changed

+282
-63
lines changed

3 files changed

+282
-63
lines changed

docker/project-base-image/Dockerfile

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
FROM node:20
2+
3+
WORKDIR /app
4+
5+
# Pre-install common frontend dependencies to speed up project startup
6+
RUN npm install -g npm@latest vite@latest
7+
8+
# Create a non-root user to run the app
9+
RUN groupadd -r appuser && useradd -r -g appuser -m appuser
10+
RUN chown -R appuser:appuser /app
11+
12+
# Switch to non-root user for security
13+
USER appuser
14+
15+
EXPOSE 5173
16+
17+
# The actual project code will be mounted as a volume
18+
# The CMD will be provided when running the container
19+
CMD ["sh", "-c", "npm install --include=dev && npm run dev -- --host 0.0.0.0"]

frontend/src/app/api/runProject/route.ts

+79-47
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import { URL_PROTOCOL_PREFIX } from '@/utils/const';
1010
const CONTAINER_STATE_FILE = path.join(process.cwd(), 'container-state.json');
1111
const PORT_STATE_FILE = path.join(process.cwd(), 'port-state.json');
1212

13+
// Base image name - this is the single image we'll use for all containers
14+
const BASE_IMAGE_NAME = 'frontend-base-image';
15+
1316
// In-memory container and port state
1417
let runningContainers = new Map<
1518
string,
@@ -23,6 +26,15 @@ const processingRequests = new Set<string>();
2326
// State lock to prevent concurrent reads/writes to state files
2427
let isUpdatingState = false;
2528

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+
2638
/**
2739
* Initialize function, loads persisted state when service starts
2840
*/
@@ -75,6 +87,9 @@ async function initializeState() {
7587
// Save cleaned-up state
7688
await saveState();
7789

90+
// Check if base image exists
91+
baseImageBuilt = await checkBaseImageExists();
92+
7893
console.log(
7994
'State initialization complete, cleaned up non-running containers and expired port allocations'
8095
);
@@ -180,6 +195,21 @@ function checkContainerRunning(containerId: string): Promise<boolean> {
180195
});
181196
}
182197

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+
183213
/**
184214
* Check if there's already a container running with the specified label
185215
*/
@@ -203,27 +233,42 @@ async function checkExistingContainer(
203233
}
204234

205235
/**
206-
* Remove node_modules and lock files
236+
* Build base image if it doesn't exist
207237
*/
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+
}
227272
}
228273

229274
/**
@@ -265,9 +310,9 @@ function execWithTimeout(
265310
}
266311

267312
/**
268-
* Build and run Docker container
313+
* Run Docker container using the base image
269314
*/
270-
async function buildAndRunDocker(
315+
async function runDockerContainer(
271316
projectPath: string
272317
): Promise<{ domain: string; containerId: string; port: number }> {
273318
const traefikDomain = process.env.TRAEFIK_DOMAIN || 'docker.localhost';
@@ -307,25 +352,17 @@ async function buildAndRunDocker(
307352
}
308353
}
309354

355+
// Ensure base image exists
356+
await ensureBaseImageExists();
357+
310358
const directory = path.join(getProjectPath(projectPath), 'frontend');
311359
const subdomain = projectPath.replace(/[^\w-]/g, '').toLowerCase();
312-
const imageName = subdomain;
313360
const containerName = `container-${subdomain}`;
314361
const domain = `${subdomain}.${traefikDomain}`;
315362

316363
// Allocate port
317364
const exposedPort = await findAvailablePort();
318365

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-
329366
try {
330367
// Check if a container with the same name already exists, remove it if found
331368
try {
@@ -342,22 +379,15 @@ async function buildAndRunDocker(
342379
// If container doesn't exist, this will error out which is expected
343380
}
344381

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-
354382
// Determine whether to use TLS or non-TLS configuration
355383
const TLS = process.env.TLS === 'true';
356384

357385
// Configure Docker run command
358386
let runCommand;
359387
if (TLS) {
360388
runCommand = `docker run -d --name ${containerName} -l "traefik.enable=true" \
389+
--memory=${memoryLimit} --memory-swap=${memoryLimit} \
390+
--cpus=${cpusLimit} \
361391
-l "traefik.http.routers.${subdomain}.rule=Host(\\"${domain}\\")" \
362392
-l "traefik.http.routers.${subdomain}.entrypoints=websecure" \
363393
-l "traefik.http.routers.${subdomain}.tls=true" \
@@ -368,9 +398,11 @@ async function buildAndRunDocker(
368398
-l "traefik.http.routers.${subdomain}.middlewares=${subdomain}-cors" \
369399
--network=docker_traefik_network -p ${exposedPort}:5173 \
370400
-v "${directory}:/app" \
371-
${imageName}`;
401+
${BASE_IMAGE_NAME}`;
372402
} else {
373403
runCommand = `docker run -d --name ${containerName} -l "traefik.enable=true" \
404+
--memory=${memoryLimit} --memory-swap=${memoryLimit} \
405+
--cpus=${cpusLimit} \
374406
-l "traefik.http.routers.${subdomain}.rule=Host(\\"${domain}\\")" \
375407
-l "traefik.http.routers.${subdomain}.entrypoints=web" \
376408
-l "traefik.http.services.${subdomain}.loadbalancer.server.port=5173" \
@@ -380,7 +412,7 @@ async function buildAndRunDocker(
380412
-l "traefik.http.routers.${subdomain}.middlewares=${subdomain}-cors" \
381413
--network=docker_traefik_network -p ${exposedPort}:5173 \
382414
-v "${directory}:/app" \
383-
${imageName}`;
415+
${BASE_IMAGE_NAME}`;
384416
}
385417

386418
// Run container
@@ -414,7 +446,7 @@ async function buildAndRunDocker(
414446
);
415447
return { domain, containerId: containerActualId, port: exposedPort };
416448
} catch (error: any) {
417-
console.error(`Error building or running container:`, error);
449+
console.error(`Error running container:`, error);
418450

419451
// Clean up allocated port
420452
allocatedPorts.delete(exposedPort);
@@ -499,15 +531,15 @@ export async function GET(req: Request) {
499531
// Prevent duplicate builds
500532
if (processingRequests.has(projectPath)) {
501533
return NextResponse.json({
502-
message: 'Build in progress',
534+
message: 'Container creation in progress',
503535
status: 'pending',
504536
});
505537
}
506538

507539
processingRequests.add(projectPath);
508540

509541
try {
510-
const { domain, containerId } = await buildAndRunDocker(projectPath);
542+
const { domain, containerId } = await runDockerContainer(projectPath);
511543

512544
return NextResponse.json({
513545
message: 'Docker container started',

0 commit comments

Comments
 (0)