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:
@@ -1,11 +1,17 @@
|
|||||||
FROM node:22-alpine
|
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
|
WORKDIR /app/backend
|
||||||
COPY backend/package.json ./
|
USER node
|
||||||
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
|
|
||||||
EXPOSE 3001
|
EXPOSE 3001
|
||||||
CMD ["pnpm", "run", "start"]
|
CMD ["pnpm", "run", "start"]
|
||||||
|
|||||||
@@ -13,17 +13,17 @@
|
|||||||
"test": "node --test --import tsx tests/*.test.ts"
|
"test": "node --test --import tsx tests/*.test.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "latest",
|
"@fastify/cors": "11.2.0",
|
||||||
"@fastify/multipart": "^10.0.0",
|
"@fastify/multipart": "10.0.0",
|
||||||
"@fastify/rate-limit": "^10.3.0",
|
"@fastify/rate-limit": "10.3.0",
|
||||||
"@fastify/static": "^9.1.3",
|
"@fastify/static": "9.1.3",
|
||||||
"fastify": "latest",
|
"fastify": "5.8.5",
|
||||||
"pg": "latest"
|
"pg": "8.20.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "latest",
|
"@types/node": "25.6.0",
|
||||||
"@types/pg": "latest",
|
"@types/pg": "8.20.0",
|
||||||
"tsx": "latest",
|
"tsx": "4.21.0",
|
||||||
"typescript": "latest"
|
"typescript": "6.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,9 +41,11 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: frontend/Dockerfile
|
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:
|
environment:
|
||||||
VITE_API_BASE_URL: http://localhost:3001
|
PORT: 20015
|
||||||
VITE_SITE_URL: https://pokopiawiki.tootaio.com
|
|
||||||
ports:
|
ports:
|
||||||
- "20015:20015"
|
- "20015:20015"
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -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
|
FROM node:22-alpine
|
||||||
|
|
||||||
WORKDIR /app/frontend
|
ENV NODE_ENV=production
|
||||||
COPY frontend/package.json ./
|
ENV PORT=20015
|
||||||
RUN corepack enable && pnpm install
|
|
||||||
COPY frontend/. .
|
WORKDIR /app
|
||||||
COPY package.json /app/package.json
|
COPY --from=build /app/frontend/dist ./dist
|
||||||
COPY system-wordings.ts /app/system-wordings.ts
|
COPY frontend/static-server.mjs ./static-server.mjs
|
||||||
|
|
||||||
|
USER node
|
||||||
EXPOSE 20015
|
EXPOSE 20015
|
||||||
CMD ["pnpm", "run", "dev"]
|
CMD ["node", "static-server.mjs"]
|
||||||
|
|||||||
@@ -12,18 +12,18 @@
|
|||||||
"test": "vitest run"
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconify/vue": "^5.0.0",
|
"@iconify/vue": "5.0.0",
|
||||||
"@vitejs/plugin-vue": "latest",
|
"@vitejs/plugin-vue": "6.0.6",
|
||||||
"vite": "latest",
|
"vite": "8.0.10",
|
||||||
"vue": "latest",
|
"vue": "3.5.33",
|
||||||
"vue-i18n": "^11.4.0",
|
"vue-i18n": "11.4.0",
|
||||||
"vue-router": "latest"
|
"vue-router": "5.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "latest",
|
"@types/node": "25.6.0",
|
||||||
"@vue/tsconfig": "latest",
|
"@vue/tsconfig": "0.9.1",
|
||||||
"typescript": "latest",
|
"typescript": "6.0.3",
|
||||||
"vitest": "latest",
|
"vitest": "4.1.5",
|
||||||
"vue-tsc": "latest"
|
"vue-tsc": "3.2.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
82
frontend/static-server.mjs
Normal file
82
frontend/static-server.mjs
Normal 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
42
pnpm-lock.yaml
generated
@@ -11,72 +11,72 @@ importers:
|
|||||||
backend:
|
backend:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@fastify/cors':
|
'@fastify/cors':
|
||||||
specifier: latest
|
specifier: 11.2.0
|
||||||
version: 11.2.0
|
version: 11.2.0
|
||||||
'@fastify/multipart':
|
'@fastify/multipart':
|
||||||
specifier: ^10.0.0
|
specifier: 10.0.0
|
||||||
version: 10.0.0
|
version: 10.0.0
|
||||||
'@fastify/rate-limit':
|
'@fastify/rate-limit':
|
||||||
specifier: ^10.3.0
|
specifier: 10.3.0
|
||||||
version: 10.3.0
|
version: 10.3.0
|
||||||
'@fastify/static':
|
'@fastify/static':
|
||||||
specifier: ^9.1.3
|
specifier: 9.1.3
|
||||||
version: 9.1.3
|
version: 9.1.3
|
||||||
fastify:
|
fastify:
|
||||||
specifier: latest
|
specifier: 5.8.5
|
||||||
version: 5.8.5
|
version: 5.8.5
|
||||||
pg:
|
pg:
|
||||||
specifier: latest
|
specifier: 8.20.0
|
||||||
version: 8.20.0
|
version: 8.20.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: latest
|
specifier: 25.6.0
|
||||||
version: 25.6.0
|
version: 25.6.0
|
||||||
'@types/pg':
|
'@types/pg':
|
||||||
specifier: latest
|
specifier: 8.20.0
|
||||||
version: 8.20.0
|
version: 8.20.0
|
||||||
tsx:
|
tsx:
|
||||||
specifier: latest
|
specifier: 4.21.0
|
||||||
version: 4.21.0
|
version: 4.21.0
|
||||||
typescript:
|
typescript:
|
||||||
specifier: latest
|
specifier: 6.0.3
|
||||||
version: 6.0.3
|
version: 6.0.3
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@iconify/vue':
|
'@iconify/vue':
|
||||||
specifier: ^5.0.0
|
specifier: 5.0.0
|
||||||
version: 5.0.0(vue@3.5.33(typescript@6.0.3))
|
version: 5.0.0(vue@3.5.33(typescript@6.0.3))
|
||||||
'@vitejs/plugin-vue':
|
'@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))
|
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:
|
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)
|
version: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(tsx@4.21.0)(yaml@2.8.3)
|
||||||
vue:
|
vue:
|
||||||
specifier: latest
|
specifier: 3.5.33
|
||||||
version: 3.5.33(typescript@6.0.3)
|
version: 3.5.33(typescript@6.0.3)
|
||||||
vue-i18n:
|
vue-i18n:
|
||||||
specifier: ^11.4.0
|
specifier: 11.4.0
|
||||||
version: 11.4.0(vue@3.5.33(typescript@6.0.3))
|
version: 11.4.0(vue@3.5.33(typescript@6.0.3))
|
||||||
vue-router:
|
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))
|
version: 5.0.6(@vue/compiler-sfc@3.5.33)(vue@3.5.33(typescript@6.0.3))
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: latest
|
specifier: 25.6.0
|
||||||
version: 25.6.0
|
version: 25.6.0
|
||||||
'@vue/tsconfig':
|
'@vue/tsconfig':
|
||||||
specifier: latest
|
specifier: 0.9.1
|
||||||
version: 0.9.1(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3))
|
version: 0.9.1(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3))
|
||||||
typescript:
|
typescript:
|
||||||
specifier: latest
|
specifier: 6.0.3
|
||||||
version: 6.0.3
|
version: 6.0.3
|
||||||
vitest:
|
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))
|
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:
|
vue-tsc:
|
||||||
specifier: latest
|
specifier: 3.2.7
|
||||||
version: 3.2.7(typescript@6.0.3)
|
version: 3.2.7(typescript@6.0.3)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|||||||
Reference in New Issue
Block a user