add deploy: docker-compose yaml

This commit is contained in:
Haitao Pan 2025-12-02 17:29:50 +08:00
parent d7b4ec7f5a
commit 01df0f46d4
13 changed files with 20697 additions and 9776 deletions

View File

@ -4,4 +4,4 @@ enableGlobalCache: false
nodeLinker: node-modules
npmRegistryServer: "https://registry.npmjs.org"
npmRegistryServer: "https://registry.npmmirror.com"

View File

@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,14 @@
This directory contains your keys and certificates.
`[cert name]/privkey.pem` : the private key for your certificate.
`[cert name]/fullchain.pem`: the certificate file used in most server software.
`[cert name]/chain.pem` : used for OCSP stapling in Nginx >=1.3.7.
`[cert name]/cert.pem` : will break many server configurations, and should not be used
without reading further documentation (see link below).
WARNING: DO NOT MOVE OR RENAME THESE FILES!
Certbot expects these files to remain in this location in order
to function properly!
We recommend not moving these files. For more information, see the Certbot
User Guide at https://certbot.eff.org/docs/using.html#where-are-my-certificates.

View File

@ -0,0 +1,168 @@
services:
zitadel-external-tls:
extends:
service: zitadel-init
command: 'start-from-setup --masterkey "MasterkeyNeedsToHave32Characters"'
environment:
ZITADEL_EXTERNALPORT: 443
ZITADEL_EXTERNALSECURE: true
ZITADEL_TLS_ENABLED: false
networks:
- app
- db
depends_on:
db:
condition: 'service_healthy'
zitadel-init:
condition: 'service_completed_successfully'
zitadel-enabled-tls:
extends:
service: zitadel-init
command: 'start-from-setup --masterkey "MasterkeyNeedsToHave32Characters"'
environment:
ZITADEL_EXTERNALPORT: 443
ZITADEL_EXTERNALSECURE: true
ZITADEL_TLS_ENABLED: true
ZITADEL_TLS_CERTPATH: /etc/letsencrypt/live/auth.svc.plus/fullchain.pem
ZITADEL_TLS_KEYPATH: /etc/letsencrypt/live/auth.svc.plus/privkey.pem
volumes:
- ./certbot/conf:/etc/letsencrypt
networks:
- app
- db
depends_on:
zitadel-init:
condition: 'service_completed_successfully'
db:
condition: 'service_healthy'
zitadel-init:
image: '${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:latest}'
command: 'init'
depends_on:
db:
condition: 'service_healthy'
environment:
# Using an external domain other than localhost proofs, that the proxy configuration works.
# If Zitadel can't resolve a requests original host to this domain,
# it will return a 404 Instance not found error.
ZITADEL_EXTERNALDOMAIN: auth.svc.plus
# In case something doesn't work as expected,
# it can be handy to be able to read the access logs.
ZITADEL_LOGSTORE_ACCESS_STDOUT_ENABLED: true
# For convenience, ZITADEL should not ask to change the initial admin users password.
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: false
# database configuration
ZITADEL_DATABASE_POSTGRES_HOST: db
ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel_pw
# Set up a service account with IAM_LOGIN_CLIENT role and write the PAT to the file ./login-client.pat
ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH: /current-dir/login-client.pat
ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_USERNAME: login-client
ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_NAME: Automatically Initialized IAM Login Client
ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_PAT_EXPIRATIONDATE: '2029-01-01T00:00:00Z'
# The master key is used to
networks:
- db
healthcheck:
test: [ "CMD", "/app/zitadel", "ready" ]
interval: '10s'
timeout: '5s'
retries: 5
start_period: '10s'
volumes:
- '.:/current-dir:rw'
db:
restart: 'always'
image: postgres:17-alpine
environment:
POSTGRES_PASSWORD: postgres
healthcheck:
test: [ "CMD-SHELL", "pg_isready" ]
interval: 5s
timeout: 60s
retries: 10
start_period: 5s
networks:
- db
volumes:
- 'data:/var/lib/postgresql/data:rw'
login-external-tls:
restart: 'unless-stopped'
image: 'ghcr.io/zitadel/zitadel-login:latest'
environment:
- ZITADEL_API_URL=http://zitadel-external-tls:8080
- NEXT_PUBLIC_BASE_PATH=/ui/v2/login
- ZITADEL_SERVICE_USER_TOKEN_FILE=/current-dir/login-client.pat
- CUSTOM_REQUEST_HEADERS=Host:auth.svc.plus
volumes:
- '.:/current-dir:ro'
networks:
- app
depends_on:
zitadel-external-tls:
condition: 'service_healthy'
login-enabled-tls:
restart: 'unless-stopped'
image: 'ghcr.io/zitadel/zitadel-login:latest'
environment:
- ZITADEL_API_URL=https://zitadel-enabled-tls:8080
- NEXT_PUBLIC_BASE_PATH=/ui/v2/login
- ZITADEL_SERVICE_USER_TOKEN_FILE=/current-dir/login-client.pat
- CUSTOM_REQUEST_HEADERS=Host:auth.svc.plus
- NODE_TLS_REJECT_UNAUTHORIZED=0
volumes:
- '.:/current-dir:ro'
networks:
- app
depends_on:
zitadel-enabled-tls:
condition: 'service_healthy'
proxy-external-tls:
image: nginx:mainline-alpine
container_name: proxy-external-tls
restart: unless-stopped
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot
ports:
- "80:80"
- "443:443"
networks:
- app
depends_on:
zitadel-external-tls:
condition: service_healthy
certbot:
image: certbot/certbot
container_name: certbot
command: >
certonly --webroot
--webroot-path=/var/www/certbot
--email manbuzhe2009@qq.com
--agree-tos
--no-eff-email
-d auth.svc.plus
volumes:
- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot
depends_on:
proxy-external-tls:
condition: service_started
networks:
- app
networks:
app:
db:
volumes:
data:

View File

@ -0,0 +1,40 @@
server {
listen 80;
server_name accounts.svc.plus;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name accounts.svc.plus;
ssl_certificate /etc/ssl/svc.plus.pem;
ssl_certificate_key /etc/ssl/svc.plus.rsa.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location ^~ /api/auth/ {
proxy_pass http://127.0.0.1:8080;
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;
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, Cookie" always;
add_header Access-Control-Allow-Credentials "true" always;
if ($request_method = OPTIONS) {
return 204;
}
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
proxy_cookie_path / "/; Secure; HttpOnly; SameSite=None";
}
}

View File

@ -0,0 +1,47 @@
server {
listen 443 ssl;
server_name dl.svc.plus cn-dl.svc.plus;
ssl_certificate /etc/ssl/svc.plus.pem;
ssl_certificate_key /etc/ssl/svc.plus.rsa.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
root /data/update-server;
index index.html;
location ^~ /.well-known/ { allow all; }
# ✅ JSON 专用——放在 / 之前
location ~* \.json$ {
try_files $uri =404;
add_header Cache-Control "public, max-age=60, s-maxage=60, stale-while-revalidate=300";
default_type application/json;
}
# 目录浏览
location / {
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
add_header Accept-Ranges bytes;
try_files $uri $uri/ =404;
}
# 大包直出
location ~* \.(?:dmg|zip|tar\.gz|deb|rpm|exe|pkg|appimage|apk|ipa)$ {
expires 7d;
access_log off;
add_header Cache-Control "public";
add_header Accept-Ranges bytes;
}
# 隐藏 dotfiles不拦 /.well-known/
location ~ /\.(?!well-known/)[^/]+ { deny all; }
}
server {
listen 80;
server_name dl.svc.plus cn-dl.svc.plus;
return 301 https://$host$request_uri;
}

View File

@ -0,0 +1,136 @@
server {
listen 80;
server_name www.svc.plus cn-homepage.svc.plus;
# Certbot HTTP-01 challenge
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# All HTTP → HTTPS
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name www.svc.plus cn-homepage.svc.plus;
ssl_certificate /etc/letsencrypt/live/auth.svc.plus/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/auth.svc.plus/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# ====== 静态根目录Next.js export 产物)======
root /dashboard/;
index index.html;
# (可选)放行 ACME/健康检查等
location ^~ /.well-known/ { allow all; }
# =======================
# API 反向代理(保持原样)
# =======================
location /api/ {
proxy_pass http://127.0.0.1:8080;
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;
}
# /api/askai 接口限流(保持原样)
location = /api/askai {
access_by_lua_block {
local redis = require "resty.redis"
local r = redis:new()
r:set_timeout(200)
local ok, err = r:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "Redis connect error: ", err)
return ngx.exit(500)
end
local user = ngx.var.arg_user or ngx.var.remote_addr
local today = os.date("%Y%m%d")
local key = "limit:user:" .. user .. ":" .. today
local count, err = r:incr(key)
if count == 1 then r:expire(key, 86400) end
if count > 200 then
ngx.status = 429
ngx.header["Content-Type"] = "text/plain; charset=utf-8"
ngx.say("Too Many Requests: daily limit reached")
return ngx.exit(429)
end
}
proxy_pass http://127.0.0.1:8080;
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;
}
# =======================
# 静态文件直出(替换原先的 Next.js 动态代理)
# =======================
# Next 导出的静态资源hash 不变 -> 长缓存)
location ^~ /_next/static/ {
try_files $uri =404;
access_log off;
expires 1y;
add_header Cache-Control "public, immutable, max-age=31536000";
}
# 其他常见静态资源:中等缓存
location ~* \.(?:js|css|png|jpg|jpeg|gif|svg|webp|ico|woff2?|ttf)$ {
try_files $uri =404;
access_log off;
expires 7d;
add_header Cache-Control "public, max-age=604800";
}
# 主页与已导出的所有路由:按文件/目录匹配
# 未命中的交给 404.html保持静态站语义
location / {
try_files $uri $uri/ /index.html =404;
}
# 显式处理 404/500 路由目录Next export 会生成 404/、500/ 与同名 .html
location = /404.html { internal; }
error_page 404 /404.html;
# 如果有 /favicon.ico则直接给文件
location = /favicon.ico {
try_files /favicon.ico =204;
access_log off;
expires 30d;
add_header Cache-Control "public, max-age=2592000";
}
# (可选)为某些目录开启目录索引(你有 dl-index、docs、download
# 若需要列表页可以这样做;不需要则删除本段
location ^~ /dl-index/ {
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
try_files $uri $uri/ =404;
}
# 拒绝访问隐藏文件(如 .env
location ~ /\. {
deny all;
}
# (可选)开启 gzip如启用 ngx_brotli也可再加 br
gzip on;
gzip_comp_level 5;
gzip_min_length 1k;
gzip_types text/plain text/css application/javascript application/json application/xml image/svg+xml;
gzip_vary on;
}

View File

@ -0,0 +1,69 @@
server {
listen 80;
server_name rag-server.svc.plus api.svc.plus;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name rag-server.svc.plus api.svc.plus;
ssl_certificate /etc/ssl/svc.plus.pem;
ssl_certificate_key /etc/ssl/svc.plus.rsa.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location ^~ /api/ {
proxy_pass http://127.0.0.1:8090;
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;
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, Cookie" always;
add_header Access-Control-Allow-Credentials "true" always;
if ($request_method = OPTIONS) {
return 204;
}
add_header Cache-Control "no-store";
}
location = /api/askai {
access_by_lua_block {
local redis = require "resty.redis"
local r = redis:new()
r:set_timeout(200)
local ok, err = r:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "Redis connect error: ", err)
return ngx.exit(500)
end
local user = ngx.var.arg_user or ngx.var.remote_addr
local today = os.date("%Y%m%d")
local key = "limit:user:" .. user .. ":" .. today
local count, err = r:incr(key)
if count == 1 then r:expire(key, 86400) end
if count > 200 then
ngx.status = 429
ngx.header["Content-Type"] = "text/plain; charset=utf-8"
ngx.say("Too Many Requests: daily limit reached")
return ngx.exit(429)
end
}
proxy_pass http://127.0.0.1:8090;
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;
}
}

View File

@ -0,0 +1,6 @@
events {}
http {
include /etc/nginx/conf.d/*.conf;
}

View File

@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
POSTGRES_VOL="postgres_data"
stop_all() {
docker compose -f docker-compose.yaml down -v || true
}
wait_db() {
until docker exec "$(docker ps -qf 'ancestor=postgres:17-alpine')" \
pg_isready -U postgres >/dev/null 2>&1; do
sleep 2
done
}
case "${1:-}" in
certbot)
docker compose -f docker-compose.yaml run --rm certbot
;;
init)
stop_all
docker compose -f docker-compose.yaml run --rm zitadel-init
docker compose -f docker-compose.yaml up -d
;;
update)
docker compose -f docker-compose.yaml pull
docker compose -f docker-compose.yaml up -d
;;
reset)
stop_all
docker volume rm -f "${POSTGRES_VOL}" || true
rm -rf ./data && mkdir -p ./data
;;
*)
echo "Usage: $0 {init|update|reset|certbot}"
exit 1
;;
esac