diff --git a/backend/Dockerfile b/backend/Dockerfile index 3db3245..901f3ac 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -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"] diff --git a/backend/package.json b/backend/package.json index 0dfd1fe..89c26f0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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" } } diff --git a/docker-compose.yml b/docker-compose.yml index f0ba8d8..e1ea38a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/frontend/Dockerfile b/frontend/Dockerfile index e2933df..db5b56b 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -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"] diff --git a/frontend/package.json b/frontend/package.json index 60ad279..5e518e3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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" } } diff --git a/frontend/static-server.mjs b/frontend/static-server.mjs new file mode 100644 index 0000000..73268a6 --- /dev/null +++ b/frontend/static-server.mjs @@ -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); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23e50e9..c81d1d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: