部署docker化

This commit is contained in:
andy
2026-08-03 11:25:55 +08:00
parent ed6229c203
commit 393b841023
9 changed files with 284 additions and 0 deletions

22
.dockerignore Normal file
View File

@@ -0,0 +1,22 @@
.git
.idea
.DS_Store
.env
.env.*
*.env
.planning
.work
.tmp
.validation
.codex-tmp
outputs
runtime
private-fixtures
node_modules
backend
*.log
*.sqlite3
*.sqlite3-*
*.xml
*.XML
*.xlsx

75
DOCKER_DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,75 @@
# Docker Deployment
This deployment runs the Condo frontend and backend in Docker while connecting
to an existing PostgreSQL database. PostgreSQL is not included in
`docker-compose.yml`.
## Runtime Shape
- `frontend`: nginx serves the static files and proxies `/api/*` to the backend.
- `backend`: Node 24 runs the Fastify API on `0.0.0.0:3000`.
- Existing PostgreSQL: must expose the `booking_test` database to the backend
container.
The browser only needs the frontend URL. API requests go to the same origin
under `/api`, so the backend port does not need to be published.
## Environment
Use the variables shown in `deploy/.env.production.example`. The important
settings are:
- `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_NAME=booking_test`
- `AUTH_USERNAME`, `AUTH_PASSWORD`
- `AUTH_COOKIE_SECURE=false` for HTTP test machines
- `AUTH_COOKIE_SECURE=true` when the public frontend URL uses HTTPS
- `FRONTEND_PORT`, default `8080`
`DB_HOST` must be resolvable from inside the backend container. If PostgreSQL
runs directly on the Docker host, use the host address that works for that
server environment.
## Build And Run
```bash
docker compose build
docker compose up -d
```
Open the frontend at:
```text
http://<test-machine-host>:8080
```
## Database Migration And Checks
The database scripts intentionally read one JSON connection line from standard
input. Example shape:
```json
{"host":"DB_HOST","port":5432,"user":"DB_USER","password":"DB_PASSWORD","database":"booking_test","ssl":false}
```
Run migration safety checks without database access:
```bash
docker compose run --rm backend npm run db:check-migrations
```
Run inventory and migrations against the existing PostgreSQL connection:
```bash
printf '%s\n' "$CONDON_DB_CONNECTION_JSON" | docker compose run --rm -T backend npm run db:inventory
printf '%s\n' "$CONDON_DB_CONNECTION_JSON" | docker compose run --rm -T backend npm run db:migrate:up
```
For an already imported database that matches the retained production baseline,
run:
```bash
printf '%s\n' "$CONDON_DB_CONNECTION_JSON" | docker compose run --rm -T backend npm run db:verify
```
`/api/auth/session` is public and works as a basic process check. `/api/health`
requires a valid login cookie.

10
Dockerfile.frontend Normal file
View File

@@ -0,0 +1,10 @@
FROM nginx:1.27-alpine
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
COPY deploy/frontend-entrypoint.sh /docker-entrypoint.d/50-condo-runtime-config.sh
COPY index.html styles.css app.js api-client.js owners-data.js favicon.svg runtime-config.js /usr/share/nginx/html/
RUN chmod +x /docker-entrypoint.d/50-condo-runtime-config.sh
EXPOSE 80

10
backend/.dockerignore Normal file
View File

@@ -0,0 +1,10 @@
.env
.env.*
*.env
*.log
node_modules
dist
coverage
.DS_Store
.idea
__pycache__

32
backend/Dockerfile Normal file
View File

@@ -0,0 +1,32 @@
# syntax=docker/dockerfile:1.7
FROM node:24-alpine AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM dependencies AS build
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
FROM node:24-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production \
API_HOST=0.0.0.0 \
API_PORT=3000
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=build /app/dist ./dist
COPY migrations ./migrations
COPY scripts ./scripts
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:' + (process.env.API_PORT || 3000) + '/auth/session').then(response => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))"
CMD ["node", "dist/src/server.js"]

View File

@@ -0,0 +1,26 @@
# Frontend
FRONTEND_PORT=8080
CONDO_API_BASE_URL=/api
CONDO_DEFAULT_MODE=api
CONDO_PERIOD_YEAR=
# Backend
NODE_ENV=production
LOG_LEVEL=info
CORS_ORIGINS=http://localhost:8080
AUTH_USERNAME=wyndhamcondon
AUTH_PASSWORD=replace-with-test-password
AUTH_SESSION_TTL_HOURS=12
# Set true only when the public frontend is served over HTTPS.
AUTH_COOKIE_SECURE=false
# Existing PostgreSQL. DB_HOST must be reachable from inside the backend container.
DB_HOST=replace-with-existing-pg-host
DB_PORT=5432
DB_USER=replace-with-existing-pg-user
DB_PASSWORD=replace-with-existing-pg-password
DB_NAME=booking_test
DB_SSL=false
DB_POOL_MAX=10
DB_STATEMENT_TIMEOUT_MS=15000

View File

@@ -0,0 +1,34 @@
#!/bin/sh
set -eu
runtime_config_path="/usr/share/nginx/html/runtime-config.js"
default_mode="${CONDO_DEFAULT_MODE:-api}"
api_base_url="${CONDO_API_BASE_URL:-/api}"
period_year="${CONDO_PERIOD_YEAR:-}"
escape_js_string() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}
case "$period_year" in
"" )
period_year_expression="new Date().getUTCFullYear()"
;;
*[!0-9]* )
period_year_expression="new Date().getUTCFullYear()"
;;
* )
period_year_expression="$period_year"
;;
esac
default_mode_escaped="$(escape_js_string "$default_mode")"
api_base_url_escaped="$(escape_js_string "$api_base_url")"
cat > "$runtime_config_path" <<EOF
window.CONDO_RUNTIME_CONFIG = Object.freeze({
defaultMode: "$default_mode_escaped",
apiBaseUrl: "$api_base_url_escaped",
periodYear: $period_year_expression
});
EOF

35
deploy/nginx.conf Normal file
View File

@@ -0,0 +1,35 @@
upstream condo_backend {
server backend:3000;
}
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location = /api {
return 308 /api/;
}
location /api/ {
proxy_http_version 1.1;
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;
proxy_set_header Connection "";
proxy_pass http://condo_backend/;
}
location / {
try_files $uri $uri/ /index.html;
}
location ~* \.(?:css|js|svg)$ {
expires 1h;
add_header Cache-Control "public";
try_files $uri =404;
}
}

40
docker-compose.yml Normal file
View File

@@ -0,0 +1,40 @@
services:
backend:
build:
context: ./backend
dockerfile: Dockerfile
restart: unless-stopped
environment:
NODE_ENV: ${NODE_ENV:-production}
API_HOST: 0.0.0.0
API_PORT: 3000
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:8080}
LOG_LEVEL: ${LOG_LEVEL:-info}
AUTH_USERNAME: ${AUTH_USERNAME:-wyndhamcondon}
AUTH_PASSWORD: ${AUTH_PASSWORD:?Set AUTH_PASSWORD for the test deployment}
AUTH_SESSION_TTL_HOURS: ${AUTH_SESSION_TTL_HOURS:-12}
AUTH_COOKIE_SECURE: ${AUTH_COOKIE_SECURE:-false}
DB_HOST: ${DB_HOST:?Set DB_HOST to the existing PostgreSQL host}
DB_PORT: ${DB_PORT:-5432}
DB_USER: ${DB_USER:?Set DB_USER for PostgreSQL}
DB_PASSWORD: ${DB_PASSWORD:?Set DB_PASSWORD for PostgreSQL}
DB_NAME: ${DB_NAME:-booking_test}
DB_SSL: ${DB_SSL:-false}
DB_POOL_MAX: ${DB_POOL_MAX:-10}
DB_STATEMENT_TIMEOUT_MS: ${DB_STATEMENT_TIMEOUT_MS:-15000}
expose:
- "3000"
frontend:
build:
context: .
dockerfile: Dockerfile.frontend
restart: unless-stopped
depends_on:
- backend
environment:
CONDO_API_BASE_URL: ${CONDO_API_BASE_URL:-/api}
CONDO_DEFAULT_MODE: ${CONDO_DEFAULT_MODE:-api}
CONDO_PERIOD_YEAR: ${CONDO_PERIOD_YEAR:-}
ports:
- "${FRONTEND_PORT:-8080}:80"