build: optimize Dockerfiles for production and pin dependencies

Implement multi-stage build and static server for frontend
Run containers as non-root user and set production environment
Pin all package dependencies to exact versions
This commit is contained in:
2026-05-03 15:35:00 +08:00
parent 7aa80430d9
commit 590bd6a0ae
7 changed files with 165 additions and 57 deletions

View File

@@ -1,11 +1,17 @@
FROM node:22-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY backend/package.json ./backend/package.json
COPY frontend/package.json ./frontend/package.json
RUN corepack enable && corepack prepare pnpm@10.33.2 --activate && pnpm install --frozen-lockfile --filter @pokopia/backend...
COPY backend ./backend
COPY data ./data
COPY system-wordings.ts ./system-wordings.ts
RUN mkdir -p /app/uploads && chown -R node:node /app
ENV NODE_ENV=production
WORKDIR /app/backend
COPY backend/package.json ./
RUN corepack enable && pnpm install
COPY backend/. .
COPY data /app/data
COPY package.json /app/package.json
COPY system-wordings.ts /app/system-wordings.ts
USER node
EXPOSE 3001
CMD ["pnpm", "run", "start"]

View File

@@ -13,17 +13,17 @@
"test": "node --test --import tsx tests/*.test.ts"
},
"dependencies": {
"@fastify/cors": "latest",
"@fastify/multipart": "^10.0.0",
"@fastify/rate-limit": "^10.3.0",
"@fastify/static": "^9.1.3",
"fastify": "latest",
"pg": "latest"
"@fastify/cors": "11.2.0",
"@fastify/multipart": "10.0.0",
"@fastify/rate-limit": "10.3.0",
"@fastify/static": "9.1.3",
"fastify": "5.8.5",
"pg": "8.20.0"
},
"devDependencies": {
"@types/node": "latest",
"@types/pg": "latest",
"tsx": "latest",
"typescript": "latest"
"@types/node": "25.6.0",
"@types/pg": "8.20.0",
"tsx": "4.21.0",
"typescript": "6.0.3"
}
}

View File

@@ -41,9 +41,11 @@ services:
build:
context: .
dockerfile: frontend/Dockerfile
args:
VITE_API_BASE_URL: ${VITE_API_BASE_URL:-http://localhost:3001}
VITE_SITE_URL: ${VITE_SITE_URL:-https://pokopiawiki.tootaio.com}
environment:
VITE_API_BASE_URL: http://localhost:3001
VITE_SITE_URL: https://pokopiawiki.tootaio.com
PORT: 20015
ports:
- "20015:20015"
depends_on:

View File

@@ -1,10 +1,28 @@
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY backend/package.json ./backend/package.json
COPY frontend/package.json ./frontend/package.json
RUN corepack enable && corepack prepare pnpm@10.33.2 --activate && pnpm install --frozen-lockfile --filter @pokopia/frontend...
COPY frontend ./frontend
COPY system-wordings.ts ./system-wordings.ts
ARG VITE_API_BASE_URL=http://localhost:3001
ARG VITE_SITE_URL=https://pokopiawiki.tootaio.com
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
ENV VITE_SITE_URL=$VITE_SITE_URL
RUN pnpm --filter @pokopia/frontend build
FROM node:22-alpine
WORKDIR /app/frontend
COPY frontend/package.json ./
RUN corepack enable && pnpm install
COPY frontend/. .
COPY package.json /app/package.json
COPY system-wordings.ts /app/system-wordings.ts
ENV NODE_ENV=production
ENV PORT=20015
WORKDIR /app
COPY --from=build /app/frontend/dist ./dist
COPY frontend/static-server.mjs ./static-server.mjs
USER node
EXPOSE 20015
CMD ["pnpm", "run", "dev"]
CMD ["node", "static-server.mjs"]

View File

@@ -12,18 +12,18 @@
"test": "vitest run"
},
"dependencies": {
"@iconify/vue": "^5.0.0",
"@vitejs/plugin-vue": "latest",
"vite": "latest",
"vue": "latest",
"vue-i18n": "^11.4.0",
"vue-router": "latest"
"@iconify/vue": "5.0.0",
"@vitejs/plugin-vue": "6.0.6",
"vite": "8.0.10",
"vue": "3.5.33",
"vue-i18n": "11.4.0",
"vue-router": "5.0.6"
},
"devDependencies": {
"@types/node": "latest",
"@vue/tsconfig": "latest",
"typescript": "latest",
"vitest": "latest",
"vue-tsc": "latest"
"@types/node": "25.6.0",
"@vue/tsconfig": "0.9.1",
"typescript": "6.0.3",
"vitest": "4.1.5",
"vue-tsc": "3.2.7"
}
}

View File

@@ -0,0 +1,82 @@
import { createReadStream } from 'node:fs';
import { stat } from 'node:fs/promises';
import { createServer } from 'node:http';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'dist');
const indexPath = path.join(root, 'index.html');
const host = process.env.HOST ?? '0.0.0.0';
const port = Number.parseInt(process.env.PORT ?? '20015', 10);
const contentTypes = new Map([
['.css', 'text/css; charset=utf-8'],
['.gif', 'image/gif'],
['.html', 'text/html; charset=utf-8'],
['.ico', 'image/x-icon'],
['.jpg', 'image/jpeg'],
['.js', 'text/javascript; charset=utf-8'],
['.json', 'application/json; charset=utf-8'],
['.png', 'image/png'],
['.svg', 'image/svg+xml'],
['.txt', 'text/plain; charset=utf-8'],
['.wasm', 'application/wasm'],
['.webmanifest', 'application/manifest+json; charset=utf-8'],
['.webp', 'image/webp'],
['.woff', 'font/woff'],
['.woff2', 'font/woff2'],
['.xml', 'application/xml; charset=utf-8']
]);
function isInsideRoot(filePath) {
const relativePath = path.relative(root, filePath);
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
}
function resolvePath(url) {
try {
const pathname = new URL(url ?? '/', 'http://localhost').pathname;
const filePath = path.resolve(root, `.${decodeURIComponent(pathname)}`);
return isInsideRoot(filePath) ? filePath : indexPath;
} catch {
return indexPath;
}
}
async function findStaticFile(url) {
const filePath = resolvePath(url);
try {
const fileStat = await stat(filePath);
if (fileStat.isFile()) {
return { filePath, fileStat };
}
} catch {
return { filePath: indexPath, fileStat: await stat(indexPath) };
}
return { filePath: indexPath, fileStat: await stat(indexPath) };
}
createServer(async (request, response) => {
if (request.method !== 'GET' && request.method !== 'HEAD') {
response.writeHead(405);
response.end();
return;
}
const { filePath, fileStat } = await findStaticFile(request.url);
const contentType = contentTypes.get(path.extname(filePath)) ?? 'application/octet-stream';
response.writeHead(200, {
'Cache-Control': filePath.endsWith('index.html') ? 'no-cache' : 'public, max-age=31536000, immutable',
'Content-Length': fileStat.size,
'Content-Type': contentType
});
if (request.method === 'HEAD') {
response.end();
return;
}
createReadStream(filePath).pipe(response);
}).listen(port, host);

42
pnpm-lock.yaml generated
View File

@@ -11,72 +11,72 @@ importers:
backend:
dependencies:
'@fastify/cors':
specifier: latest
specifier: 11.2.0
version: 11.2.0
'@fastify/multipart':
specifier: ^10.0.0
specifier: 10.0.0
version: 10.0.0
'@fastify/rate-limit':
specifier: ^10.3.0
specifier: 10.3.0
version: 10.3.0
'@fastify/static':
specifier: ^9.1.3
specifier: 9.1.3
version: 9.1.3
fastify:
specifier: latest
specifier: 5.8.5
version: 5.8.5
pg:
specifier: latest
specifier: 8.20.0
version: 8.20.0
devDependencies:
'@types/node':
specifier: latest
specifier: 25.6.0
version: 25.6.0
'@types/pg':
specifier: latest
specifier: 8.20.0
version: 8.20.0
tsx:
specifier: latest
specifier: 4.21.0
version: 4.21.0
typescript:
specifier: latest
specifier: 6.0.3
version: 6.0.3
frontend:
dependencies:
'@iconify/vue':
specifier: ^5.0.0
specifier: 5.0.0
version: 5.0.0(vue@3.5.33(typescript@6.0.3))
'@vitejs/plugin-vue':
specifier: latest
specifier: 6.0.6
version: 6.0.6(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.33(typescript@6.0.3))
vite:
specifier: latest
specifier: 8.0.10
version: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(tsx@4.21.0)(yaml@2.8.3)
vue:
specifier: latest
specifier: 3.5.33
version: 3.5.33(typescript@6.0.3)
vue-i18n:
specifier: ^11.4.0
specifier: 11.4.0
version: 11.4.0(vue@3.5.33(typescript@6.0.3))
vue-router:
specifier: latest
specifier: 5.0.6
version: 5.0.6(@vue/compiler-sfc@3.5.33)(vue@3.5.33(typescript@6.0.3))
devDependencies:
'@types/node':
specifier: latest
specifier: 25.6.0
version: 25.6.0
'@vue/tsconfig':
specifier: latest
specifier: 0.9.1
version: 0.9.1(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3))
typescript:
specifier: latest
specifier: 6.0.3
version: 6.0.3
vitest:
specifier: latest
specifier: 4.1.5
version: 4.1.5(@types/node@25.6.0)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(tsx@4.21.0)(yaml@2.8.3))
vue-tsc:
specifier: latest
specifier: 3.2.7
version: 3.2.7(typescript@6.0.3)
packages: