Skip to content

Commit e441064

Browse files
feat : multi-stage build and optimization
fix: copilot reviews fix: non static deployement fix: non static deployement fix: non static deployement fix: non static deployement fix: non static deployement fix: non static deployement fix: non static deployement fix: non static deployement
1 parent a0c516e commit e441064

3 files changed

Lines changed: 115 additions & 41 deletions

File tree

src/backend/scripts.ts

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,79 @@
11
import { exec } from "./dependencies.ts";
2-
import dockerize from "./utils/container.ts";
2+
import dockerize, { dockerignore } from "./utils/container.ts";
33
import DfContentMap from "./types/maps_interface.ts";
44

55
const MEMORY_LIMIT = Deno.env.get("MEMORY_LIMIT");
66

7+
function shellEscape(input: string, label = "input"): string {
8+
if (!input) return "";
9+
if (input.startsWith("-")) {
10+
throw new Error(`[scripts] Invalid ${label}: cannot start with a hyphen`);
11+
}
12+
const safeCharPattern = /^[a-zA-Z0-9.\-_:/~?=#]+$/;
13+
14+
if (!safeCharPattern.test(input)) {
15+
throw new Error(`[scripts] Invalid characters in ${label}: ${input}`);
16+
}
17+
return input;
18+
}
19+
20+
async function safeExec(command: string): Promise<void> {
21+
try {
22+
await exec(command);
23+
} catch (error) {
24+
console.error(`[scripts] exec failed: ${command}`);
25+
console.error(error);
26+
throw error;
27+
}
28+
}
29+
730
async function addScript(
831
document: DfContentMap,
932
env_content: string,
1033
static_content: string,
11-
dockerfile_present:string,
34+
dockerfile_present: string,
1235
stack: string,
1336
port: string,
1437
build_cmds: string,
1538
) {
39+
const subdomain = shellEscape(document.subdomain, "subdomain");
40+
const resource = shellEscape(document.resource, "resource");
41+
const safePort = shellEscape(port, "port");
42+
const memLimit = shellEscape(MEMORY_LIMIT || "512m", "MEMORY_LIMIT");
43+
1644
if (document.resource_type === "URL") {
17-
await exec(
18-
`bash -c "echo 'bash ../../src/backend/shell_scripts/automate.sh -u ${document.resource} ${document.subdomain}' > /hostpipe/pipe"`,
45+
await safeExec(
46+
`bash -c "echo 'bash ../../src/backend/shell_scripts/automate.sh -u ${resource} ${subdomain}' > /hostpipe/pipe"`,
1947
);
2048
} else if (document.resource_type === "PORT") {
21-
await exec(
22-
`bash -c "echo 'bash ../../src/backend/shell_scripts/automate.sh -p ${document.resource} ${document.subdomain}' > /hostpipe/pipe"`,
49+
await safeExec(
50+
`bash -c "echo 'bash ../../src/backend/shell_scripts/automate.sh -p ${resource} ${subdomain}' > /hostpipe/pipe"`,
2351
);
2452
} else if (document.resource_type === "GITHUB" && static_content == "Yes") {
25-
Deno.writeTextFile(`/hostpipe/.env`, env_content);
26-
await exec(
27-
`bash -c "echo 'bash ../../src/backend/shell_scripts/container.sh -s ${document.subdomain} ${document.resource} 80 ${MEMORY_LIMIT}' > /hostpipe/pipe"`,
53+
await Deno.writeTextFile(`/hostpipe/.env`, env_content);
54+
await safeExec(
55+
`bash -c "echo 'bash ../../src/backend/shell_scripts/container.sh -s ${subdomain} ${resource} 80 ${memLimit}' > /hostpipe/pipe"`,
2856
);
2957
} else if (document.resource_type === "GITHUB" && static_content == "No") {
30-
if(dockerfile_present === 'No'){
31-
const dockerfile = dockerize(stack, port, build_cmds);
32-
Deno.writeTextFile(`/hostpipe/Dockerfile`, dockerfile);
33-
Deno.writeTextFile(`/hostpipe/.env`, env_content);
34-
await exec(
35-
`bash -c "echo 'bash ../../src/backend/shell_scripts/container.sh -g ${document.subdomain} ${document.resource} ${port} ${MEMORY_LIMIT}' > /hostpipe/pipe"`,
36-
);
37-
}else if(dockerfile_present === 'Yes'){
38-
39-
await exec(
40-
`bash -c "echo 'bash ../../src/backend/shell_scripts/container.sh -d ${document.subdomain} ${document.resource} ${port} ${MEMORY_LIMIT}' > /hostpipe/pipe"`,
58+
if (dockerfile_present === 'No') {
59+
await Deno.writeTextFile(`/hostpipe/Dockerfile`, dockerize(stack, safePort, build_cmds));
60+
await Deno.writeTextFile(`/hostpipe/.dockerignore`, dockerignore(stack));
61+
await Deno.writeTextFile(`/hostpipe/.env`, env_content);
62+
await safeExec(
63+
`bash -c "echo 'bash ../../src/backend/shell_scripts/container.sh -g ${subdomain} ${resource} ${safePort} ${memLimit}' > /hostpipe/pipe"`,
64+
);
65+
} else if (dockerfile_present === 'Yes') {
66+
await safeExec(
67+
`bash -c "echo 'bash ../../src/backend/shell_scripts/container.sh -d ${subdomain} ${resource} ${safePort} ${memLimit}' > /hostpipe/pipe"`,
4168
);
4269
}
43-
4470
}
4571
}
4672

4773
async function deleteScript(document: DfContentMap) {
48-
await exec(
49-
`bash -c "echo 'bash ../../src/backend/shell_scripts/delete.sh ${document.subdomain}' > /hostpipe/pipe"`,
74+
const subdomain = shellEscape(document.subdomain, "subdomain");
75+
await safeExec(
76+
`bash -c "echo 'bash ../../src/backend/shell_scripts/delete.sh ${subdomain}' > /hostpipe/pipe"`,
5077
);
5178
}
5279

src/backend/shell_scripts/container.sh

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ cd $name
2323

2424
if [ $flag = "-g" ]; then
2525
sudo cp ../Dockerfile ./
26+
sudo cp ../.dockerignore ./ 2>/dev/null || true
2627
elif [ $flag = "-s" ]; then
2728
sudo echo "
2829
FROM nginx:alpine
@@ -31,18 +32,18 @@ elif [ $flag = "-s" ]; then
3132
fi
3233

3334
sudo docker build -t $name .
34-
sudo docker run --memory=$max_mem --name=$name -d -p ${available_ports[$AVAILABLE]}:$exp_port $2
35+
sudo docker run --memory=$max_mem --name=$name -d -p ${available_ports[$AVAILABLE]}:$exp_port $name
3536
cd ..
3637
sudo rm -rf $name
3738
sudo rm Dockerfile
3839
sudo rm .env
39-
sudo touch /etc/nginx/sites-available/$2.conf
40-
sudo chmod 666 /etc/nginx/sites-available/$2.conf
41-
sudo echo "# Virtual Host configuration for $2
40+
sudo touch /etc/nginx/sites-available/$name.conf
41+
sudo chmod 666 /etc/nginx/sites-available/$name.conf
42+
sudo echo "# Virtual Host configuration for $name
4243
server {
4344
listen 80;
4445
listen [::]:80;
45-
server_name $2;
46+
server_name $name;
4647
location / {
4748
proxy_pass http://localhost:${available_ports[$AVAILABLE]};
4849
proxy_http_version 1.1;
@@ -53,6 +54,6 @@ sudo echo "# Virtual Host configuration for $2
5354
}
5455
charset utf-8;
5556
client_max_body_size 20M;
56-
}" > /etc/nginx/sites-available/$2.conf
57-
sudo ln -s /etc/nginx/sites-available/$2.conf /etc/nginx/sites-enabled/$2.conf
57+
}" > /etc/nginx/sites-available/$name.conf
58+
sudo ln -s /etc/nginx/sites-available/$name.conf /etc/nginx/sites-enabled/$name.conf
5859
sudo systemctl reload nginx

src/backend/utils/container.ts

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,65 @@ export default function dockerize(
55
) {
66
let dockerfile = "";
77
build_cmds = build_cmds.replace(/\r?\n$/, '');
8-
const run_cmd = build_cmds.split("\n");
9-
const execute_cmd = "CMD " + JSON.stringify(run_cmd.pop()?.split(" "));
10-
const build_cmds_mapped = run_cmd.map((elem) => {
11-
return "RUN " + elem;
12-
}).join("\n");
8+
const run_cmd = build_cmds.split("\n").map(c => c.trim()).filter(Boolean);
9+
const last_cmd = run_cmd.pop();
10+
if (!last_cmd) {
11+
throw new Error("build_cmds must contain at least one valid execution command");
12+
}
13+
const execute_cmd = "CMD " + JSON.stringify(last_cmd.split(" "));
14+
const build_steps = run_cmd.filter(Boolean).map((cmd) => `RUN ${cmd}`);
1315
if (stack == "Python") {
14-
dockerfile =
15-
"FROM python:latest \nWORKDIR /app \nCOPY requirements.txt . \nRUN pip install --no-cache-dir -r requirements.txt \nCOPY . ." +
16-
build_cmds_mapped + `\nEXPOSE ${port}\n` + execute_cmd;
16+
dockerfile = [
17+
"FROM python:3.12-slim AS builder",
18+
"WORKDIR /app",
19+
"RUN python -m venv /opt/venv",
20+
"ENV PATH=\"/opt/venv/bin:$PATH\"",
21+
"COPY . .",
22+
"RUN if [ -f requirements.txt ]; then pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir -r requirements.txt; fi",
23+
...build_steps,
24+
"",
25+
"FROM python:3.12-slim",
26+
"RUN groupadd -r appuser && useradd -r -g appuser appuser",
27+
"WORKDIR /app",
28+
"COPY --from=builder /opt/venv /opt/venv",
29+
"COPY --from=builder /app /app",
30+
"ENV PATH=\"/opt/venv/bin:$PATH\"",
31+
"USER appuser",
32+
`EXPOSE ${port}`,
33+
execute_cmd,
34+
].join("\n");
1735
} else if (stack == "NodeJS") {
18-
dockerfile =
19-
"FROM node:latest \nWORKDIR /app \nCOPY ./package*.json . \nRUN npm install \nCOPY . ." +
20-
build_cmds_mapped + `\nEXPOSE ${port} \n` + execute_cmd;
36+
dockerfile = [
37+
"FROM node:22-alpine AS builder",
38+
"WORKDIR /app",
39+
"COPY . .",
40+
"RUN if [ -f package.json ]; then npm install && npm cache clean --force; fi",
41+
...build_steps,
42+
"RUN rm -rf node_modules",
43+
"",
44+
"FROM node:22-alpine",
45+
"WORKDIR /app",
46+
"ENV NODE_ENV=production",
47+
"COPY --from=builder /app ./",
48+
"RUN if [ -f package.json ]; then npm install --omit=dev && npm cache clean --force; fi",
49+
"USER node",
50+
`EXPOSE ${port}`,
51+
execute_cmd,
52+
].join("\n");
2153
}
2254
return dockerfile.toString();
2355
}
56+
57+
export function dockerignore(stack: string): string {
58+
const common = [
59+
".git", ".gitignore", ".dockerignore",
60+
"*.md", ".DS_Store",
61+
];
62+
63+
const stackRules: Record<string, string[]> = {
64+
Python: ["__pycache__/", "*.pyc", "*.pyo", ".venv/", "dist/", "*.egg-info/"],
65+
NodeJS: ["node_modules/", "dist/", ".npm/", "*.log", "coverage/"],
66+
};
67+
68+
return [...common, ...(stackRules[stack] ?? [])].join("\n") + "\n";
69+
}

0 commit comments

Comments
 (0)