feat(ci): add docker, docker compose

This commit is contained in:
2025-09-30 17:01:21 +08:00
parent 8094ab21b5
commit 431a247963
8 changed files with 109 additions and 4 deletions

7
.dockerignore Normal file
View File

@@ -0,0 +1,7 @@
.git
.gitignore
**/node_modules
**/dist
Dockerfile
docker-compose.yml
npm-debug.log

View File

@@ -49,6 +49,16 @@ traveling-around-the-world/
> **注意**Leaflet 需要載入 CSS已在 `TravelMap` 中引入 `leaflet/dist/leaflet.css`)。 > **注意**Leaflet 需要載入 CSS已在 `TravelMap` 中引入 `leaflet/dist/leaflet.css`)。
### Docker Compose 啟動
若想直接以容器方式啟動整個堆疊MongoDB + Node API + Nginx 前端):
1. 建立映像:`docker compose build`
2. 啟動服務:`docker compose up -d`
3. 前端:<http://localhost:3000>
4. API<http://localhost:4000>
> 預設前端仍採 LocalStorage。如果要改用 API 資料來源,請在 `client/src/app/App.tsx` 把 `VisitProvider mode="local"` 改成 `mode="api"`。
## 後端server ## 後端server
### 技術堆疊 ### 技術堆疊

16
client/Dockerfile Normal file
View File

@@ -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;"]

19
client/nginx.conf Normal file
View File

@@ -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;
}
}

View File

@@ -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 type { Map as LeafletMap } from 'leaflet';
import { useVisitModalStore } from '../../../state/useVisitModalStore'; import { useVisitModalStore } from '../../../state/useVisitModalStore';
export function useResizeMap(mapRef: React.RefObject<LeafletMap | null>, deps: DependencyList = []) { export function useResizeMap(mapRef: RefObject<LeafletMap | null>, deps: DependencyList = []) {
const isModalOpen = useVisitModalStore((state) => state.isOpen); const isModalOpen = useVisitModalStore((state) => state.isOpen);
useEffect(() => { useEffect(() => {
const map = mapRef.current; const map = mapRef.current;
if (!map) return; if (!map) return;
setTimeout(() => map.invalidateSize(), 0); const timer = setTimeout(() => {
map.invalidateSize();
}, 0);
return () => clearTimeout(timer);
}, [mapRef, isModalOpen, ...deps]); }, [mapRef, isModalOpen, ...deps]);
} }

38
docker-compose.yml Normal file
View File

@@ -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:

12
server/Dockerfile Normal file
View File

@@ -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"]

View File

@@ -1,5 +1,5 @@
import type { VisitDoc } from './visit.model'; import type { VisitDoc } from './visit.model';
import type { VisitDto } from '../../../shared/visit'; import type { VisitDto } from '../../../../shared/visit';
export function toVisitDto(doc: VisitDoc): VisitDto { export function toVisitDto(doc: VisitDoc): VisitDto {
const json = doc.toJSON(); const json = doc.toJSON();