ops: add isolated production deployment
This commit is contained in:
parent
088a24c3a9
commit
a85acabde5
5
backend/.dockerignore
Normal file
5
backend/.dockerignore
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
.env
|
||||
npm-debug.log*
|
||||
coverage
|
||||
runtime
|
||||
16
backend/Dockerfile.production
Normal file
16
backend/Dockerfile.production
Normal file
@ -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"]
|
||||
14
deploy/production/README.md
Normal file
14
deploy/production/README.md
Normal file
@ -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 管理账号使用独立密钥,关闭密码登录。
|
||||
79
deploy/production/docker-compose.yml
Normal file
79
deploy/production/docker-compose.yml
Normal file
@ -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
|
||||
69
deploy/production/nginx-ck.yydd.ai.conf
Normal file
69
deploy/production/nginx-ck.yydd.ai.conf
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
15
remote-access/server/Dockerfile.production
Normal file
15
remote-access/server/Dockerfile.production
Normal file
@ -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"]
|
||||
12
remote-access/server/docker-entrypoint.sh
Normal file
12
remote-access/server/docker-entrypoint.sh
Normal file
@ -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
|
||||
@ -1,7 +1,6 @@
|
||||
Match User cangku-tunnel
|
||||
AuthenticationMethods publickey
|
||||
PasswordAuthentication no
|
||||
KbdInteractiveAuthentication no
|
||||
AllowTcpForwarding local
|
||||
PermitOpen 127.0.0.1:3307
|
||||
GatewayPorts no
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user