diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..5f9713d --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,5 @@ +node_modules +.env +npm-debug.log* +coverage +runtime diff --git a/backend/Dockerfile.production b/backend/Dockerfile.production new file mode 100644 index 0000000..dfa160a --- /dev/null +++ b/backend/Dockerfile.production @@ -0,0 +1,16 @@ +FROM node:22-bookworm-slim + +ENV NODE_ENV=production +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev --no-audit --no-fund + +COPY . . + +EXPOSE 4090 +CMD ["node", "src/index.js"] diff --git a/deploy/production/README.md b/deploy/production/README.md new file mode 100644 index 0000000..3f46d5d --- /dev/null +++ b/deploy/production/README.md @@ -0,0 +1,14 @@ +# 生产部署(Docker) + +生产实例使用三个隔离容器:MySQL 系统数据库、Node.js 后端和部署注册服务。MySQL、后端与注册服务只映射到服务器回环地址,公网仅由 Nginx 暴露 `ck.yydd.ai:8443`。 + +服务器目录约定: + +- `/opt/cangku/app`:仓库代码和前端构建结果 +- `/opt/cangku/shared`:权限为 `0700` 的环境配置 +- `/opt/cangku/data`:MySQL 和部署注册服务持久数据 +- `/www/wwwroot/cangku-acme`:Let's Encrypt HTTP-01 校验目录 + +真实密码、JWT 密钥、部署 Key 和第三方数据库配置不得写入仓库。`mysql.env`、`backend.env` 和 `bootstrap.env` 只在服务器生成并保持 `0600` 权限。 + +SSH 隧道账号只接受由注册服务 CA 签发的短期证书,只允许转发到服务器 `127.0.0.1:3307`,并关闭终端、TTY、代理转发和密码认证。生产主机的 SSH 管理账号使用独立密钥,关闭密码登录。 diff --git a/deploy/production/docker-compose.yml b/deploy/production/docker-compose.yml new file mode 100644 index 0000000..c324e03 --- /dev/null +++ b/deploy/production/docker-compose.yml @@ -0,0 +1,79 @@ +name: cangku + +services: + mysql: + image: mysql:8.0.39 + container_name: cangku-mysql + restart: unless-stopped + env_file: + - /opt/cangku/shared/mysql.env + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + ports: + - 127.0.0.1:3307:3306 + volumes: + - /opt/cangku/data/mysql:/var/lib/mysql + - ../../backend/database/init.sql:/docker-entrypoint-initdb.d/001-init.sql:ro + - ../../backend/database/summary_tables.sql:/docker-entrypoint-initdb.d/002-summary-tables.sql:ro + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent"] + interval: 5s + timeout: 5s + retries: 30 + start_period: 30s + networks: + - cangku-internal + + backend: + build: + context: ../../backend + dockerfile: Dockerfile.production + container_name: cangku-backend + restart: unless-stopped + env_file: + - /opt/cangku/shared/backend.env + ports: + - 127.0.0.1:4090:4090 + volumes: + - /opt/cangku/data/backend:/app/runtime + depends_on: + mysql: + condition: service_healthy + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:4090/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"] + interval: 10s + timeout: 5s + retries: 18 + start_period: 30s + networks: + - cangku-internal + + bootstrap: + build: + context: ../../remote-access/server + dockerfile: Dockerfile.production + container_name: cangku-bootstrap + restart: unless-stopped + env_file: + - /opt/cangku/shared/bootstrap.env + ports: + - 127.0.0.1:4092:4092 + volumes: + - /opt/cangku/data/bootstrap:/var/lib/cangku-bootstrap + - /etc/ssh/ssh_host_ed25519_key.pub:/etc/ssh/ssh_host_ed25519_key.pub:ro + depends_on: + mysql: + condition: service_healthy + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:4092/health', timeout=3)"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 10s + networks: + - cangku-internal + +networks: + cangku-internal: + name: cangku-internal diff --git a/deploy/production/nginx-ck.yydd.ai.conf b/deploy/production/nginx-ck.yydd.ai.conf new file mode 100644 index 0000000..3293fc7 --- /dev/null +++ b/deploy/production/nginx-ck.yydd.ai.conf @@ -0,0 +1,69 @@ +server { + listen 80; + server_name ck.yydd.ai; + + location ^~ /.well-known/acme-challenge/ { + root /www/wwwroot/cangku-acme; + default_type text/plain; + } + + location / { + return 308 https://$host:8443$request_uri; + } +} + +server { + listen 8443 ssl http2; + server_name ck.yydd.ai; + + ssl_certificate /etc/letsencrypt/live/ck.yydd.ai/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/ck.yydd.ai/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_session_cache shared:CangkuTLS:10m; + ssl_session_timeout 1d; + + root /opt/cangku/app/frontend/dist; + index index.html; + client_max_body_size 100m; + + add_header X-Content-Type-Options nosniff always; + add_header Referrer-Policy strict-origin-when-cross-origin always; + + location = /health { + proxy_pass http://127.0.0.1:4090/health; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + location = /v1/enroll { + limit_except POST { deny all; } + proxy_pass http://127.0.0.1:4092/v1/enroll; + 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 https; + } + + location /api/ { + proxy_pass http://127.0.0.1:4090; + 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 https; + } + + location /cubejs-api/ { + proxy_pass http://127.0.0.1:4090; + 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 https; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/remote-access/server/Dockerfile.production b/remote-access/server/Dockerfile.production new file mode 100644 index 0000000..c62d434 --- /dev/null +++ b/remote-access/server/Dockerfile.production @@ -0,0 +1,15 @@ +FROM python:3.12-slim-bookworm + +RUN apt-get update \ + && apt-get install -y --no-install-recommends openssh-client ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +COPY bootstrap_service.py docker-entrypoint.sh ./ +RUN chmod 0755 /app/docker-entrypoint.sh + +EXPOSE 4092 +ENTRYPOINT ["/app/docker-entrypoint.sh"] diff --git a/remote-access/server/docker-entrypoint.sh b/remote-access/server/docker-entrypoint.sh new file mode 100644 index 0000000..37288dd --- /dev/null +++ b/remote-access/server/docker-entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/sh +set -eu + +ca_key=${BOOTSTRAP_SSH_CA_KEY:-/var/lib/cangku-bootstrap/ssh_user_ca} +if [ ! -f "$ca_key" ]; then + mkdir -p "$(dirname "$ca_key")" + ssh-keygen -q -t ed25519 -N '' -C cangku-bootstrap-user-ca -f "$ca_key" +fi +chmod 0600 "$ca_key" +chmod 0644 "$ca_key.pub" + +exec python /app/bootstrap_service.py serve diff --git a/remote-access/server/sshd-cangku-tunnel.conf b/remote-access/server/sshd-cangku-tunnel.conf index ea72354..698a778 100644 --- a/remote-access/server/sshd-cangku-tunnel.conf +++ b/remote-access/server/sshd-cangku-tunnel.conf @@ -1,7 +1,6 @@ Match User cangku-tunnel AuthenticationMethods publickey PasswordAuthentication no - KbdInteractiveAuthentication no AllowTcpForwarding local PermitOpen 127.0.0.1:3307 GatewayPorts no