diff --git a/Dockerfile b/Dockerfile index ad6a673..d25d347 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ RUN pnpm install --frozen-lockfile # Copy frontend source COPY web/ ./ -# Build frontend +# Build frontend (SSR with adapter-node) RUN pnpm build # Stage 2: Build backend @@ -29,14 +29,12 @@ RUN dart pub get # Copy app source code and compile COPY bin/ ./bin/ -COPY --from=frontend-build /app/web/build ./web/build/ - RUN dart compile exe bin/server.dart -o bin/server -# Stage 3: Runtime image with libvips -FROM debian:bookworm-slim AS runtime +# Stage 3: Runtime image with Node.js, libvips and Dart runtime +FROM node:22-bookworm-slim AS runtime -# Install libvips and ca-certificates +# Install libvips, ca-certificates and other dependencies RUN apt-get update && apt-get install -y --no-install-recommends \ libvips42 \ ca-certificates \ @@ -45,21 +43,32 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ WORKDIR /app -# Copy the AOT runtime and compiled server from build stage +# Copy Dart AOT runtime from build stage COPY --from=backend-build /runtime/ / -COPY --from=backend-build /app/bin/server /app/bin/ -COPY --from=backend-build /app/web/build /app/web/build + +# Copy the compiled Dart server +COPY --from=backend-build /app/bin/server /app/bin/server + +# Copy the built frontend (Node.js SSR app) +COPY --from=frontend-build /app/web/build /app/web/build # Create data and cache directories RUN mkdir -p /app/data /app/cache +# Copy startup script +COPY docker/entrypoint.sh /app/entrypoint.sh +RUN chmod +x /app/entrypoint.sh + # Set environment variables (can be overridden at runtime) ENV PORT=8080 \ + HOST=0.0.0.0 \ DATA_DIR=/app/data \ - CACHE_DIR=/app/cache + CACHE_DIR=/app/cache \ + BACKEND_URL=http://127.0.0.1:8081 \ + NODE_ENV=production -# Expose port +# Expose port (frontend serves on this port) EXPOSE 8080 -# Start server -CMD ["/app/bin/server"] \ No newline at end of file +# Start both services using entrypoint script +ENTRYPOINT ["/app/entrypoint.sh"] \ No newline at end of file diff --git a/README.md b/README.md index 86a94a0..dda34d5 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,39 @@ --- +## ๐Ÿ›๏ธ ๆžถๆž„่ฏดๆ˜Ž + +Docker ้ƒจ็ฝฒ้‡‡็”จ**ๅŒๆœๅŠกๆžถๆž„**๏ผš + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Docker Container โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Node.js (SvelteKit SSR) โ”‚ โ”‚ +โ”‚ โ”‚ ็ซฏๅฃ: 8080 (ๅฏนๅค–ๆšด้œฒ) โ”‚ โ”‚ +โ”‚ โ”‚ - ๅค„็†้กต้ข่ฏทๆฑ‚ โ”‚ โ”‚ +โ”‚ โ”‚ - ไปฃ็† /api/* โ†’ Dart ๅŽ็ซฏ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ ๅ†…้ƒจ้€šไฟก โ”‚ +โ”‚ โ–ผ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Dart Server (Shelf) โ”‚ โ”‚ +โ”‚ โ”‚ ็ซฏๅฃ: 8081 (ไป…ๅ†…้ƒจ) โ”‚ โ”‚ +โ”‚ โ”‚ - ๆไพ› RESTful API โ”‚ โ”‚ +โ”‚ โ”‚ - ๅ›พ็‰‡/็ผฉ็•ฅๅ›พๆœๅŠก โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**ๅทฅไฝœๆต็จ‹๏ผš** +1. ็”จๆˆท่ฎฟ้—ฎ `:8080`๏ผŒ่ฏทๆฑ‚็”ฑ SvelteKit SSR ๅค„็† +2. ๅ‰็ซฏ `/api/*` ่ฏทๆฑ‚่ขซไปฃ็†ๅˆฐ Dart ๅŽ็ซฏ `:8081` +3. Dart ๅŽ็ซฏๅค„็†ๆ•ฐๆฎ่ฏทๆฑ‚๏ผŒ่ฟ”ๅ›ž JSON ๆˆ–ๅ›พ็‰‡ๆต + +--- + ## ๐Ÿ—๏ธ ้กน็›ฎ็ป“ๆž„ ``` @@ -109,9 +142,13 @@ docker run -d \ -p 8080:8080 \ -v $(pwd)/data:/app/data \ -v $(pwd)/cache:/app/cache \ + -e ORIGIN=http://localhost:8080 \ + -e BETTER_AUTH_SECRET=your-secret-key-here \ loongyan ``` +> **ๆณจๆ„**: ็”Ÿไบง็Žฏๅขƒ่ฏทๅŠกๅฟ…่ฎพ็ฝฎ `ORIGIN` ไธบไฝ ็š„ๅฎž้™…ๅŸŸๅ๏ผŒๅนถไฝฟ็”จๅฎ‰ๅ…จ็š„้šๆœบๅญ—็ฌฆไธฒไฝœไธบ `BETTER_AUTH_SECRET`ใ€‚ + ### Docker Compose๏ผˆๆŽจ่๏ผ‰ ```yaml @@ -126,6 +163,8 @@ services: - ./cache:/app/cache environment: - PORT=8080 + - ORIGIN=http://localhost:8080 + - BETTER_AUTH_SECRET=your-secret-key-here restart: unless-stopped ``` @@ -262,7 +301,9 @@ pnpm auth:schema | ๅ˜้‡ๅ | ้ป˜่ฎคๅ€ผ | ่ฏดๆ˜Ž | |--------|--------|------| -| `PORT` | `8080` | ๆœๅŠกๅ™จ็›‘ๅฌ็ซฏๅฃ | +| `PORT` | `8081` | Dart ๅŽ็ซฏ็›‘ๅฌ็ซฏๅฃ๏ผˆๅฎนๅ™จๅ†…้ƒจ๏ผ‰ | +| `DATA_DIR` | `/app/data` | ๆ•ฐๆฎ็›ฎๅฝ• | +| `CACHE_DIR` | `/app/cache` | ็ผ“ๅญ˜็›ฎๅฝ• | ### ๅ‰็ซฏ็Žฏๅขƒๅ˜้‡ @@ -270,9 +311,13 @@ pnpm auth:schema ```bash # web/.env -PUBLIC_API_URL=http://localhost:8080/api/v1 +ORIGIN=http://localhost:8080 # ็”Ÿไบง็Žฏๅขƒ็š„ๅฎž้™…ๅŸŸๅ +BETTER_AUTH_SECRET=your-secret-key # ่ฎค่ฏๅฏ†้’ฅ๏ผˆ32ๅญ—็ฌฆ้šๆœบๅญ—็ฌฆไธฒ๏ผ‰ +BACKEND_URL=http://127.0.0.1:8081 # Dart ๅŽ็ซฏๅœฐๅ€๏ผˆๅ†…้ƒจ้€šไฟก๏ผ‰ ``` +> **้‡่ฆ**: ็”Ÿไบง็Žฏๅขƒๅฟ…้กป่ฎพ็ฝฎ `ORIGIN` ๅ’Œ `BETTER_AUTH_SECRET`ใ€‚ + --- ## ๐Ÿ“ฆ ๆŠ€ๆœฏๆ ˆ diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..9edc7dc --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,30 @@ +#!/bin/sh +set -e + +# ๅฏๅŠจ Dart ๅŽ็ซฏ (ๅ†…้ƒจ็ซฏๅฃ 8081) +echo "Starting Dart backend on port 8081..." +export PORT=8081 +/app/bin/server & +BACKEND_PID=$! + +# ็ญ‰ๅพ…ๅŽ็ซฏๅฏๅŠจ +sleep 2 + +# ๅฏๅŠจๅ‰็ซฏ (Node.js SSR, ็ซฏๅฃ 8080) +echo "Starting frontend on port 8080..." +export PORT=8080 +export HOST=0.0.0.0 +export BACKEND_URL=http://127.0.0.1:8081 +cd /app/web/build +node index.js & +FRONTEND_PID=$! + +# ๆ•่Žท้€€ๅ‡บไฟกๅท๏ผŒไผ˜้›…ๅ…ณ้—ญไธคไธชๆœๅŠก +trap "echo 'Shutting down...'; kill $BACKEND_PID $FRONTEND_PID 2>/dev/null; exit 0" SIGTERM SIGINT + +# ็ญ‰ๅพ…ไปปๆ„่ฟ›็จ‹้€€ๅ‡บ +wait -n $BACKEND_PID $FRONTEND_PID + +# ๅฆ‚ๆžœๆœ‰ไธ€ไธช่ฟ›็จ‹้€€ๅ‡บ๏ผŒๆ€ๆญปๅฆไธ€ไธช +kill $BACKEND_PID $FRONTEND_PID 2>/dev/null || true +exit 1 \ No newline at end of file diff --git a/web/.env.example b/web/.env.example index 6205e49..24af2ff 100644 --- a/web/.env.example +++ b/web/.env.example @@ -7,3 +7,7 @@ ORIGIN="" # For production use 32 characters and generated with high entropy # https://www.better-auth.com/docs/installation BETTER_AUTH_SECRET="" + +# Backend API URL (Dart server) +# In production, the frontend proxies /api requests to this URL +BACKEND_URL="http://127.0.0.1:8080" diff --git a/web/package.json b/web/package.json index 56a917f..8218cd4 100644 --- a/web/package.json +++ b/web/package.json @@ -21,7 +21,7 @@ "@eslint/compat": "^2.0.2", "@eslint/js": "^9.39.2", "@inlang/paraglide-js": "^2.10.0", - "@sveltejs/adapter-auto": "^7.0.0", + "@sveltejs/adapter-node": "^5.2.12", "@sveltejs/kit": "^2.50.2", "@sveltejs/vite-plugin-svelte": "^6.2.4", "@types/better-sqlite3": "^7.6.13", diff --git a/web/src/hooks.server.js b/web/src/hooks.server.js index 4454ac6..e76c601 100644 --- a/web/src/hooks.server.js +++ b/web/src/hooks.server.js @@ -4,6 +4,7 @@ import { auth } from '$lib/server/auth'; import { svelteKitHandler } from 'better-auth/svelte-kit'; import { getTextDirection } from '$lib/paraglide/runtime'; import { paraglideMiddleware } from '$lib/paraglide/server'; +import { env } from '$env/dynamic/private'; /** @type {import('@sveltejs/kit').Handle} */ const handleParaglide = ({ event, resolve }) => @@ -32,6 +33,35 @@ const handleBetterAuth = async ({ event, resolve }) => { return svelteKitHandler({ event, resolve, auth, building }); }; +/** + * API ไปฃ็†ๅค„็† - ๅฐ† /api/v1 ่ฏทๆฑ‚ไปฃ็†ๅˆฐ Dart ๅŽ็ซฏ + * ไป…ๅœจ็”Ÿไบง็Žฏๅขƒ็”Ÿๆ•ˆ๏ผŒๅผ€ๅ‘็Žฏๅขƒ็”ฑ Vite proxy ๅค„็† + * @type {import('@sveltejs/kit').Handle} */ +const handleApiProxy = async ({ event, resolve }) => { + const { url, request } = event; + + // ๅชๅค„็† /api/v1 ่ทฏๅพ„็š„่ฏทๆฑ‚ + if (url.pathname.startsWith('/api/v1')) { + const backendUrl = env.BACKEND_URL ?? 'http://127.0.0.1:8080'; + const targetUrl = `${backendUrl}${url.pathname}${url.search}`; + + // ่ฝฌๅ‘่ฏทๆฑ‚ๅˆฐ Dart ๅŽ็ซฏ + const backendResponse = await fetch(targetUrl, { + method: request.method, + headers: request.headers, + body: request.method !== 'GET' && request.method !== 'HEAD' ? await request.arrayBuffer() : undefined + }); + + // ่ฟ”ๅ›žๅŽ็ซฏๅ“ๅบ” + return new Response(backendResponse.body, { + status: backendResponse.status, + headers: backendResponse.headers + }); + } + + return resolve(event); +}; + /** @type {import('@sveltejs/kit').Handle} */ const handleCache = async ({ event, resolve }) => { const response = await resolve(event); @@ -66,4 +96,4 @@ const handleCache = async ({ event, resolve }) => { return newResponse; }; -export const handle = sequence(handleParaglide, handleBetterAuth, handleCache); +export const handle = sequence(handleApiProxy, handleParaglide, handleBetterAuth, handleCache); \ No newline at end of file diff --git a/web/svelte.config.js b/web/svelte.config.js index 7cc091d..dd93af7 100644 --- a/web/svelte.config.js +++ b/web/svelte.config.js @@ -1,13 +1,13 @@ import { mdsvex } from 'mdsvex'; -import adapter from '@sveltejs/adapter-auto'; +import adapter from '@sveltejs/adapter-node'; /** @type {import('@sveltejs/kit').Config} */ const config = { kit: { - // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. - // If your environment is not supported, or you settled on a specific environment, switch out the adapter. - // See https://svelte.dev/docs/kit/adapters for more information about adapters. - adapter: adapter() + adapter: adapter({ + // ๅ‰็ซฏๆœๅŠก็ซฏๅฃ๏ผŒ้€š่ฟ‡็Žฏๅขƒๅ˜้‡้…็ฝฎ + env: { port: process.env.PORT ?? '3000', host: process.env.HOST ?? '0.0.0.0' } + }) }, vitePlugin: { dynamicCompileOptions: ({ filename }) => ({ runes: !filename.includes('node_modules') }) @@ -16,4 +16,4 @@ const config = { extensions: ['.svelte', '.svx'] }; -export default config; +export default config; \ No newline at end of file