部署docker化
This commit is contained in:
22
.dockerignore
Normal file
22
.dockerignore
Normal 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
75
DOCKER_DEPLOYMENT.md
Normal 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
10
Dockerfile.frontend
Normal 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
10
backend/.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
*.env
|
||||||
|
*.log
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
coverage
|
||||||
|
.DS_Store
|
||||||
|
.idea
|
||||||
|
__pycache__
|
||||||
32
backend/Dockerfile
Normal file
32
backend/Dockerfile
Normal 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"]
|
||||||
26
deploy/.env.production.example
Normal file
26
deploy/.env.production.example
Normal 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
|
||||||
34
deploy/frontend-entrypoint.sh
Normal file
34
deploy/frontend-entrypoint.sh
Normal 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
35
deploy/nginx.conf
Normal 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
40
docker-compose.yml
Normal 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"
|
||||||
Reference in New Issue
Block a user