From 431a247963a1fb70c120c228ec7f8d360849004b Mon Sep 17 00:00:00 2001 From: MH Hung Date: Tue, 30 Sep 2025 17:01:21 +0800 Subject: [PATCH] feat(ci): add docker, docker compose --- .dockerignore | 7 ++++ README.md | 10 +++++ client/Dockerfile | 16 ++++++++ client/nginx.conf | 19 ++++++++++ .../src/features/visits/hooks/useResizeMap.ts | 9 +++-- docker-compose.yml | 38 +++++++++++++++++++ server/Dockerfile | 12 ++++++ server/src/modules/visits/visit.mapper.ts | 2 +- 8 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 .dockerignore create mode 100644 client/Dockerfile create mode 100644 client/nginx.conf create mode 100644 docker-compose.yml create mode 100644 server/Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..30815ec --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +.gitignore +**/node_modules +**/dist +Dockerfile +docker-compose.yml +npm-debug.log diff --git a/README.md b/README.md index aa8ff32..104760b 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,16 @@ traveling-around-the-world/ > **注意**:Leaflet 需要載入 CSS(已在 `TravelMap` 中引入 `leaflet/dist/leaflet.css`)。 +### Docker Compose 啟動 +若想直接以容器方式啟動整個堆疊(MongoDB + Node API + Nginx 前端): + +1. 建立映像:`docker compose build` +2. 啟動服務:`docker compose up -d` +3. 前端: +4. API: + +> 預設前端仍採 LocalStorage。如果要改用 API 資料來源,請在 `client/src/app/App.tsx` 把 `VisitProvider mode="local"` 改成 `mode="api"`。 + ## 後端(server) ### 技術堆疊 diff --git a/client/Dockerfile b/client/Dockerfile new file mode 100644 index 0000000..b3e38b3 --- /dev/null +++ b/client/Dockerfile @@ -0,0 +1,16 @@ +# syntax=docker/dockerfile:1 + +FROM node:18-alpine AS build +WORKDIR /app +COPY client/package*.json ./client/ +WORKDIR /app/client +RUN npm install +COPY client/ . +COPY shared /app/shared +RUN npm run build + +FROM nginx:1.25-alpine +COPY client/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/client/dist /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/client/nginx.conf b/client/nginx.conf new file mode 100644 index 0000000..5660079 --- /dev/null +++ b/client/nginx.conf @@ -0,0 +1,19 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /api/ { + proxy_pass http://server:4000/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/client/src/features/visits/hooks/useResizeMap.ts b/client/src/features/visits/hooks/useResizeMap.ts index ac326f7..04f9601 100644 --- a/client/src/features/visits/hooks/useResizeMap.ts +++ b/client/src/features/visits/hooks/useResizeMap.ts @@ -1,13 +1,16 @@ -import { useEffect, type DependencyList } from 'react'; +import { useEffect, type DependencyList, type RefObject } from 'react'; import type { Map as LeafletMap } from 'leaflet'; import { useVisitModalStore } from '../../../state/useVisitModalStore'; -export function useResizeMap(mapRef: React.RefObject, deps: DependencyList = []) { +export function useResizeMap(mapRef: RefObject, deps: DependencyList = []) { const isModalOpen = useVisitModalStore((state) => state.isOpen); useEffect(() => { const map = mapRef.current; if (!map) return; - setTimeout(() => map.invalidateSize(), 0); + const timer = setTimeout(() => { + map.invalidateSize(); + }, 0); + return () => clearTimeout(timer); }, [mapRef, isModalOpen, ...deps]); } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..22bc2c0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,38 @@ +services: + mongo: + image: mongo:7 + restart: unless-stopped + container_name: travel-mongo + volumes: + - mongo_data:/data/db + environment: + MONGO_INITDB_DATABASE: travel-journal + ports: + - "27017:27017" + + server: + build: + context: . + dockerfile: server/Dockerfile + depends_on: + - mongo + environment: + NODE_ENV: production + PORT: 4000 + MONGODB_URI: mongodb://mongo:27017/travel-journal + ports: + - "4000:4000" + restart: unless-stopped + + client: + build: + context: . + dockerfile: client/Dockerfile + depends_on: + - server + ports: + - "3000:80" + restart: unless-stopped + +volumes: + mongo_data: diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..8e9e340 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,12 @@ +# syntax=docker/dockerfile:1 + +FROM node:18-alpine +WORKDIR /app/server +COPY server/package*.json ./ +RUN npm install +COPY server/ . +COPY shared /app/shared +ENV NODE_ENV=production +ENV PORT=4000 +EXPOSE 4000 +CMD ["node", "--loader", "ts-node/esm", "src/index.ts"] diff --git a/server/src/modules/visits/visit.mapper.ts b/server/src/modules/visits/visit.mapper.ts index 20988cf..cfd390a 100644 --- a/server/src/modules/visits/visit.mapper.ts +++ b/server/src/modules/visits/visit.mapper.ts @@ -1,5 +1,5 @@ import type { VisitDoc } from './visit.model'; -import type { VisitDto } from '../../../shared/visit'; +import type { VisitDto } from '../../../../shared/visit'; export function toVisitDto(doc: VisitDoc): VisitDto { const json = doc.toJSON();