Merge pull request #99 from cloud-neutral/codex/implement-xcontrol-docker-playbook

Template rag server configuration
This commit is contained in:
cloudneutral 2025-12-10 15:28:40 +08:00 committed by GitHub
commit ff8c0d2c3d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 915 additions and 0 deletions

View File

@ -0,0 +1,113 @@
# XControl Docker role
This role provisions the XControl stack (Postgres, account service, RAG server, dashboard, Redis, and Nginx proxy with Certbot assets). Templates from `templates/` and static assets from `files/` are rendered into `{{ xcontrol_workspace }}` and the Docker Compose stack is started.
## Layout
```
files/
├── docker-compose.yaml
├── config/
│ ├── account.yaml
│ └── server.yaml
├── certbot/
│ ├── conf/
│ └── www/
├── nginx/
│ ├── conf.d/
│ │ ├── accounts.conf
│ │ ├── artifact.conf
│ │ ├── bootstrap-nginx.conf
│ │ ├── default.conf
│ │ ├── homepage.conf
│ │ └── rag-server.conf
│ └── nginx.conf
└── run.sh
```
## Defaults
- `xcontrol_deploy_dir`: `/opt/xcontrol`
- `xcontrol_workspace`: `{{ xcontrol_deploy_dir }}`
- `xcontrol_certbot_domains`: `svc.plus` (comma-separated)
- `xcontrol_certbot_email`: `manbuzhe2009@qq.com`
- `xcontrol_homepage_domain`: `{{ xcontrol_primary_domain }}`
- `xcontrol_homepage_alias_domain`: `www.{{ xcontrol_primary_domain }}`
- `xcontrol_homepage_cn_domain`: `cn-homepage.{{ xcontrol_primary_domain }}`
- `xcontrol_accounts_domain`: `accounts.{{ xcontrol_primary_domain }}`
- `xcontrol_rag_domain`: `rag-server.{{ xcontrol_primary_domain }}`
- `xcontrol_rag_api_domain`: `api.{{ xcontrol_primary_domain }}`
- `xcontrol_artifact_domain`: `dl.{{ xcontrol_primary_domain }}`
- `xcontrol_artifact_cn_domain`: `cn-dl.{{ xcontrol_primary_domain }}`
- `xcontrol_db_host`: `db`
- `xcontrol_db_port`: `5432`
- `xcontrol_db_name`: `xcontrol`
- `xcontrol_db_user`: `xcontrol`
- `xcontrol_db_password`: `xcontrol`
- `xcontrol_account_mode`: `server-agent`
- `xcontrol_account_log_level`: `info`
- `xcontrol_account_auth_enable`: `true`
- `xcontrol_account_public_token`: `xcontrol-public-token-2024`
- `xcontrol_account_refresh_secret`: `xcontrol-refresh-secret-2024`
- `xcontrol_account_access_secret`: `xcontrol-access-secret-2024`
- `xcontrol_account_access_expiry`: `1h`
- `xcontrol_account_refresh_expiry`: `168h`
- `xcontrol_account_server_addr`: `:8080`
- `xcontrol_account_read_timeout`: `15s`
- `xcontrol_account_write_timeout`: `15s`
- `xcontrol_account_public_url`: `https://accounts.{{ xcontrol_primary_domain }}`
- `xcontrol_account_tls_enabled`: `false`
- `xcontrol_account_tls_redirect_http`: `false`
- `xcontrol_account_store_driver`: `postgres`
- `xcontrol_account_db_name`: `{{ xcontrol_db_name }}`
- `xcontrol_account_db_sslmode`: `disable`
- `xcontrol_account_db_max_open_conns`: `30`
- `xcontrol_account_db_max_idle_conns`: `10`
- `xcontrol_account_session_ttl`: `24h`
- `xcontrol_account_session_cache`: `memory`
- `xcontrol_account_smtp_host`: `smtp.example.com`
- `xcontrol_account_smtp_port`: `587`
- `xcontrol_account_smtp_username`: `apikey`
- `xcontrol_account_smtp_password`: `change-me`
- `xcontrol_account_smtp_from`: `XControl Account <no-reply@example.com>`
- `xcontrol_account_smtp_timeout`: `10s`
- `xcontrol_account_smtp_tls_mode`: `auto`
- `xcontrol_account_smtp_tls_insecure_skip_verify`: `false`
- `xcontrol_account_xray_sync_enabled`: `false`
- `xcontrol_account_xray_sync_interval`: `5m`
- `xcontrol_account_xray_output_path`: `/usr/local/etc/xray/config.json`
- `xcontrol_account_xray_template_path`: `account/config/xray.config.template.json`
- `xcontrol_account_xray_validate_command`: `[]`
- `xcontrol_account_xray_restart_command`: `["systemctl", "restart", "xray.service"]`
- `xcontrol_account_agent_id`: `account-primary`
- `xcontrol_rag_server_addr`: `:8090`
- `xcontrol_rag_read_timeout`: `120s`
- `xcontrol_rag_write_timeout`: `120s`
- `xcontrol_rag_public_url`: `https://{{ xcontrol_rag_api_domain }}`
- `xcontrol_rag_allowed_origins`: `["https://{{ xcontrol_rag_api_domain }}", "https://{{ xcontrol_homepage_alias_domain }}", "https://{{ xcontrol_homepage_domain }}", "https://{{ xcontrol_accounts_domain }}", "http://localhost:3000", "http://127.0.0.1:3000"]`
- `xcontrol_rag_auth_enable`: `false`
- `xcontrol_rag_auth_url`: `https://{{ xcontrol_accounts_domain }}`
- `xcontrol_rag_api_base_url`: `https://{{ xcontrol_rag_api_domain }}`
- `xcontrol_rag_public_token`: `xcontrol-public-token-2025`
- `xcontrol_rag_redis_addr`: `""`
- `xcontrol_rag_redis_password`: `""`
- `xcontrol_rag_vectordb_db_name`: `rag`
- `xcontrol_rag_vectordb_sslmode`: `disable`
- `xcontrol_rag_vectordb_pgurl`: `postgres://{{ xcontrol_db_user }}:{{ xcontrol_db_password }}@{{ xcontrol_db_host }}:{{ xcontrol_db_port }}/{{ xcontrol_rag_vectordb_db_name }}?sslmode={{ xcontrol_rag_vectordb_sslmode }}`
- `xcontrol_rag_datasources`: `[{"name": "XControl", "repo": "https://github.com/svc-design/XControl", "path": "docs"}]`
- `xcontrol_rag_sync_repo_proxy`: `""`
- `xcontrol_rag_embedder_provider`: `chutes`
- `xcontrol_rag_embedder_models`: `["bge-m3"]`
- `xcontrol_rag_embedder_baseurl`: `http://127.0.0.1:9000`
- `xcontrol_rag_embedder_endpoint`: `http://127.0.0.1:9000/v1/embeddings`
- `xcontrol_rag_generator_provider`: `chutes`
- `xcontrol_rag_generator_models`: `["deepseek-r1:8b"]`
- `xcontrol_rag_generator_baseurl`: `http://127.0.0.1:11434`
- `xcontrol_rag_generator_endpoint`: `http://127.0.0.1:11434/v1/chat/completions`
- `xcontrol_rag_embedding_max_batch`: `64`
- `xcontrol_rag_embedding_dimension`: `1024`
## RUN
```
ansible-playbook -i inventory.ini deploy_XControl_docker.yaml -e "domain=svc.plus" -D -C -l svc.plus
ansible-playbook -i inventory.ini deploy_XControl_docker.yaml -e "domain=svc.plus" -D -l svc.plus
```

View File

@ -0,0 +1,112 @@
---
# Default deployment directory for XControl Docker stack
xcontrol_deploy_dir: /opt/xcontrol
xcontrol_workspace: "{{ xcontrol_deploy_dir }}"
# Primary domain (first in the comma-separated domain list)
xcontrol_certbot_domains: svc.plus
xcontrol_primary_domain: "{{ xcontrol_certbot_domains.split(',')[0] | trim }}"
xcontrol_certbot_email: manbuzhe2009@qq.com
# Subdomains for individual services
xcontrol_homepage_domain: "{{ xcontrol_primary_domain }}"
xcontrol_homepage_alias_domain: "www.{{ xcontrol_primary_domain }}"
xcontrol_homepage_cn_domain: "cn-homepage.{{ xcontrol_primary_domain }}"
xcontrol_accounts_domain: "accounts.{{ xcontrol_primary_domain }}"
xcontrol_rag_domain: "rag-server.{{ xcontrol_primary_domain }}"
xcontrol_rag_api_domain: "api.{{ xcontrol_primary_domain }}"
xcontrol_artifact_domain: "dl.{{ xcontrol_primary_domain }}"
xcontrol_artifact_cn_domain: "cn-dl.{{ xcontrol_primary_domain }}"
# Database defaults
xcontrol_db_host: db
xcontrol_db_port: 5432
xcontrol_db_name: xcontrol
xcontrol_db_user: xcontrol
xcontrol_db_password: xcontrol
# Account service configuration defaults
xcontrol_account_mode: server-agent
xcontrol_account_log_level: info
xcontrol_account_auth_enable: true
xcontrol_account_public_token: xcontrol-public-token-2024
xcontrol_account_refresh_secret: xcontrol-refresh-secret-2024
xcontrol_account_access_secret: xcontrol-access-secret-2024
xcontrol_account_access_expiry: 1h
xcontrol_account_refresh_expiry: 168h
xcontrol_account_server_addr: ":8080"
xcontrol_account_read_timeout: 15s
xcontrol_account_write_timeout: 15s
xcontrol_account_public_url: "https://{{ xcontrol_accounts_domain }}"
xcontrol_account_tls_enabled: false
xcontrol_account_tls_redirect_http: false
xcontrol_account_store_driver: postgres
xcontrol_account_db_name: "{{ xcontrol_db_name }}"
xcontrol_account_db_sslmode: disable
xcontrol_account_db_max_open_conns: 30
xcontrol_account_db_max_idle_conns: 10
xcontrol_account_session_ttl: 24h
xcontrol_account_session_cache: memory
xcontrol_account_smtp_host: smtp.example.com
xcontrol_account_smtp_port: 587
xcontrol_account_smtp_username: apikey
xcontrol_account_smtp_password: change-me
xcontrol_account_smtp_from: "XControl Account <no-reply@example.com>"
xcontrol_account_smtp_timeout: 10s
xcontrol_account_smtp_tls_mode: auto
xcontrol_account_smtp_tls_insecure_skip_verify: false
xcontrol_account_xray_sync_enabled: false
xcontrol_account_xray_sync_interval: 5m
xcontrol_account_xray_output_path: /usr/local/etc/xray/config.json
xcontrol_account_xray_template_path: account/config/xray.config.template.json
xcontrol_account_xray_validate_command: []
xcontrol_account_xray_restart_command:
- systemctl
- restart
- xray.service
xcontrol_account_agent_id: account-primary
# Image overrides (optional)
xcontrol_account_image: ghcr.io/cloud-neutral-toolkit/account:latest
xcontrol_rag_image: manbuzhe2009/rag-server:latest
xcontrol_dashboard_image: manbuzhe2009/dashboard:latest
xcontrol_db_image: manbuzhe2009/postgres-runtime:latest
# RAG server configuration defaults
xcontrol_rag_server_addr: ":8090"
xcontrol_rag_read_timeout: 120s
xcontrol_rag_write_timeout: 120s
xcontrol_rag_public_url: "https://{{ xcontrol_rag_api_domain }}"
xcontrol_rag_allowed_origins:
- "https://{{ xcontrol_rag_api_domain }}"
- "https://{{ xcontrol_homepage_alias_domain }}"
- "https://{{ xcontrol_homepage_domain }}"
- "https://{{ xcontrol_accounts_domain }}"
- "http://localhost:3000"
- "http://127.0.0.1:3000"
xcontrol_rag_auth_enable: false
xcontrol_rag_auth_url: "https://{{ xcontrol_accounts_domain }}"
xcontrol_rag_api_base_url: "https://{{ xcontrol_rag_api_domain }}"
xcontrol_rag_public_token: xcontrol-public-token-2025
xcontrol_rag_redis_addr: ""
xcontrol_rag_redis_password: ""
xcontrol_rag_vectordb_db_name: rag
xcontrol_rag_vectordb_sslmode: disable
xcontrol_rag_vectordb_pgurl: "postgres://{{ xcontrol_db_user }}:{{ xcontrol_db_password }}@{{ xcontrol_db_host }}:{{ xcontrol_db_port }}/{{ xcontrol_rag_vectordb_db_name }}?sslmode={{ xcontrol_rag_vectordb_sslmode }}"
xcontrol_rag_datasources:
- name: XControl
repo: https://github.com/svc-design/XControl
path: docs
xcontrol_rag_sync_repo_proxy: ""
xcontrol_rag_embedder_provider: chutes
xcontrol_rag_embedder_models:
- bge-m3
xcontrol_rag_embedder_baseurl: http://127.0.0.1:9000
xcontrol_rag_embedder_endpoint: http://127.0.0.1:9000/v1/embeddings
xcontrol_rag_generator_provider: chutes
xcontrol_rag_generator_models:
- deepseek-r1:8b
xcontrol_rag_generator_baseurl: http://127.0.0.1:11434
xcontrol_rag_generator_endpoint: http://127.0.0.1:11434/v1/chat/completions
xcontrol_rag_embedding_max_batch: 64
xcontrol_rag_embedding_dimension: 1024

View File

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

View File

@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
# Helper script to start the XControl docker compose stack
cd "$(dirname "$0")"
docker compose -f docker-compose.yaml up -d

View File

@ -0,0 +1,76 @@
---
- name: Ensure XControl directories exist
become: true
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- "{{ xcontrol_workspace }}"
- "{{ xcontrol_workspace }}/certbot"
- "{{ xcontrol_workspace }}/certbot/conf"
- "{{ xcontrol_workspace }}/certbot/www"
- "{{ xcontrol_workspace }}/config"
- "{{ xcontrol_workspace }}/nginx"
- "{{ xcontrol_workspace }}/nginx/conf.d"
- name: Ensure XControl workspace ownership
become: true
ansible.builtin.file:
path: "{{ xcontrol_workspace }}"
state: directory
recurse: true
owner: "1000"
group: "1000"
mode: "0755"
- name: Template XControl configuration files
become: true
ansible.builtin.template:
src: "{{ item.src }}"
dest: "{{ xcontrol_workspace }}/{{ item.dest }}"
mode: "{{ item.mode | default('0644') }}"
loop:
- { src: 'docker-compose.yaml', dest: 'docker-compose.yaml' }
- { src: 'config/account.yaml', dest: 'config/account.yaml' }
- { src: 'config/server.yaml', dest: 'config/server.yaml' }
- { src: 'nginx/conf.d/default.conf', dest: 'nginx/conf.d/default.conf' }
- { src: 'nginx/conf.d/bootstrap-nginx.conf', dest: 'nginx/conf.d/bootstrap-nginx.conf' }
- { src: 'nginx/conf.d/accounts.conf', dest: 'nginx/conf.d/accounts.conf' }
- { src: 'nginx/conf.d/homepage.conf', dest: 'nginx/conf.d/homepage.conf' }
- { src: 'nginx/conf.d/rag-server.conf', dest: 'nginx/conf.d/rag-server.conf' }
- { src: 'nginx/conf.d/artifact.conf', dest: 'nginx/conf.d/artifact.conf' }
- name: Copy XControl static files
become: true
ansible.builtin.copy:
src: "{{ item.src }}"
dest: "{{ xcontrol_workspace }}/{{ item.dest }}"
mode: "{{ item.mode | default('0644') }}"
loop:
- { src: 'run.sh', dest: 'run.sh', mode: '0755' }
- { src: 'nginx/nginx.conf', dest: 'nginx/nginx.conf' }
- name: Bootstrap NGINX (80-only for ACME)
become: true
command: docker compose --profile bootstrap -f {{ xcontrol_workspace }}/docker-compose.yaml up -d bootstrap-nginx
args:
chdir: "{{ xcontrol_workspace }}"
- name: Run certbot initial ACME challenge
become: true
command: docker compose --profile bootstrap -f {{ xcontrol_workspace }}/docker-compose.yaml run --rm certbot
args:
chdir: "{{ xcontrol_workspace }}"
- name: Destroy Bootstrap NGINX (80-only for ACME)
become: true
command: docker compose --profile bootstrap -f {{ xcontrol_workspace }}/docker-compose.yaml down bootstrap-nginx
args:
chdir: "{{ xcontrol_workspace }}"
- name: Bring up XControl stack
become: true
command: docker compose -f {{ xcontrol_workspace }}/docker-compose.yaml up -d
args:
chdir: "{{ xcontrol_workspace }}"

View File

@ -0,0 +1,63 @@
mode: "{{ xcontrol_account_mode }}"
log:
level: {{ xcontrol_account_log_level | to_nice_yaml(indent=0) | trim }}
auth:
enable: {{ xcontrol_account_auth_enable | bool | lower }}
token:
publicToken: "{{ xcontrol_account_public_token }}"
refreshSecret: "{{ xcontrol_account_refresh_secret }}"
accessSecret: "{{ xcontrol_account_access_secret }}"
accessExpiry: "{{ xcontrol_account_access_expiry }}"
refreshExpiry: "{{ xcontrol_account_refresh_expiry }}"
server:
addr: "{{ xcontrol_account_server_addr }}"
readTimeout: {{ xcontrol_account_read_timeout | to_nice_yaml(indent=0) | trim }}
writeTimeout: {{ xcontrol_account_write_timeout | to_nice_yaml(indent=0) | trim }}
publicUrl: "{{ xcontrol_account_public_url }}"
allowedOrigins:
- "https://{{ xcontrol_accounts_domain }}"
- "https://{{ xcontrol_rag_api_domain }}"
- "https://{{ xcontrol_homepage_alias_domain }}"
- "http://localhost:3000"
- "http://127.0.0.1:3000"
- "http://localhost:8080"
- "http://127.0.0.1:8080"
tls:
enabled: {{ xcontrol_account_tls_enabled | bool | lower }}
redirectHttp: {{ xcontrol_account_tls_redirect_http | bool | lower }}
store:
driver: "{{ xcontrol_account_store_driver }}"
dsn: "postgres://{{ xcontrol_db_user }}:{{ xcontrol_db_password }}@{{ xcontrol_db_host }}:{{ xcontrol_db_port }}/{{ xcontrol_account_db_name }}?sslmode={{ xcontrol_account_db_sslmode }}"
maxOpenConns: {{ xcontrol_account_db_max_open_conns }}
maxIdleConns: {{ xcontrol_account_db_max_idle_conns }}
session:
ttl: {{ xcontrol_account_session_ttl | to_nice_yaml(indent=0) | trim }}
cache: "{{ xcontrol_account_session_cache }}"
smtp:
host: "{{ xcontrol_account_smtp_host }}"
port: {{ xcontrol_account_smtp_port }}
username: "{{ xcontrol_account_smtp_username }}"
password: "{{ xcontrol_account_smtp_password }}"
from: "{{ xcontrol_account_smtp_from }}"
timeout: {{ xcontrol_account_smtp_timeout | to_nice_yaml(indent=0) | trim }}
tls:
mode: "{{ xcontrol_account_smtp_tls_mode }}"
insecureSkipVerify: {{ xcontrol_account_smtp_tls_insecure_skip_verify | bool | lower }}
xray:
sync:
enabled: {{ xcontrol_account_xray_sync_enabled | bool | lower }}
interval: {{ xcontrol_account_xray_sync_interval | to_nice_yaml(indent=0) | trim }}
outputPath: "{{ xcontrol_account_xray_output_path }}"
templatePath: "{{ xcontrol_account_xray_template_path }}"
validateCommand: {{ xcontrol_account_xray_validate_command | to_nice_yaml(indent=2) | trim }}
restartCommand: {{ xcontrol_account_xray_restart_command | to_nice_yaml(indent=2) | trim }}
agent:
id: "{{ xcontrol_account_agent_id }}"

View File

@ -0,0 +1,54 @@
server:
addr: "{{ xcontrol_rag_server_addr }}"
readTimeout: "{{ xcontrol_rag_read_timeout }}"
writeTimeout: "{{ xcontrol_rag_write_timeout }}"
publicUrl: "{{ xcontrol_rag_public_url }}"
allowedOrigins:
{% for origin in xcontrol_rag_allowed_origins %}
- "{{ origin }}"
{% endfor %}
auth:
enable: {{ xcontrol_rag_auth_enable | bool }}
authUrl: "{{ xcontrol_rag_auth_url }}"
apiBaseUrl: "{{ xcontrol_rag_api_base_url }}"
publicToken: "{{ xcontrol_rag_public_token }}"
global:
redis:
addr: "{{ xcontrol_rag_redis_addr }}"
password: "{{ xcontrol_rag_redis_password }}"
vectordb:
pgurl: "{{ xcontrol_rag_vectordb_pgurl }}"
datasources:
{% for datasource in xcontrol_rag_datasources %}
- name: "{{ datasource.name }}"
repo: "{{ datasource.repo }}"
path: "{{ datasource.path }}"
{% endfor %}
sync:
repo:
proxy: "{{ xcontrol_rag_sync_repo_proxy }}"
models:
embedder:
provider: "{{ xcontrol_rag_embedder_provider }}"
models:
{% for model in xcontrol_rag_embedder_models %}
- "{{ model }}"
{% endfor %}
baseurl: "{{ xcontrol_rag_embedder_baseurl }}"
endpoint: "{{ xcontrol_rag_embedder_endpoint }}"
generator:
provider: "{{ xcontrol_rag_generator_provider }}"
models:
{% for model in xcontrol_rag_generator_models %}
- "{{ model }}"
{% endfor %}
baseurl: "{{ xcontrol_rag_generator_baseurl }}"
endpoint: "{{ xcontrol_rag_generator_endpoint }}"
embedding:
max_batch: {{ xcontrol_rag_embedding_max_batch }}
dimension: {{ xcontrol_rag_embedding_dimension }}

View File

@ -0,0 +1,147 @@
services:
db:
image: "{{ xcontrol_db_image }}"
container_name: xcontrol-db
restart: unless-stopped
environment:
POSTGRES_DB: "{{ xcontrol_db_name }}"
POSTGRES_USER: "{{ xcontrol_db_user }}"
POSTGRES_PASSWORD: "{{ xcontrol_db_password }}"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U {{ xcontrol_db_user }}"]
interval: 5s
timeout: 60s
retries: 10
start_period: 5s
volumes:
- "data:/var/lib/postgresql/data:rw"
networks:
- db
account:
image: "{{ xcontrol_account_image }}"
container_name: account
restart: unless-stopped
environment:
PORT: 8080
CONFIG_PATH: /etc/xcontrol/account-compose.yaml
volumes:
- "{{ xcontrol_workspace }}/config/account.yaml:/etc/xcontrol/account-compose.yaml:ro"
depends_on:
db:
condition: service_healthy
ports:
- "8080:8080"
networks:
- app
- db
rag-server:
image: "{{ xcontrol_rag_image }}"
container_name: rag-server
restart: unless-stopped
environment:
PORT: 8090
CONFIG_PATH: /etc/rag-server/server-compose.yaml
volumes:
- "{{ xcontrol_workspace }}/config/server.yaml:/etc/rag-server/server-compose.yaml:ro"
depends_on:
db:
condition: service_healthy
ports:
- "8090:8090"
networks:
- app
- db
dashboard:
image: "{{ xcontrol_dashboard_image }}"
container_name: dashboard
restart: unless-stopped
environment:
PORT: 3000
ports:
- "3000:3000"
depends_on:
account:
condition: service_started
rag-server:
condition: service_started
networks:
- app
proxy-external-tls:
image: nginx:mainline-alpine
container_name: proxy-external-tls
restart: unless-stopped
volumes:
- "{{ xcontrol_workspace }}/nginx/nginx.conf:/etc/nginx/nginx.conf"
- "{{ xcontrol_workspace }}/nginx/conf.d:/etc/nginx/conf.d:ro"
- "{{ xcontrol_workspace }}/certbot/conf:/etc/letsencrypt"
- "{{ xcontrol_workspace }}/certbot/www:/var/www/certbot"
ports:
- "80:80"
- "443:443"
networks:
- app
depends_on:
account:
condition: service_started
rag-server:
condition: service_started
dashboard:
condition: service_started
redis:
image: redis:7-alpine
container_name: redis
restart: unless-stopped
command: ["redis-server", "--save", "", "--appendonly", "no"]
networks:
- app
bootstrap-nginx:
profiles: ["bootstrap"]
image: nginx:mainline-alpine
container_name: bootstrap-nginx
volumes:
- "{{ xcontrol_workspace }}/certbot/www:/var/www/certbot"
- "{{ xcontrol_workspace }}/certbot/conf:/etc/letsencrypt"
- "{{ xcontrol_workspace }}/nginx/nginx.conf:/etc/nginx/nginx.conf"
- "{{ xcontrol_workspace }}/nginx/conf.d/bootstrap-nginx.conf:/etc/nginx/conf.d/default.conf"
ports:
- "80:80"
networks:
- app
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost"]
interval: 3s
timeout: 2s
retries: 10
start_period: 3s
certbot:
profiles: ["bootstrap"]
image: certbot/certbot
container_name: certbot
command: >
certonly --webroot
--webroot-path=/var/www/certbot
--email {{ xcontrol_certbot_email }}
--agree-tos
--no-eff-email
--keep-until-expiring
--non-interactive
-d {{ xcontrol_certbot_domains }}
volumes:
- "{{ xcontrol_workspace }}/certbot/conf:/etc/letsencrypt"
- "{{ xcontrol_workspace }}/certbot/www:/var/www/certbot"
networks:
- app
networks:
app:
db:
volumes:
data:

View File

@ -0,0 +1,40 @@
server {
listen 80;
server_name {{ xcontrol_accounts_domain }};
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name {{ xcontrol_accounts_domain }};
ssl_certificate /etc/letsencrypt/live/{{ xcontrol_primary_domain }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ xcontrol_primary_domain }}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location ^~ /api/auth/ {
proxy_pass http://account: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 {{ xcontrol_artifact_domain }} {{ xcontrol_artifact_cn_domain }};
ssl_certificate /etc/letsencrypt/live/{{ xcontrol_primary_domain }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ xcontrol_primary_domain }}/privkey.pem;
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 {{ xcontrol_artifact_domain }} {{ xcontrol_artifact_cn_domain }};
return 301 https://$host$request_uri;
}

View File

@ -0,0 +1,12 @@
server {
listen 80;
server_name _;
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 200 "bootstrap";
}
}

View File

@ -0,0 +1,35 @@
# ----------------------------------------------------
# 80 - ACME Challenge + Redirect to HTTPS for homepage
# ----------------------------------------------------
server {
listen 80;
server_name {{ xcontrol_homepage_domain }};
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
# ----------------------------------------------------
# 443 - TLS Termination for homepage
# ----------------------------------------------------
server {
listen 443 ssl http2;
server_name {{ xcontrol_homepage_domain }};
ssl_certificate /etc/letsencrypt/live/{{ xcontrol_primary_domain }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ xcontrol_primary_domain }}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://dashboard:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}
}

View File

@ -0,0 +1,136 @@
server {
listen 80;
server_name {{ xcontrol_homepage_alias_domain }} {{ xcontrol_homepage_cn_domain }};
# 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 {{ xcontrol_homepage_alias_domain }} {{ xcontrol_homepage_cn_domain }};
ssl_certificate /etc/letsencrypt/live/{{ xcontrol_primary_domain }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ xcontrol_primary_domain }}/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://account: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("redis", 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://account: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 {{ xcontrol_rag_domain }} {{ xcontrol_rag_api_domain }};
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name {{ xcontrol_rag_domain }} {{ xcontrol_rag_api_domain }};
ssl_certificate /etc/letsencrypt/live/{{ xcontrol_primary_domain }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ xcontrol_primary_domain }}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location ^~ /api/ {
proxy_pass http://rag-server: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("redis", 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://rag-server: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;
}
}