Refactor ACP vhosts deployment layout

This commit is contained in:
Haitao Pan 2026-04-09 14:16:05 +08:00
parent 9d6e59e802
commit 672ea8ba32
72 changed files with 2167 additions and 164 deletions

View File

@ -0,0 +1,12 @@
---
- name: Deploy ACP Codex vhosts
hosts: all
become: true
gather_facts: true
roles:
- role: roles/vhosts/deploy_acp_vhosts/
vars:
deploy_acp_codex: true
deploy_acp_opencode: false
deploy_acp_gemini: false
tags: [deploy_acp_vhosts, acp_codex]

View File

@ -0,0 +1,12 @@
---
- name: Deploy ACP Gemini vhosts
hosts: all
become: true
gather_facts: true
roles:
- role: roles/vhosts/deploy_acp_vhosts/
vars:
deploy_acp_codex: false
deploy_acp_opencode: false
deploy_acp_gemini: true
tags: [deploy_acp_vhosts, acp_gemini]

View File

@ -1,6 +1,12 @@
---
- import_playbook: deploy_acp_vhosts.yml
vars:
deploy_acp_codex: false
deploy_acp_opencode: true
deploy_acp_unified: false
- name: Deploy ACP OpenCode vhosts
hosts: all
become: true
gather_facts: true
roles:
- role: roles/vhosts/deploy_acp_vhosts/
vars:
deploy_acp_codex: false
deploy_acp_opencode: true
deploy_acp_gemini: false
tags: [deploy_acp_vhosts, acp_opencode]

View File

@ -1,23 +0,0 @@
---
- name: Deploy ACP vhosts
hosts: all
become: true
gather_facts: true
vars:
deploy_acp_codex: true
deploy_acp_opencode: true
deploy_acp_unified: true
deploy_acp_bridge_server: true
roles:
- role: roles/vhosts/acp_codex/
when: deploy_acp_codex
tags: [acp_codex]
- role: roles/vhosts/acp_opencode/
when: deploy_acp_opencode
tags: [acp_opencode]
- role: roles/vhosts/acp_bridge_server/
when: deploy_acp_bridge_server
tags: [acp_bridge_server]
- role: roles/vhosts/acp_vhosts/
when: deploy_acp_unified
tags: [acp_vhosts]

83
deploy_agent_svc_plus.yml Normal file
View File

@ -0,0 +1,83 @@
- name: Deploy managed agent.svc.plus service
hosts: "{{ agent_service_hosts | default('agent_svc_plus') }}"
gather_facts: true
become: true
vars:
agent_svc_plus_repo_url: >-
{{ lookup('ansible.builtin.env', 'AGENT_REPO_URL')
| default('https://github.com/x-evor/agent.svc.plus.git', true) }}
agent_svc_plus_repo_version: >-
{{ lookup('ansible.builtin.env', 'AGENT_REPO_VERSION')
| default('main', true) }}
agent_svc_plus_app_dir: >-
{{ lookup('ansible.builtin.env', 'AGENT_APP_DIR')
| default('/opt/agent.svc.plus', true) }}
agent_svc_plus_go_version: >-
{{ lookup('ansible.builtin.env', 'AGENT_GO_VERSION')
| default('1.25.1', true) }}
agent_id: >-
{{ lookup('ansible.builtin.env', 'AGENT_ID')
| default('node-xhttp.svc.plus', true) }}
agent_controller_url: >-
{{ lookup('ansible.builtin.env', 'AGENT_CONTROLLER_URL')
| default('https://accounts.svc.plus', true) }}
agent_api_token: >-
{{ lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN')
| default('', true) }}
agent_billing_enabled: >-
{{ lookup('ansible.builtin.env', 'AGENT_BILLING_ENABLED')
| default(true, true) | bool }}
agent_billing_base_url: >-
{{ lookup('ansible.builtin.env', 'BILLING_SERVICE_BASE_URL')
| default('http://127.0.0.1:8081', true) }}
agent_billing_http_timeout: >-
{{ lookup('ansible.builtin.env', 'AGENT_BILLING_HTTP_TIMEOUT')
| default('15s', true) }}
agent_billing_collect_interval: >-
{{ lookup('ansible.builtin.env', 'AGENT_BILLING_COLLECT_INTERVAL')
| default('1m', true) }}
agent_billing_reconcile_interval: >-
{{ lookup('ansible.builtin.env', 'AGENT_BILLING_RECONCILE_INTERVAL')
| default('5m', true) }}
xray_enabled: >-
{{ lookup('ansible.builtin.env', 'AGENT_XRAY_ENABLED')
| default(true, true) | bool }}
xray_uuid: >-
{{ lookup('ansible.builtin.env', 'XRAY_UUID')
| default('00000000-0000-0000-0000-000000000000', true) }}
pre_tasks:
- name: Validate INTERNAL_SERVICE_TOKEN is present
ansible.builtin.assert:
that:
- agent_api_token | length > 0
fail_msg: "INTERNAL_SERVICE_TOKEN must be exported before running this playbook."
success_msg: "INTERNAL_SERVICE_TOKEN found"
- name: Gather service facts
ansible.builtin.service_facts:
- name: Assert host is bootstrapped with setup-proxy.sh services
ansible.builtin.assert:
that:
- "'xray.service' in ansible_facts.services"
- "'xray-tcp.service' in ansible_facts.services"
- "'caddy.service' in ansible_facts.services"
fail_msg: "Target host must already be bootstrapped by setup-proxy.sh (missing xray.service, xray-tcp.service, or caddy.service)."
success_msg: "Target host already has the setup-proxy.sh service layout."
- name: Assert setup-proxy.sh config paths exist
ansible.builtin.stat:
path: "{{ item }}"
loop:
- /etc/caddy/Caddyfile
- /usr/local/etc/xray/templates
register: agent_bootstrap_paths
- name: Validate setup-proxy.sh config paths are present
ansible.builtin.assert:
that:
- agent_bootstrap_paths.results | map(attribute='stat.exists') | min
fail_msg: "Target host is missing /etc/caddy/Caddyfile or /usr/local/etc/xray/templates. Run setup-proxy.sh first."
success_msg: "setup-proxy.sh config paths exist."
roles:
- roles/vhosts/agent-svc-plus

View File

@ -0,0 +1,44 @@
- name: Deploy billing-service
hosts: "{{ billing_service_hosts | default('billing_service') }}"
gather_facts: true
become: true
vars:
billing_service_source_dir: >-
{{ lookup('ansible.builtin.env', 'BILLING_SERVICE_SOURCE_DIR')
| default(playbook_dir ~ '/../billing-service', true) }}
billing_service_exporter_base_url: >-
{{ lookup('ansible.builtin.env', 'EXPORTER_BASE_URL')
| default('http://127.0.0.1:8080', true) }}
billing_service_database_url: >-
{{ lookup('ansible.builtin.env', 'DATABASE_URL')
| default('', true) }}
billing_service_listen_addr: >-
{{ lookup('ansible.builtin.env', 'BILLING_SERVICE_LISTEN_ADDR')
| default('127.0.0.1:8081', true) }}
billing_service_collect_interval: >-
{{ lookup('ansible.builtin.env', 'COLLECT_INTERVAL')
| default('1m', true) }}
billing_service_default_region: >-
{{ lookup('ansible.builtin.env', 'DEFAULT_REGION')
| default('', true) }}
billing_service_source_revision: >-
{{ lookup('ansible.builtin.env', 'SOURCE_REVISION')
| default('billing-service-v1', true) }}
billing_service_price_per_byte: >-
{{ lookup('ansible.builtin.env', 'PRICE_PER_BYTE')
| default('0', true) }}
billing_service_initial_included_quota_bytes: >-
{{ lookup('ansible.builtin.env', 'INITIAL_INCLUDED_QUOTA_BYTES')
| default('0', true) }}
billing_service_initial_balance: >-
{{ lookup('ansible.builtin.env', 'INITIAL_BALANCE')
| default('0', true) }}
pre_tasks:
- name: Validate DATABASE_URL is present
ansible.builtin.assert:
that:
- billing_service_database_url | length > 0
fail_msg: "DATABASE_URL must be exported before running this playbook."
success_msg: "DATABASE_URL found"
roles:
- roles/vhosts/billing-service

View File

@ -1,6 +0,0 @@
---
- import_playbook: deploy_acp_vhosts.yml
vars:
deploy_acp_codex: true
deploy_acp_opencode: false
deploy_acp_unified: false

View File

@ -0,0 +1,7 @@
- name: Deploy managed console.svc.plus service
hosts: "{{ console_service_hosts | default('console') }}"
gather_facts: true
become: true
roles:
- roles/vhosts/docker
- roles/vhosts/console_service

41
deploy_xray_exporter.yml Normal file
View File

@ -0,0 +1,41 @@
- name: Deploy xray-exporter service
hosts: "{{ xray_exporter_hosts | default('xray_exporter') }}"
gather_facts: true
become: true
vars:
xray_exporter_source_dir: >-
{{ lookup('ansible.builtin.env', 'XRAY_EXPORTER_SOURCE_DIR')
| default(playbook_dir ~ '/../xray-exporter', true) }}
xray_exporter_node_id: >-
{{ lookup('ansible.builtin.env', 'EXPORTER_NODE_ID')
| default('node-xhttp.svc.plus', true) }}
xray_exporter_env_name: >-
{{ lookup('ansible.builtin.env', 'EXPORTER_ENV')
| default('prod', true) }}
xray_exporter_stats_url: >-
{{ lookup('ansible.builtin.env', 'XRAY_STATS_URL')
| default('http://127.0.0.1:49227/debug/vars', true) }}
xray_exporter_stats_token: >-
{{ lookup('ansible.builtin.env', 'XRAY_STATS_TOKEN')
| default('', true) }}
xray_exporter_accounts_base_url: >-
{{ lookup('ansible.builtin.env', 'ACCOUNTS_BASE_URL')
| default('https://accounts.svc.plus', true) }}
xray_exporter_internal_service_token: >-
{{ lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN')
| default('', true) }}
xray_exporter_listen_addr: >-
{{ lookup('ansible.builtin.env', 'XRAY_EXPORTER_LISTEN_ADDR')
| default('127.0.0.1:8080', true) }}
xray_exporter_scrape_interval: >-
{{ lookup('ansible.builtin.env', 'SCRAPE_INTERVAL')
| default('1m', true) }}
pre_tasks:
- name: Validate INTERNAL_SERVICE_TOKEN is present
ansible.builtin.assert:
that:
- xray_exporter_internal_service_token | length > 0
fail_msg: "INTERNAL_SERVICE_TOKEN must be exported before running this playbook."
success_msg: "INTERNAL_SERVICE_TOKEN found"
roles:
- roles/vhosts/xray-exporter

View File

@ -0,0 +1,8 @@
---
- name: Deploy ACP vhosts through xworkmate bridge
hosts: "{{ xworkmate_bridge_hosts | default('xworkmate-bridge.svc.plus') }}"
become: true
gather_facts: true
roles:
- role: roles/vhosts/deploy_acp_vhosts/
tags: [deploy_acp_vhosts]

View File

@ -4,14 +4,25 @@ cn-front.svc.plus ansible_host=47.120.61.35 ansible_user=roo
[agent_proxy]
jp-xhttp-contabo.svc.plus ansible_host=46.250.251.132 ansible_user=root
[agent_svc_plus]
jp-xhttp-contabo.svc.plus ansible_host=46.250.251.132 ansible_user=root
[xray_exporter]
jp-xhttp-contabo.svc.plus ansible_host=46.250.251.132 ansible_user=root
[billing_service]
jp-xhttp-contabo.svc.plus ansible_host=46.250.251.132 ansible_user=root
[accounts]
acp-server.svc.plus ansible_host=46.250.251.132 ansible_user=root
xworkmate-bridge.svc.plus ansible_host=46.250.251.132 ansible_user=root
[apisix]
api.svc.plus ansible_host=46.250.251.132 ansible_user=root
[postgresql]
acp-server.svc.plus ansible_host=46.250.251.132 ansible_user=root
xworkmate-bridge.svc.plus ansible_host=46.250.251.132 ansible_user=root
[k3s]
jp-k3s-vultr.svc.plus ansible_host=167.179.110.129 ansible_user=root

View File

@ -1,19 +1,16 @@
# Zitadel Docker role
This role provisions a Zitadel stack with Postgres, optional TLS termination, login frontend, Nginx proxy, and Certbot assets. Templates from `templates/` and static assets from `files/` are rendered into `{{ zitadel_workspace }}` and the Docker Compose stack is started.
This role provisions a Zitadel stack with Postgres and the login frontend, then exposes both services on localhost-only ports so the host Caddy instance can terminate TLS and reverse proxy traffic for `{{ zitadel_domain }}`.
The previous embedded `nginx/certbot` deployment mode now lives in the separate legacy role `docker/zitadel_legacy`.
## Layout
```
files/
├── certbot/
│ ├── conf/
│ └── www/
├── docker-compose.yaml
├── nginx/
│ ├── conf.d/
│ │ └── default.conf
│ └── nginx.conf
└── run.sh
templates/
├── docker-compose.yaml
└── zitadel-site.caddy.j2
```
## Defaults
@ -21,6 +18,12 @@ files/
- `zitadel_workspace`: `{{ zitadel_deploy_dir }}`
- `zitadel_domain`: `auth.svc.plus`
- `zitadel_masterkey`: `MasterkeyNeedsToHave32Characters`
- `zitadel_api_bind_host`: `127.0.0.1`
- `zitadel_api_port`: `19080`
- `zitadel_login_bind_host`: `127.0.0.1`
- `zitadel_login_port`: `19081`
- `zitadel_caddy_conf_dir`: `/etc/caddy/conf.d`
- `zitadel_caddy_fragment_path`: `/etc/caddy/conf.d/zitadel.caddy`
## RUN

View File

@ -4,3 +4,10 @@ zitadel_deploy_dir: /opt/zitadel
zitadel_workspace: "{{ zitadel_deploy_dir }}"
zitadel_domain: auth.svc.plus
zitadel_masterkey: MasterkeyNeedsToHave32Characters
zitadel_api_bind_host: 127.0.0.1
zitadel_api_port: 19080
zitadel_login_bind_host: 127.0.0.1
zitadel_login_port: 19081
zitadel_caddyfile_path: /etc/caddy/Caddyfile
zitadel_caddy_conf_dir: /etc/caddy/conf.d
zitadel_caddy_fragment_path: /etc/caddy/conf.d/zitadel.caddy

View File

@ -3,4 +3,4 @@ set -euo pipefail
# Helper script to start the Zitadel docker compose stack
cd "$(dirname "$0")"
docker-compose -f docker-compose.yaml up -d
docker compose -f docker-compose.yaml up -d --remove-orphans

View File

@ -0,0 +1,5 @@
---
- name: Reload caddy
ansible.builtin.service:
name: caddy
state: reloaded

View File

@ -7,11 +7,6 @@
mode: "0755"
loop:
- "{{ zitadel_workspace }}"
- "{{ zitadel_workspace }}/certbot"
- "{{ zitadel_workspace }}/certbot/conf"
- "{{ zitadel_workspace }}/certbot/www"
- "{{ zitadel_workspace }}/nginx"
- "{{ zitadel_workspace }}/nginx/conf.d"
- name: Ensure Zitadel workspace ownership
become: true
@ -31,8 +26,6 @@
mode: "{{ item.mode | default('0644') }}"
loop:
- { src: 'docker-compose.yaml', dest: 'docker-compose.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' }
- name: Copy Zitadel static files
become: true
@ -42,25 +35,42 @@
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)
- name: Check caddy CLI is present on the target node
become: true
command: docker compose --profile bootstrap -f {{ zitadel_workspace }}/docker-compose.yaml up -d bootstrap-nginx
args:
chdir: "{{ zitadel_workspace }}"
ansible.builtin.command: caddy version
changed_when: false
- name: Run certbot initial ACME challenge
- name: Ensure Caddy fragment directory exists for Zitadel
become: true
command: docker compose --profile bootstrap -f {{ zitadel_workspace }}/docker-compose.yaml run --rm certbot
args:
chdir: "{{ zitadel_workspace }}"
ansible.builtin.file:
path: "{{ zitadel_caddy_conf_dir }}"
state: directory
owner: root
group: root
mode: "0755"
- name: Destroy Bootstrap NGINX (80-only for ACME)
- name: Deploy Zitadel Caddy site fragment
become: true
command: docker compose --profile bootstrap -f {{ zitadel_workspace }}/docker-compose.yaml down bootstrap-nginx
args:
chdir: "{{ zitadel_workspace }}"
ansible.builtin.template:
src: zitadel-site.caddy.j2
dest: "{{ zitadel_caddy_fragment_path }}"
owner: root
group: root
mode: "0644"
notify: Reload caddy
- name: Ensure Caddy is enabled and running for Zitadel
become: true
ansible.builtin.systemd:
name: caddy
enabled: true
state: started
- name: Validate Caddy configuration for Zitadel
become: true
ansible.builtin.command: caddy validate --config "{{ zitadel_caddyfile_path }}"
changed_when: false
# -------------------------------------------------------------------
# 1. 判断 Zitadel 是否已经初始化
@ -100,6 +110,6 @@
# -------------------------------------------------------------------
- name: Bring up Zitadel stack
become: true
command: docker compose -f {{ zitadel_workspace }}/docker-compose.yaml up -d
command: docker compose -f {{ zitadel_workspace }}/docker-compose.yaml up -d --remove-orphans
args:
chdir: "{{ zitadel_workspace }}"

View File

@ -1,6 +1,5 @@
services:
zitadel-external-tls:
zitadel:
extends:
service: zitadel-init
command: 'start-from-setup --masterkey "{{ zitadel_masterkey }}"'
@ -11,33 +10,14 @@ services:
networks:
- app
- db
ports:
- "{{ zitadel_api_bind_host }}:{{ zitadel_api_port }}:8080"
depends_on:
db:
condition: 'service_healthy'
zitadel-init:
condition: 'service_completed_successfully'
zitadel-enabled-tls:
extends:
service: zitadel-init
command: 'start-from-setup --masterkey "{{ zitadel_masterkey }}"'
environment:
ZITADEL_EXTERNALPORT: 443
ZITADEL_EXTERNALSECURE: true
ZITADEL_TLS_ENABLED: true
ZITADEL_TLS_CERTPATH: /etc/letsencrypt/live/{{ zitadel_domain }}/fullchain.pem
ZITADEL_TLS_KEYPATH: /etc/letsencrypt/live/{{ zitadel_domain }}/privkey.pem
volumes:
- "{{ zitadel_workspace }}/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'
@ -91,10 +71,11 @@ services:
- 'data:/var/lib/postgresql/data:rw'
login-external-tls:
container_name: login-external-tls
restart: 'unless-stopped'
image: 'ghcr.io/zitadel/zitadel-login:latest'
environment:
- ZITADEL_API_URL=http://zitadel-external-tls:8080
- ZITADEL_API_URL=http://zitadel:8080
- NEXT_PUBLIC_BASE_PATH=/ui/v2/login
- ZITADEL_SERVICE_USER_TOKEN_FILE=/current-dir/login-client.pat
- CUSTOM_REQUEST_HEADERS=Host:{{ zitadel_domain }}
@ -102,82 +83,11 @@ services:
- "{{ zitadel_workspace }}:/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:{{ zitadel_domain }}
- NODE_TLS_REJECT_UNAUTHORIZED=0
volumes:
- "{{ zitadel_workspace }}:/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:
- "{{ zitadel_workspace }}/nginx/nginx.conf:/etc/nginx/nginx.conf"
- "{{ zitadel_workspace }}/nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf:ro"
- "{{ zitadel_workspace }}/certbot/conf:/etc/letsencrypt"
- "{{ zitadel_workspace }}/certbot/www:/var/www/certbot"
ports:
- "80:80"
- "443:443"
networks:
- app
- "{{ zitadel_login_bind_host }}:{{ zitadel_login_port }}:3000"
depends_on:
zitadel-external-tls:
condition: service_healthy
bootstrap-nginx:
profiles: ["bootstrap"]
image: nginx:mainline-alpine
container_name: bootstrap-nginx
volumes:
- "{{ zitadel_workspace }}/certbot/www:/var/www/certbot"
- "{{ zitadel_workspace }}/certbot/conf:/etc/letsencrypt"
- "{{ zitadel_workspace }}/nginx/nginx.conf:/etc/nginx/nginx.conf"
- "{{ zitadel_workspace }}/nginx/conf.d/bootstrap-nginx.conf:/etc/nginx/conf.d/bootstrap-nginx.conf"
ports:
- "80: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 manbuzhe2009@qq.com
--agree-tos
--no-eff-email
--keep-until-expiring
--non-interactive
-d {{ zitadel_domain }}
volumes:
- "{{ zitadel_workspace }}/certbot/conf:/etc/letsencrypt"
- "{{ zitadel_workspace }}/certbot/www:/var/www/certbot"
networks:
- app
zitadel:
condition: 'service_healthy'
networks:
app:

View File

@ -0,0 +1,24 @@
{{ zitadel_domain }} {
encode zstd gzip
@login {
path /ui/v2/login*
}
handle @login {
reverse_proxy {{ zitadel_login_bind_host }}:{{ zitadel_login_port }} {
header_up Host {host}
header_up X-Forwarded-Proto https
}
}
handle {
reverse_proxy {{ zitadel_api_bind_host }}:{{ zitadel_api_port }} {
transport http {
versions h2c 2
}
header_up Host {host}
header_up X-Forwarded-Proto https
}
}
}

View File

@ -0,0 +1,30 @@
# Zitadel Docker legacy role
Legacy role that preserves the original bundled `nginx/certbot` deployment mode.
This role provisions a Zitadel stack with Postgres, optional TLS termination, login frontend, Nginx proxy, and Certbot assets. Templates from `templates/` and static assets from `files/` are rendered into `{{ zitadel_workspace }}` and the Docker Compose stack is started.
## Layout
```
files/
├── certbot/
│ ├── conf/
│ └── www/
├── docker-compose.yaml
├── nginx/
│ ├── conf.d/
│ │ └── default.conf
│ └── nginx.conf
└── run.sh
```
## Defaults
- `zitadel_deploy_dir`: `/opt/zitadel`
- `zitadel_workspace`: `{{ zitadel_deploy_dir }}`
- `zitadel_domain`: `auth.svc.plus`
- `zitadel_masterkey`: `MasterkeyNeedsToHave32Characters`
## RUN
ansible-playbook -i inventory.ini deploy_zitadel_docker.yaml -e "domain=auth.svc.plus" -D -C -l auth.svc.plus
ansible-playbook -i inventory.ini deploy_zitadel_docker.yaml -e "domain=auth.svc.plus" -D -l auth.svc.plus

View File

@ -0,0 +1,6 @@
---
# Default deployment directory for Zitadel Docker stack
zitadel_deploy_dir: /opt/zitadel
zitadel_workspace: "{{ zitadel_deploy_dir }}"
zitadel_domain: auth.svc.plus
zitadel_masterkey: MasterkeyNeedsToHave32Characters

View File

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

View File

@ -0,0 +1,105 @@
---
- name: Ensure Zitadel directories exist
become: true
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- "{{ zitadel_workspace }}"
- "{{ zitadel_workspace }}/certbot"
- "{{ zitadel_workspace }}/certbot/conf"
- "{{ zitadel_workspace }}/certbot/www"
- "{{ zitadel_workspace }}/nginx"
- "{{ zitadel_workspace }}/nginx/conf.d"
- name: Ensure Zitadel workspace ownership
become: true
ansible.builtin.file:
path: "{{ zitadel_workspace }}"
state: directory
recurse: true
owner: "1000"
group: "1000"
mode: "0755"
- name: Template Zitadel configuration files
become: true
ansible.builtin.template:
src: "{{ item.src }}"
dest: "{{ zitadel_workspace }}/{{ item.dest }}"
mode: "{{ item.mode | default('0644') }}"
loop:
- { src: 'docker-compose.yaml', dest: 'docker-compose.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' }
- name: Copy Zitadel static files
become: true
ansible.builtin.copy:
src: "{{ item.src }}"
dest: "{{ zitadel_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 {{ zitadel_workspace }}/docker-compose.yaml up -d bootstrap-nginx
args:
chdir: "{{ zitadel_workspace }}"
- name: Run certbot initial ACME challenge
become: true
command: docker compose --profile bootstrap -f {{ zitadel_workspace }}/docker-compose.yaml run --rm certbot
args:
chdir: "{{ zitadel_workspace }}"
- name: Destroy Bootstrap NGINX (80-only for ACME)
become: true
command: docker compose --profile bootstrap -f {{ zitadel_workspace }}/docker-compose.yaml down bootstrap-nginx
args:
chdir: "{{ zitadel_workspace }}"
# -------------------------------------------------------------------
# 1. 判断 Zitadel 是否已经初始化
# (是否已经生成 login-client.pat 或其它初始化标记)
# -------------------------------------------------------------------
- name: Check if Zitadel initialized
stat:
path: "{{ zitadel_workspace }}/login-client.pat"
register: zitadel_initialized
# -------------------------------------------------------------------
# 2. 如果未初始化,先清理所有可能失败的残留容器、状态
# -------------------------------------------------------------------
- name: Zitadel containers and Zitadel postgres volume (cleanup)
become: true
shell: |
docker compose -f {{ zitadel_workspace }}/docker-compose.yaml down || true
docker volume rm zitadel_data || true
args:
chdir: "{{ zitadel_workspace }}"
when: not zitadel_initialized.stat.exists
# -------------------------------------------------------------------
# 3. 执行第一次初始化init + setup
# -------------------------------------------------------------------
- name: Run Zitadel init (one-time)
become: true
shell: |
docker compose -f {{ zitadel_workspace }}/docker-compose.yaml run --rm zitadel-init || true
args:
chdir: "{{ zitadel_workspace }}"
when: not zitadel_initialized.stat.exists
# -------------------------------------------------------------------
# 4. 启动正式 Zitadel stackstart-only
# -------------------------------------------------------------------
- name: Bring up Zitadel stack
become: true
command: docker compose -f {{ zitadel_workspace }}/docker-compose.yaml up -d
args:
chdir: "{{ zitadel_workspace }}"

View File

@ -0,0 +1,187 @@
services:
zitadel-external-tls:
extends:
service: zitadel-init
command: 'start-from-setup --masterkey "{{ zitadel_masterkey }}"'
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 "{{ zitadel_masterkey }}"'
environment:
ZITADEL_EXTERNALPORT: 443
ZITADEL_EXTERNALSECURE: true
ZITADEL_TLS_ENABLED: true
ZITADEL_TLS_CERTPATH: /etc/letsencrypt/live/{{ zitadel_domain }}/fullchain.pem
ZITADEL_TLS_KEYPATH: /etc/letsencrypt/live/{{ zitadel_domain }}/privkey.pem
volumes:
- "{{ zitadel_workspace }}/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: {{ zitadel_domain }}
# 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:
- "{{ zitadel_workspace }}:/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:{{ zitadel_domain }}
volumes:
- "{{ zitadel_workspace }}:/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:{{ zitadel_domain }}
- NODE_TLS_REJECT_UNAUTHORIZED=0
volumes:
- "{{ zitadel_workspace }}:/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:
- "{{ zitadel_workspace }}/nginx/nginx.conf:/etc/nginx/nginx.conf"
- "{{ zitadel_workspace }}/nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf:ro"
- "{{ zitadel_workspace }}/certbot/conf:/etc/letsencrypt"
- "{{ zitadel_workspace }}/certbot/www:/var/www/certbot"
ports:
- "80:80"
- "443:443"
networks:
- app
depends_on:
zitadel-external-tls:
condition: service_healthy
bootstrap-nginx:
profiles: ["bootstrap"]
image: nginx:mainline-alpine
container_name: bootstrap-nginx
volumes:
- "{{ zitadel_workspace }}/certbot/www:/var/www/certbot"
- "{{ zitadel_workspace }}/certbot/conf:/etc/letsencrypt"
- "{{ zitadel_workspace }}/nginx/nginx.conf:/etc/nginx/nginx.conf"
- "{{ zitadel_workspace }}/nginx/conf.d/bootstrap-nginx.conf:/etc/nginx/conf.d/bootstrap-nginx.conf"
ports:
- "80: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 manbuzhe2009@qq.com
--agree-tos
--no-eff-email
--keep-until-expiring
--non-interactive
-d {{ zitadel_domain }}
volumes:
- "{{ zitadel_workspace }}/certbot/conf:/etc/letsencrypt"
- "{{ zitadel_workspace }}/certbot/www:/var/www/certbot"
networks:
- app
networks:
app:
db:
volumes:
data:

View File

@ -24,6 +24,7 @@ acp_codex_bridge_allowed_origins:
acp_codex_public_base_url: https://acp-server.svc.plus/codex
acp_codex_caddyfile_path: /etc/caddy/Caddyfile
acp_codex_caddy_conf_dir: /etc/caddy/conf.d
acp_codex_manage_caddy: true
acp_codex_obsolete_caddy_fragment_paths:
- /etc/caddy/conf.d/acp-server-codex.caddy
acp_codex_enable_ufw: true

View File

@ -60,6 +60,8 @@
group: root
mode: "0644"
notify: Reload caddy
when:
- acp_codex_manage_caddy | bool
- name: Deploy Codex ACP systemd service
ansible.builtin.template:
@ -88,6 +90,8 @@
name: caddy
enabled: true
state: started
when:
- acp_codex_manage_caddy | bool
- name: Ensure Codex ACP service is enabled and running
ansible.builtin.systemd:

View File

@ -0,0 +1,26 @@
---
acp_gemini_service_name: acp-gemini-adapter
acp_gemini_service_user: root
acp_gemini_service_group: root
acp_gemini_home: /root
acp_gemini_workdir: /root
acp_gemini_binary_path: /opt/homebrew/bin/gemini
acp_gemini_args: --experimental-acp
acp_gemini_bridge_binary_path: /usr/local/bin/xworkmate-go-core
acp_gemini_bridge_local_source_dir: "{{ playbook_dir }}/../xworkmate-bridge"
acp_gemini_bridge_local_build_dir: "{{ playbook_dir }}/.artifacts/acp_gemini"
acp_gemini_bridge_local_binary_path: "{{ acp_gemini_bridge_local_build_dir }}/xworkmate-go-core"
acp_gemini_bridge_build_goos: linux
acp_gemini_bridge_build_goarch: amd64
acp_gemini_listen_host: 127.0.0.1
acp_gemini_listen_port: 8791
acp_gemini_allowed_origins:
- https://xworkmate.svc.plus
- http://localhost:*
- http://127.0.0.1:*
acp_gemini_public_base_url: https://acp-server.svc.plus/gemini
acp_gemini_manage_caddy: false
acp_gemini_environment: {}
acp_gemini_packages:
- caddy

View File

@ -0,0 +1,6 @@
---
- name: Restart gemini acp adapter
ansible.builtin.systemd:
name: "{{ acp_gemini_service_name }}"
state: restarted
daemon_reload: true

View File

@ -0,0 +1,48 @@
---
- name: Ensure local Gemini ACP build directory exists
ansible.builtin.file:
path: "{{ acp_gemini_bridge_local_build_dir }}"
state: directory
mode: "0755"
delegate_to: localhost
become: false
- name: Build XWorkmate Go ACP adapter locally for Gemini
ansible.builtin.command:
cmd: go build -o "{{ acp_gemini_bridge_local_binary_path }}" .
chdir: "{{ acp_gemini_bridge_local_source_dir }}"
environment:
GOOS: "{{ acp_gemini_bridge_build_goos }}"
GOARCH: "{{ acp_gemini_bridge_build_goarch }}"
CGO_ENABLED: "0"
GO111MODULE: "on"
delegate_to: localhost
become: false
- name: Upload XWorkmate Go ACP adapter binary for Gemini
ansible.builtin.copy:
src: "{{ acp_gemini_bridge_local_binary_path }}"
dest: "{{ acp_gemini_bridge_binary_path }}"
owner: root
group: root
mode: "0755"
notify: Restart gemini acp adapter
- name: Deploy Gemini ACP adapter service
ansible.builtin.template:
src: gemini-acp-adapter.service.j2
dest: "/etc/systemd/system/{{ acp_gemini_service_name }}.service"
owner: root
group: root
mode: "0644"
notify: Restart gemini acp adapter
- name: Reload systemd manager configuration for Gemini ACP
ansible.builtin.systemd:
daemon_reload: true
- name: Ensure Gemini ACP adapter service is enabled and running
ansible.builtin.systemd:
name: "{{ acp_gemini_service_name }}"
enabled: true
state: started

View File

@ -0,0 +1,10 @@
---
- name: Install Gemini ACP packages
ansible.builtin.apt:
name: "{{ acp_gemini_packages }}"
state: present
update_cache: true
environment:
DEBIAN_FRONTEND: noninteractive
APT_LISTCHANGES_FRONTEND: none
become: true

View File

@ -0,0 +1,16 @@
---
- name: Install Gemini ACP prerequisites
ansible.builtin.import_tasks: install.yml
tags: [acp_gemini, acp_gemini_install]
- name: Configure Gemini ACP adapter
ansible.builtin.import_tasks: config.yml
tags: [acp_gemini, acp_gemini_config]
- name: Flush Gemini ACP handlers before validation
ansible.builtin.meta: flush_handlers
tags: [acp_gemini, acp_gemini_config, acp_gemini_validate]
- name: Validate Gemini ACP readiness
ansible.builtin.import_tasks: validate.yml
tags: [acp_gemini, acp_gemini_validate]

View File

@ -0,0 +1,36 @@
---
- name: Check Gemini ACP adapter listener
ansible.builtin.command: ss -ltnp
register: acp_gemini_ss
changed_when: false
- name: Validate local Gemini ACP adapter HTTP endpoint
ansible.builtin.uri:
url: "http://{{ acp_gemini_listen_host }}:{{ acp_gemini_listen_port }}/acp/rpc"
method: POST
body_format: json
body:
jsonrpc: "2.0"
id: 1
method: acp.capabilities
params: {}
return_content: true
status_code: 200
register: acp_gemini_bridge_http
- name: Show Gemini ACP adapter status
ansible.builtin.command: systemctl status "{{ acp_gemini_service_name }}" --no-pager
register: acp_gemini_status
changed_when: false
failed_when: false
- name: Show Gemini ACP validation summary
ansible.builtin.debug:
msg:
- "Gemini public base URL: {{ acp_gemini_public_base_url }}"
- "Preferred WebSocket endpoint: {{ acp_gemini_public_base_url }}/acp"
- "Compatibility HTTP RPC endpoint: {{ acp_gemini_public_base_url }}/acp/rpc"
- "Gemini adapter listener: {{ acp_gemini_listen_host }}:{{ acp_gemini_listen_port }}"
- "Service: {{ acp_gemini_status.stdout | default('N/A') }}"
- "Socket: {{ acp_gemini_ss.stdout | default('N/A') }}"
- "Bridge capabilities HTTP: {{ acp_gemini_bridge_http.content | default('N/A') }}"

View File

@ -0,0 +1,25 @@
[Unit]
Description=XWorkmate Gemini ACP adapter
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User={{ acp_gemini_service_user }}
Group={{ acp_gemini_service_group }}
WorkingDirectory={{ acp_gemini_workdir }}
Environment=HOME={{ acp_gemini_home }}
Environment=TERM=xterm-256color
Environment=GEMINI_ADAPTER_LISTEN_ADDR={{ acp_gemini_listen_host }}:{{ acp_gemini_listen_port }}
Environment=GEMINI_ADAPTER_BIN={{ acp_gemini_binary_path }}
Environment=GEMINI_ADAPTER_ARGS={{ acp_gemini_args }}
Environment=GEMINI_ADAPTER_ALLOWED_ORIGINS={{ acp_gemini_allowed_origins | join(',') }}
{% for key, value in acp_gemini_environment | dictsort %}
Environment={{ key }}={{ value }}
{% endfor %}
ExecStart={{ acp_gemini_bridge_binary_path }} gemini-acp-adapter --listen {{ acp_gemini_listen_host }}:{{ acp_gemini_listen_port }} --gemini-bin {{ acp_gemini_binary_path }} --gemini-args "{{ acp_gemini_args }}"
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target

View File

@ -26,6 +26,7 @@ acp_opencode_expected_content_type: text/html
acp_opencode_expected_body_marker: opencode-theme-id
acp_opencode_caddyfile_path: /etc/caddy/Caddyfile
acp_opencode_caddy_conf_dir: /etc/caddy/conf.d
acp_opencode_manage_caddy: true
acp_opencode_obsolete_caddy_fragment_paths:
- /etc/caddy/conf.d/acp-server-opencode.caddy
acp_opencode_enable_ufw: true

View File

@ -47,6 +47,8 @@
group: root
mode: "0644"
notify: Reload caddy
when:
- acp_opencode_manage_caddy | bool
- name: Remove deprecated standalone OpenCode Caddy fragments
ansible.builtin.file:
@ -82,6 +84,8 @@
name: caddy
enabled: true
state: started
when:
- acp_opencode_manage_caddy | bool
- name: Ensure OpenCode ACP service is enabled and running
ansible.builtin.systemd:

View File

@ -0,0 +1,47 @@
---
# Agent-svc-plus deployment defaults
agent_svc_plus_repo_url: "https://github.com/x-evor/agent.svc.plus.git"
agent_svc_plus_repo_version: "main"
agent_svc_plus_app_dir: "/opt/agent.svc.plus"
agent_svc_plus_go_version: "1.25.1"
agent_svc_plus_go_root: "/usr/local/go"
agent_svc_plus_go_bin: "{{ agent_svc_plus_go_root }}/bin/go"
agent_svc_plus_binary_name: "agent-svc-plus"
agent_svc_plus_binary_path: "/usr/local/bin/{{ agent_svc_plus_binary_name }}"
agent_svc_plus_build_target: "./cmd/agent"
agent_svc_plus_service_name: "agent-svc-plus"
agent_svc_plus_service_description: "agent.svc.plus service"
agent_svc_plus_user: "root"
agent_svc_plus_group: "root"
agent_svc_plus_config_dir: "/etc/agent"
agent_svc_plus_config_file: "account-agent.yaml"
agent_svc_plus_config_path: "{{ agent_svc_plus_config_dir }}/{{ agent_svc_plus_config_file }}"
agent_svc_plus_data_dir: "/var/lib/agent-svc-plus"
agent_id: "node-xhttp.svc.plus"
agent_controller_url: "https://accounts.svc.plus"
agent_api_token: ""
agent_http_timeout: "15s"
agent_status_interval: "1m"
agent_sync_interval: "5m"
agent_tls_insecure_skip_verify: false
agent_billing_enabled: true
agent_billing_base_url: "http://127.0.0.1:8081"
agent_billing_http_timeout: "15s"
agent_billing_collect_interval: "1m"
agent_billing_reconcile_interval: "5m"
xray_enabled: true
xray_sync_interval: "5m"
xray_uuid: "00000000-0000-0000-0000-000000000000"
xray_config_path: "/usr/local/etc/xray/config.json"
xray_tcp_config_path: "/usr/local/etc/xray/tcp-config.json"
xray_template_dir: "/usr/local/etc/xray/templates"
agent_log_level: "info"

View File

@ -0,0 +1,6 @@
---
- name: Restart agent-svc-plus
ansible.builtin.systemd:
name: "{{ agent_svc_plus_service_name }}"
state: restarted
daemon_reload: true

View File

@ -0,0 +1,123 @@
---
- name: Map Go architecture
ansible.builtin.set_fact:
agent_svc_plus_go_arch: >-
{{ 'arm64' if ansible_architecture in ['aarch64', 'arm64'] else 'amd64' }}
- name: Ensure agent build prerequisites are installed
ansible.builtin.apt:
name:
- git
- curl
- ca-certificates
- tar
state: present
update_cache: true
- name: Check installed Go version
ansible.builtin.command: "{{ agent_svc_plus_go_bin }} version"
register: agent_svc_plus_go_version_check
changed_when: false
failed_when: false
- name: Download Go toolchain archive
ansible.builtin.get_url:
url: "https://go.dev/dl/go{{ agent_svc_plus_go_version }}.linux-{{ agent_svc_plus_go_arch }}.tar.gz"
dest: "/tmp/go{{ agent_svc_plus_go_version }}.linux-{{ agent_svc_plus_go_arch }}.tar.gz"
mode: "0644"
when: agent_svc_plus_go_version_check.rc != 0 or ('go' ~ agent_svc_plus_go_version) not in agent_svc_plus_go_version_check.stdout
- name: Remove previous Go installation
ansible.builtin.file:
path: "{{ agent_svc_plus_go_root }}"
state: absent
when: agent_svc_plus_go_version_check.rc != 0 or ('go' ~ agent_svc_plus_go_version) not in agent_svc_plus_go_version_check.stdout
- name: Install Go toolchain
ansible.builtin.unarchive:
src: "/tmp/go{{ agent_svc_plus_go_version }}.linux-{{ agent_svc_plus_go_arch }}.tar.gz"
dest: "/usr/local"
remote_src: true
when: agent_svc_plus_go_version_check.rc != 0 or ('go' ~ agent_svc_plus_go_version) not in agent_svc_plus_go_version_check.stdout
- name: Create agent directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ agent_svc_plus_user }}"
group: "{{ agent_svc_plus_group }}"
mode: "0755"
loop:
- "{{ agent_svc_plus_app_dir }}"
- "{{ agent_svc_plus_config_dir }}"
- "{{ agent_svc_plus_data_dir }}"
- "{{ xray_template_dir }}"
- name: Clone agent.svc.plus repository
ansible.builtin.git:
repo: "{{ agent_svc_plus_repo_url }}"
dest: "{{ agent_svc_plus_app_dir }}"
version: "{{ agent_svc_plus_repo_version }}"
update: true
notify: Restart agent-svc-plus
- name: Build agent.svc.plus binary
ansible.builtin.command: >-
{{ agent_svc_plus_go_bin }} build -o {{ agent_svc_plus_binary_path }} {{ agent_svc_plus_build_target }}
args:
chdir: "{{ agent_svc_plus_app_dir }}"
environment:
GOROOT: "{{ agent_svc_plus_go_root }}"
PATH: "{{ agent_svc_plus_go_root }}/bin:{{ ansible_env.PATH }}"
GOTOOLCHAIN: local
notify: Restart agent-svc-plus
- name: Ensure agent binary permissions
ansible.builtin.file:
path: "{{ agent_svc_plus_binary_path }}"
owner: "{{ agent_svc_plus_user }}"
group: "{{ agent_svc_plus_group }}"
mode: "0755"
- name: Deploy agent configuration
ansible.builtin.template:
src: agent-config.yaml.j2
dest: "{{ agent_svc_plus_config_path }}"
owner: "{{ agent_svc_plus_user }}"
group: "{{ agent_svc_plus_group }}"
mode: "0600"
notify: Restart agent-svc-plus
- name: Deploy XHTTP template
ansible.builtin.template:
src: xray.xhttp.template.json.j2
dest: "{{ xray_template_dir }}/xray.xhttp.template.json"
owner: "{{ agent_svc_plus_user }}"
group: "{{ agent_svc_plus_group }}"
mode: "0644"
when: xray_enabled | bool
- name: Deploy TCP template
ansible.builtin.template:
src: xray.tcp.template.json.j2
dest: "{{ xray_template_dir }}/xray.tcp.template.json"
owner: "{{ agent_svc_plus_user }}"
group: "{{ agent_svc_plus_group }}"
mode: "0644"
when: xray_enabled | bool
- name: Install agent systemd service
ansible.builtin.template:
src: agent-svc-plus.service.j2
dest: "/etc/systemd/system/{{ agent_svc_plus_service_name }}.service"
owner: root
group: root
mode: "0644"
notify: Restart agent-svc-plus
- name: Enable and start agent-svc-plus
ansible.builtin.systemd:
name: "{{ agent_svc_plus_service_name }}"
enabled: true
state: started
daemon_reload: true

View File

@ -0,0 +1,45 @@
mode: "agent"
log:
level: {{ agent_log_level }}
agent:
id: "{{ agent_id }}"
controllerUrl: "{{ agent_controller_url }}"
apiToken: "{{ agent_api_token }}"
httpTimeout: {{ agent_http_timeout }}
statusInterval: {{ agent_status_interval }}
syncInterval: {{ agent_sync_interval }}
tls:
insecureSkipVerify: {{ agent_tls_insecure_skip_verify | lower }}
billing:
enabled: {{ agent_billing_enabled | lower }}
baseURL: "{{ agent_billing_base_url }}"
httpTimeout: {{ agent_billing_http_timeout }}
collectInterval: {{ agent_billing_collect_interval }}
reconcileInterval: {{ agent_billing_reconcile_interval }}
{% if xray_enabled %}
xray:
sync:
enabled: true
interval: {{ xray_sync_interval }}
targets:
- name: "xhttp"
outputPath: "{{ xray_config_path }}"
templatePath: "{{ xray_template_dir }}/xray.xhttp.template.json"
validateCommand: []
restartCommand:
- "systemctl"
- "restart"
- "xray.service"
- name: "tcp"
outputPath: "{{ xray_tcp_config_path }}"
templatePath: "{{ xray_template_dir }}/xray.tcp.template.json"
validateCommand: []
restartCommand:
- "systemctl"
- "restart"
- "xray-tcp.service"
{% endif %}

View File

@ -0,0 +1,14 @@
[Unit]
Description={{ agent_svc_plus_service_description }}
After=network.target
[Service]
Type=simple
WorkingDirectory={{ agent_svc_plus_data_dir }}
ExecStart={{ agent_svc_plus_binary_path }} -config {{ agent_svc_plus_config_path }}
Restart=always
User={{ agent_svc_plus_user }}
Group={{ agent_svc_plus_group }}
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,84 @@
{
"log": {
"loglevel": "warning"
},
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"type": "field",
"ip": [
"geoip:cn"
],
"outboundTag": "block"
}
]
},
"inbounds": [
{
"listen": "0.0.0.0",
"port": 1443,
"protocol": "vless",
"settings": {
"clients": [
{
"id": "{% raw %}{{ UUID }}{% endraw %}",
"flow": "xtls-rprx-vision"
}
],
"decryption": "none",
"fallbacks": [
{
"dest": "8001",
"xver": 1
},
{
"alpn": "h2",
"dest": "8002",
"xver": 1
}
]
},
"streamSettings": {
"network": "tcp",
"security": "tls",
"tlsSettings": {
"rejectUnknownSni": true,
"minVersion": "1.2",
"certificates": [
{
"ocspStapling": 3600,
"certificateFile": "/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/{{ agent_id }}/{{ agent_id }}.crt",
"keyFile": "/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/{{ agent_id }}/{{ agent_id }}.key"
}
]
}
},
"sniffing": {
"enabled": true,
"destOverride": [
"http",
"tls"
]
}
}
],
"outbounds": [
{
"protocol": "freedom",
"tag": "direct"
},
{
"protocol": "blackhole",
"tag": "block"
}
],
"policy": {
"levels": {
"0": {
"handshake": 2,
"connIdle": 120
}
}
}
}

View File

@ -0,0 +1,50 @@
{
"log": {
"loglevel": "debug"
},
"inbounds": [
{
"listen": "/dev/shm/xray.sock,0666",
"protocol": "vless",
"settings": {
"clients": [
{
"id": "{% raw %}{{ UUID }}{% endraw %}"
}
],
"decryption": "none"
},
"streamSettings": {
"network": "xhttp",
"xhttpSettings": {
"mode": "auto",
"path": "/split"
}
}
}
],
"outbounds": [
{
"tag": "direct",
"protocol": "freedom",
"settings": {}
},
{
"tag": "blocked",
"protocol": "blackhole",
"settings": {}
}
],
"routing": {
"domainStrategy": "AsIs",
"rules": [
{
"type": "field",
"ip": [
"geoip:private"
],
"outboundTag": "blocked"
}
]
}
}

View File

@ -149,6 +149,8 @@
do
grep -q "^${key}=" "{{ apisix_service_env_file }}"
done
args:
executable: /bin/bash
changed_when: false
when: apisix_service_validate_env | bool

View File

@ -0,0 +1,24 @@
---
billing_service_source_dir: "{{ playbook_dir }}/../billing-service"
billing_service_app_dir: "/opt/billing-service"
billing_service_go_version: "1.25.1"
billing_service_go_root: "/usr/local/go"
billing_service_go_bin: "{{ billing_service_go_root }}/bin/go"
billing_service_binary_name: "billing-service"
billing_service_binary_path: "/usr/local/bin/{{ billing_service_binary_name }}"
billing_service_service_name: "billing-service"
billing_service_service_description: "billing-service service"
billing_service_user: "root"
billing_service_group: "root"
billing_service_env_dir: "/etc/default"
billing_service_env_path: "{{ billing_service_env_dir }}/billing-service"
billing_service_listen_addr: "127.0.0.1:8081"
billing_service_exporter_base_url: "http://127.0.0.1:8080"
billing_service_database_url: ""
billing_service_collect_interval: "1m"
billing_service_default_region: ""
billing_service_source_revision: "billing-service-v1"
billing_service_price_per_byte: "0"
billing_service_initial_included_quota_bytes: "0"
billing_service_initial_balance: "0"

View File

@ -0,0 +1,6 @@
---
- name: Restart billing-service
ansible.builtin.systemd:
name: "{{ billing_service_service_name }}"
state: restarted
daemon_reload: true

View File

@ -0,0 +1,102 @@
---
- name: Map Go architecture
ansible.builtin.set_fact:
billing_service_go_arch: >-
{{ 'arm64' if ansible_architecture in ['aarch64', 'arm64'] else 'amd64' }}
- name: Ensure billing-service build prerequisites are installed
ansible.builtin.apt:
name:
- curl
- ca-certificates
- tar
- rsync
state: present
update_cache: true
- name: Check installed Go version
ansible.builtin.command: "{{ billing_service_go_bin }} version"
register: billing_service_go_version_check
changed_when: false
failed_when: false
- name: Download Go toolchain archive
ansible.builtin.get_url:
url: "https://go.dev/dl/go{{ billing_service_go_version }}.linux-{{ billing_service_go_arch }}.tar.gz"
dest: "/tmp/go{{ billing_service_go_version }}.linux-{{ billing_service_go_arch }}.tar.gz"
mode: "0644"
when: billing_service_go_version_check.rc != 0 or ('go' ~ billing_service_go_version) not in billing_service_go_version_check.stdout
- name: Remove previous Go installation
ansible.builtin.file:
path: "{{ billing_service_go_root }}"
state: absent
when: billing_service_go_version_check.rc != 0 or ('go' ~ billing_service_go_version) not in billing_service_go_version_check.stdout
- name: Install Go toolchain
ansible.builtin.unarchive:
src: "/tmp/go{{ billing_service_go_version }}.linux-{{ billing_service_go_arch }}.tar.gz"
dest: "/usr/local"
remote_src: true
when: billing_service_go_version_check.rc != 0 or ('go' ~ billing_service_go_version) not in billing_service_go_version_check.stdout
- name: Create billing-service directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ billing_service_user }}"
group: "{{ billing_service_group }}"
mode: "0755"
loop:
- "{{ billing_service_app_dir }}"
- "{{ billing_service_env_dir }}"
- name: Sync billing-service source tree
ansible.posix.synchronize:
src: "{{ billing_service_source_dir }}/"
dest: "{{ billing_service_app_dir }}/"
delete: true
notify: Restart billing-service
- name: Build billing-service binary
ansible.builtin.command: >-
{{ billing_service_go_bin }} build -buildvcs=false -o {{ billing_service_binary_path }} ./cmd/billing-service
args:
chdir: "{{ billing_service_app_dir }}"
environment:
GOROOT: "{{ billing_service_go_root }}"
PATH: "{{ billing_service_go_root }}/bin:{{ ansible_env.PATH }}"
GOTOOLCHAIN: local
notify: Restart billing-service
- name: Ensure billing-service binary permissions
ansible.builtin.file:
path: "{{ billing_service_binary_path }}"
owner: "{{ billing_service_user }}"
group: "{{ billing_service_group }}"
mode: "0755"
- name: Deploy billing-service environment file
ansible.builtin.template:
src: billing-service.env.j2
dest: "{{ billing_service_env_path }}"
owner: root
group: root
mode: "0600"
notify: Restart billing-service
- name: Install billing-service systemd service
ansible.builtin.template:
src: billing-service.service.j2
dest: "/etc/systemd/system/{{ billing_service_service_name }}.service"
owner: root
group: root
mode: "0644"
notify: Restart billing-service
- name: Enable and start billing-service
ansible.builtin.systemd:
name: "{{ billing_service_service_name }}"
enabled: true
state: started
daemon_reload: true

View File

@ -0,0 +1,9 @@
EXPORTER_BASE_URL={{ billing_service_exporter_base_url }}
DATABASE_URL={{ billing_service_database_url }}
LISTEN_ADDR={{ billing_service_listen_addr }}
COLLECT_INTERVAL={{ billing_service_collect_interval }}
DEFAULT_REGION={{ billing_service_default_region }}
SOURCE_REVISION={{ billing_service_source_revision }}
PRICE_PER_BYTE={{ billing_service_price_per_byte }}
INITIAL_INCLUDED_QUOTA_BYTES={{ billing_service_initial_included_quota_bytes }}
INITIAL_BALANCE={{ billing_service_initial_balance }}

View File

@ -0,0 +1,15 @@
[Unit]
Description={{ billing_service_service_description }}
After=network.target
[Service]
Type=simple
WorkingDirectory={{ billing_service_app_dir }}
EnvironmentFile={{ billing_service_env_path }}
ExecStart={{ billing_service_binary_path }}
Restart=always
User={{ billing_service_user }}
Group={{ billing_service_group }}
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,67 @@
---
console_service_base_dir: "{{ lookup('ansible.builtin.env', 'CONSOLE_BASE_DIR') | default('/opt/console-svc-plus', true) }}"
console_service_compose_file: "{{ console_service_base_dir }}/docker-compose.yml"
console_service_caddyfile: "{{ console_service_base_dir }}/Caddyfile"
console_service_runtime_env_file: "{{ console_service_base_dir }}/.env.runtime"
console_service_project_name: "{{ lookup('ansible.builtin.env', 'CONSOLE_PROJECT_NAME') | default('console-svc-plus', true) }}"
console_service_image_repo: "{{ lookup('ansible.builtin.env', 'CONSOLE_IMAGE_REPO') | default('ghcr.io/x-evor/dashboard', true) }}"
console_service_image_tag: "{{ lookup('ansible.builtin.env', 'CONSOLE_IMAGE_TAG') | default('latest', true) }}"
console_service_frontend_image: "{{ lookup('ansible.builtin.env', 'FRONTEND_IMAGE') | default('', true) }}"
console_service_registry: "{{ lookup('ansible.builtin.env', 'CONSOLE_REGISTRY') | default('ghcr.io', true) }}"
console_service_registry_username: "{{ lookup('ansible.builtin.env', 'GHCR_USERNAME') | default('', true) }}"
console_service_registry_password: "{{ lookup('ansible.builtin.env', 'GHCR_PASSWORD') | default('', true) }}"
console_service_primary_domain: "{{ lookup('ansible.builtin.env', 'PRIMARY_DOMAIN') | default('cn-console.svc.plus', true) }}"
console_service_secondary_domain: "{{ lookup('ansible.builtin.env', 'SECONDARY_DOMAIN') | default('cn-console.onwalk.net', true) }}"
console_service_port: "{{ lookup('ansible.builtin.env', 'PORT') | default('3000', true) }}"
console_service_node_env: production
console_service_runtime_env: "{{ lookup('ansible.builtin.env', 'RUNTIME_ENV') | default('prod', true) }}"
console_service_region: "{{ lookup('ansible.builtin.env', 'REGION') | default('cn', true) }}"
console_service_app_base_url: "{{ lookup('ansible.builtin.env', 'APP_BASE_URL') | default('https://' ~ console_service_primary_domain, true) }}"
console_service_next_public_app_base_url: "{{ lookup('ansible.builtin.env', 'NEXT_PUBLIC_APP_BASE_URL') | default(console_service_app_base_url, true) }}"
console_service_next_public_site_url: "{{ lookup('ansible.builtin.env', 'NEXT_PUBLIC_SITE_URL') | default(console_service_app_base_url, true) }}"
console_service_next_public_login_url: "{{ lookup('ansible.builtin.env', 'NEXT_PUBLIC_LOGIN_URL') | default(console_service_app_base_url ~ '/login', true) }}"
console_service_next_public_docs_base_url: "{{ lookup('ansible.builtin.env', 'NEXT_PUBLIC_DOCS_BASE_URL') | default(console_service_app_base_url ~ '/docs', true) }}"
console_service_session_cookie_secure: "{{ lookup('ansible.builtin.env', 'SESSION_COOKIE_SECURE') | default('true', true) }}"
console_service_next_public_session_cookie_secure: "{{ lookup('ansible.builtin.env', 'NEXT_PUBLIC_SESSION_COOKIE_SECURE') | default('true', true) }}"
console_service_runtime_hostname: "{{ lookup('ansible.builtin.env', 'RUNTIME_HOSTNAME') | default(console_service_primary_domain, true) }}"
console_service_next_runtime_hostname: "{{ lookup('ansible.builtin.env', 'NEXT_RUNTIME_HOSTNAME') | default(console_service_primary_domain, true) }}"
console_service_deployment_hostname: "{{ lookup('ansible.builtin.env', 'DEPLOYMENT_HOSTNAME') | default(console_service_primary_domain, true) }}"
console_service_next_public_runtime_environment: "{{ lookup('ansible.builtin.env', 'NEXT_PUBLIC_RUNTIME_ENVIRONMENT') | default('prod', true) }}"
console_service_next_public_runtime_region: "{{ lookup('ansible.builtin.env', 'NEXT_PUBLIC_RUNTIME_REGION') | default('cn', true) }}"
console_service_account_service_url: "{{ lookup('ansible.builtin.env', 'ACCOUNT_SERVICE_URL') | default('https://accounts.svc.plus', true) }}"
console_service_next_public_account_service_url: "{{ lookup('ansible.builtin.env', 'NEXT_PUBLIC_ACCOUNT_SERVICE_URL') | default(console_service_account_service_url, true) }}"
console_service_server_service_url: "{{ lookup('ansible.builtin.env', 'SERVER_SERVICE_URL') | default('https://api.svc.plus', true) }}"
console_service_next_public_server_service_url: "{{ lookup('ansible.builtin.env', 'NEXT_PUBLIC_SERVER_SERVICE_URL') | default(console_service_server_service_url, true) }}"
console_service_server_service_internal_url: "{{ lookup('ansible.builtin.env', 'SERVER_SERVICE_INTERNAL_URL') | default('', true) }}"
console_service_docs_service_url: "{{ lookup('ansible.builtin.env', 'DOCS_SERVICE_URL') | default('https://docs.svc.plus', true) }}"
console_service_docs_service_internal_url: "{{ lookup('ansible.builtin.env', 'DOCS_SERVICE_INTERNAL_URL') | default('', true) }}"
console_service_openclaw_gateway_remote_url: "{{ lookup('ansible.builtin.env', 'OPENCLAW_GATEWAY_REMOTE_URL') | default('', true) }}"
console_service_openclaw_gateway_token: "{{ lookup('ansible.builtin.env', 'OPENCLAW_GATEWAY_TOKEN') | default('', true) }}"
console_service_vault_server_url: "{{ lookup('ansible.builtin.env', 'VAULT_SERVER_URL') | default('', true) }}"
console_service_vault_namespace: "{{ lookup('ansible.builtin.env', 'VAULT_NAMESPACE') | default('', true) }}"
console_service_vault_token: "{{ lookup('ansible.builtin.env', 'VAULT_TOKEN') | default('', true) }}"
console_service_apisix_ai_gateway_url: "{{ lookup('ansible.builtin.env', 'APISIX_AI_GATEWAY_URL') | default('', true) }}"
console_service_ai_gateway_access_token: "{{ lookup('ansible.builtin.env', 'AI_GATEWAY_ACCESS_TOKEN') | default('', true) }}"
console_service_internal_service_token: "{{ lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN') | default('', true) }}"
console_service_cloudflare_api_token: "{{ lookup('ansible.builtin.env', 'CLOUDFLARE_API_TOKEN') | default('', true) }}"
console_service_cloudflare_account_id: "{{ lookup('ansible.builtin.env', 'CLOUDFLARE_ACCOUNT_ID') | default('', true) }}"
console_service_cloudflare_web_analytics_site_tag: "{{ lookup('ansible.builtin.env', 'CLOUDFLARE_WEB_ANALYTICS_SITE_TAG') | default('', true) }}"
console_service_cloudflare_zone_tag: "{{ lookup('ansible.builtin.env', 'CLOUDFLARE_ZONE_TAG') | default('', true) }}"
console_service_root_email_whitelist: "{{ lookup('ansible.builtin.env', 'ROOT_EMAIL_WHITELIST') | default('admin@svc.plus', true) }}"
console_service_next_public_paypal_client_id: "{{ lookup('ansible.builtin.env', 'NEXT_PUBLIC_PAYPAL_CLIENT_ID') | default('', true) }}"
console_service_next_public_giscus_repo: "{{ lookup('ansible.builtin.env', 'NEXT_PUBLIC_GISCUS_REPO') | default('x-evor/console.svc.plus', true) }}"
console_service_next_public_giscus_repo_id: "{{ lookup('ansible.builtin.env', 'NEXT_PUBLIC_GISCUS_REPO_ID') | default('', true) }}"
console_service_next_public_giscus_category: "{{ lookup('ansible.builtin.env', 'NEXT_PUBLIC_GISCUS_CATEGORY') | default('General', true) }}"
console_service_next_public_giscus_category_id: "{{ lookup('ansible.builtin.env', 'NEXT_PUBLIC_GISCUS_CATEGORY_ID') | default('', true) }}"
console_service_next_public_stripe_price_xstream_paygo: "{{ lookup('ansible.builtin.env', 'NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO') | default('', true) }}"
console_service_next_public_stripe_price_xstream_subscription: "{{ lookup('ansible.builtin.env', 'NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION') | default('', true) }}"
console_service_next_public_stripe_price_xscopehub_paygo: "{{ lookup('ansible.builtin.env', 'NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO') | default('', true) }}"
console_service_next_public_stripe_price_xscopehub_subscription: "{{ lookup('ansible.builtin.env', 'NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION') | default('', true) }}"
console_service_next_public_stripe_price_xcloudflow_paygo: "{{ lookup('ansible.builtin.env', 'NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO') | default('', true) }}"
console_service_next_public_stripe_price_xcloudflow_subscription: "{{ lookup('ansible.builtin.env', 'NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION') | default('', true) }}"
console_service_pull_images: true

View File

@ -0,0 +1,108 @@
---
- name: Ensure console service base directory exists
ansible.builtin.file:
path: "{{ console_service_base_dir }}"
state: directory
owner: root
group: root
mode: "0755"
- name: Assert console deploy uses prebuilt registry image
ansible.builtin.assert:
that:
- console_service_frontend_image | trim | length > 0
- "'/' in (console_service_frontend_image | trim)"
- "':' in (console_service_frontend_image | trim)"
fail_msg: >-
CONSOLE deploy requires a prebuilt image reference via FRONTEND_IMAGE.
This playbook only supports pull-only Docker Compose deployment on the target host
and must never build images remotely.
- name: Log into container registry for console service
ansible.builtin.shell: |
set -euo pipefail
printf '%s' '{{ console_service_registry_password }}' | docker login {{ console_service_registry }} -u '{{ console_service_registry_username }}' --password-stdin
args:
executable: /bin/bash
no_log: true
when:
- console_service_registry_username | length > 0
- console_service_registry_password | length > 0
- name: Render console compose file
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ console_service_compose_file }}"
owner: root
group: root
mode: "0644"
- name: Render console Caddyfile
ansible.builtin.template:
src: Caddyfile.j2
dest: "{{ console_service_caddyfile }}"
owner: root
group: root
mode: "0644"
- name: Render console runtime env file
ansible.builtin.template:
src: env.runtime.j2
dest: "{{ console_service_runtime_env_file }}"
owner: root
group: root
mode: "0600"
no_log: true
- name: Validate console compose file
ansible.builtin.command: >-
docker compose
--project-name {{ console_service_project_name }}
-f {{ console_service_compose_file }}
--env-file {{ console_service_runtime_env_file }}
config
args:
chdir: "{{ console_service_base_dir }}"
changed_when: false
- name: Pull console service images
ansible.builtin.command: >-
docker compose
--project-name {{ console_service_project_name }}
-f {{ console_service_compose_file }}
--env-file {{ console_service_runtime_env_file }}
pull dashboard caddy
args:
chdir: "{{ console_service_base_dir }}"
when: console_service_pull_images | bool
- name: Refresh console static assets volume
ansible.builtin.command: >-
docker compose
--project-name {{ console_service_project_name }}
-f {{ console_service_compose_file }}
--env-file {{ console_service_runtime_env_file }}
run --rm frontend-assets
args:
chdir: "{{ console_service_base_dir }}"
- name: Start console dashboard and caddy containers
ansible.builtin.command: >-
docker compose
--project-name {{ console_service_project_name }}
-f {{ console_service_compose_file }}
--env-file {{ console_service_runtime_env_file }}
up -d --remove-orphans dashboard caddy
args:
chdir: "{{ console_service_base_dir }}"
- name: Show console compose status
ansible.builtin.command: >-
docker compose
--project-name {{ console_service_project_name }}
-f {{ console_service_compose_file }}
--env-file {{ console_service_runtime_env_file }}
ps
args:
chdir: "{{ console_service_base_dir }}"
changed_when: false

View File

@ -0,0 +1,31 @@
{$PRIMARY_DOMAIN}, {$SECONDARY_DOMAIN} {
encode zstd gzip
@secondary host {$SECONDARY_DOMAIN}
redir @secondary https://{$PRIMARY_DOMAIN}{uri} permanent
handle_path /_next/static/* {
root * /srv
header Cache-Control "public, max-age=31536000, immutable"
file_server
}
@public_assets {
file {
root /srv/public
try_files {path}
}
}
handle @public_assets {
root * /srv/public
header Cache-Control "public, max-age=3600"
file_server
}
reverse_proxy dashboard:3000 {
header_up Host {host}
header_up X-Forwarded-Host {host}
header_up X-Forwarded-Proto {scheme}
header_up X-Forwarded-For {remote_host}
}
}

View File

@ -0,0 +1,56 @@
services:
frontend-assets:
image: ${FRONTEND_IMAGE:?set FRONTEND_IMAGE in .env.runtime}
restart: "no"
command:
- /bin/sh
- -c
- |
set -eu
rm -rf /assets/_next /assets/chunks /assets/public
mkdir -p /assets /assets/public
cp -R /app/dashboard/static/. /assets/
cp -R /app/dashboard/public/. /assets/public
volumes:
- frontend_static:/assets
dashboard:
image: ${FRONTEND_IMAGE:?set FRONTEND_IMAGE in .env.runtime}
restart: unless-stopped
env_file:
- .env.runtime
environment:
NODE_ENV: production
PORT: {{ console_service_port | quote }}
volumes:
- frontend_static:/app/dashboard/.next/static:ro
networks:
- frontend
caddy:
image: caddy:2.10-alpine
restart: unless-stopped
depends_on:
- dashboard
ports:
- "80:80"
- "443:443"
environment:
PRIMARY_DOMAIN: ${PRIMARY_DOMAIN:?set PRIMARY_DOMAIN in .env.runtime}
SECONDARY_DOMAIN: ${SECONDARY_DOMAIN:?set SECONDARY_DOMAIN in .env.runtime}
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- frontend_static:/srv:ro
- caddy_data:/data
- caddy_config:/config
networks:
- frontend
networks:
frontend:
driver: bridge
volumes:
frontend_static:
caddy_data:
caddy_config:

View File

@ -0,0 +1,50 @@
FRONTEND_IMAGE={{ console_service_frontend_image }}
PRIMARY_DOMAIN={{ console_service_primary_domain }}
SECONDARY_DOMAIN={{ console_service_secondary_domain }}
NODE_ENV={{ console_service_node_env }}
PORT={{ console_service_port }}
RUNTIME_ENV={{ console_service_runtime_env }}
REGION={{ console_service_region }}
APP_BASE_URL={{ console_service_app_base_url }}
NEXT_PUBLIC_APP_BASE_URL={{ console_service_next_public_app_base_url }}
NEXT_PUBLIC_SITE_URL={{ console_service_next_public_site_url }}
NEXT_PUBLIC_LOGIN_URL={{ console_service_next_public_login_url }}
NEXT_PUBLIC_DOCS_BASE_URL={{ console_service_next_public_docs_base_url }}
SESSION_COOKIE_SECURE={{ console_service_session_cookie_secure }}
NEXT_PUBLIC_SESSION_COOKIE_SECURE={{ console_service_next_public_session_cookie_secure }}
RUNTIME_HOSTNAME={{ console_service_runtime_hostname }}
NEXT_RUNTIME_HOSTNAME={{ console_service_next_runtime_hostname }}
DEPLOYMENT_HOSTNAME={{ console_service_deployment_hostname }}
NEXT_PUBLIC_RUNTIME_ENVIRONMENT={{ console_service_next_public_runtime_environment }}
NEXT_PUBLIC_RUNTIME_REGION={{ console_service_next_public_runtime_region }}
ACCOUNT_SERVICE_URL={{ console_service_account_service_url }}
NEXT_PUBLIC_ACCOUNT_SERVICE_URL={{ console_service_next_public_account_service_url }}
SERVER_SERVICE_URL={{ console_service_server_service_url }}
NEXT_PUBLIC_SERVER_SERVICE_URL={{ console_service_next_public_server_service_url }}
SERVER_SERVICE_INTERNAL_URL={{ console_service_server_service_internal_url }}
DOCS_SERVICE_URL={{ console_service_docs_service_url }}
DOCS_SERVICE_INTERNAL_URL={{ console_service_docs_service_internal_url }}
OPENCLAW_GATEWAY_REMOTE_URL={{ console_service_openclaw_gateway_remote_url }}
OPENCLAW_GATEWAY_TOKEN={{ console_service_openclaw_gateway_token }}
VAULT_SERVER_URL={{ console_service_vault_server_url }}
VAULT_NAMESPACE={{ console_service_vault_namespace }}
VAULT_TOKEN={{ console_service_vault_token }}
APISIX_AI_GATEWAY_URL={{ console_service_apisix_ai_gateway_url }}
AI_GATEWAY_ACCESS_TOKEN={{ console_service_ai_gateway_access_token }}
INTERNAL_SERVICE_TOKEN={{ console_service_internal_service_token }}
CLOUDFLARE_API_TOKEN={{ console_service_cloudflare_api_token }}
CLOUDFLARE_ACCOUNT_ID={{ console_service_cloudflare_account_id }}
CLOUDFLARE_WEB_ANALYTICS_SITE_TAG={{ console_service_cloudflare_web_analytics_site_tag }}
CLOUDFLARE_ZONE_TAG={{ console_service_cloudflare_zone_tag }}
ROOT_EMAIL_WHITELIST={{ console_service_root_email_whitelist }}
NEXT_PUBLIC_PAYPAL_CLIENT_ID={{ console_service_next_public_paypal_client_id }}
NEXT_PUBLIC_GISCUS_REPO={{ console_service_next_public_giscus_repo }}
NEXT_PUBLIC_GISCUS_REPO_ID={{ console_service_next_public_giscus_repo_id }}
NEXT_PUBLIC_GISCUS_CATEGORY={{ console_service_next_public_giscus_category }}
NEXT_PUBLIC_GISCUS_CATEGORY_ID={{ console_service_next_public_giscus_category_id }}
NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO={{ console_service_next_public_stripe_price_xstream_paygo }}
NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION={{ console_service_next_public_stripe_price_xstream_subscription }}
NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO={{ console_service_next_public_stripe_price_xscopehub_paygo }}
NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION={{ console_service_next_public_stripe_price_xscopehub_subscription }}
NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO={{ console_service_next_public_stripe_price_xcloudflow_paygo }}
NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION={{ console_service_next_public_stripe_price_xcloudflow_subscription }}

View File

@ -0,0 +1,38 @@
---
deploy_acp_codex: true
deploy_acp_opencode: true
deploy_acp_gemini: true
xworkmate_bridge_service_name: xworkmate-bridge
xworkmate_bridge_service_user: root
xworkmate_bridge_service_group: root
xworkmate_bridge_workdir: /root
xworkmate_bridge_listen_host: 127.0.0.1
xworkmate_bridge_listen_port: 8787
xworkmate_bridge_binary_path: /usr/local/bin/xworkmate-go-core
xworkmate_bridge_local_source_dir: "{{ playbook_dir }}/../xworkmate-bridge"
xworkmate_bridge_local_build_dir: "{{ playbook_dir }}/.artifacts/xworkmate_bridge"
xworkmate_bridge_local_binary_path: "{{ xworkmate_bridge_local_build_dir }}/xworkmate-go-core"
xworkmate_bridge_build_goos: linux
xworkmate_bridge_build_goarch: amd64
xworkmate_bridge_domain: acp-server.svc.plus
xworkmate_bridge_public_base_url: https://acp-server.svc.plus
xworkmate_bridge_caddyfile_path: /etc/caddy/Caddyfile
xworkmate_bridge_caddy_conf_dir: /etc/caddy/conf.d
xworkmate_bridge_caddy_fragment_path: /etc/caddy/conf.d/acp-server.caddy
xworkmate_bridge_codex_upstream_host: 127.0.0.1
xworkmate_bridge_codex_upstream_port: 9010
xworkmate_bridge_opencode_upstream_host: 127.0.0.1
xworkmate_bridge_opencode_upstream_port: 3910
xworkmate_bridge_gemini_upstream_host: 127.0.0.1
xworkmate_bridge_gemini_upstream_port: 8791
xworkmate_bridge_obsolete_caddy_fragment_paths:
- /etc/caddy/conf.d/acp-server-codex.caddy
- /etc/caddy/conf.d/acp-server-opencode.caddy
- /etc/caddy/conf.d/acp-server-gemini.caddy
- /etc/caddy/conf.d/acp-server-bridge.caddy
- /etc/caddy/conf.d/acp-server-bridge-server.caddy
xworkmate_bridge_packages:
- caddy

View File

@ -0,0 +1,11 @@
---
- name: Restart xworkmate bridge
ansible.builtin.systemd:
name: "{{ xworkmate_bridge_service_name }}"
state: restarted
daemon_reload: true
- name: Reload caddy
ansible.builtin.systemd:
name: caddy
state: reloaded

View File

@ -0,0 +1,115 @@
---
- name: Install xworkmate-bridge prerequisites
ansible.builtin.package:
name: "{{ xworkmate_bridge_packages }}"
state: present
- name: Ensure local xworkmate-bridge build directory exists
ansible.builtin.file:
path: "{{ xworkmate_bridge_local_build_dir }}"
state: directory
mode: "0755"
delegate_to: localhost
become: false
- name: Build xworkmate-bridge locally
ansible.builtin.command:
cmd: go build -o "{{ xworkmate_bridge_local_binary_path }}" .
chdir: "{{ xworkmate_bridge_local_source_dir }}"
environment:
GOOS: "{{ xworkmate_bridge_build_goos }}"
GOARCH: "{{ xworkmate_bridge_build_goarch }}"
CGO_ENABLED: "0"
GO111MODULE: "on"
delegate_to: localhost
become: false
- name: Upload xworkmate-bridge binary
ansible.builtin.copy:
src: "{{ xworkmate_bridge_local_binary_path }}"
dest: "{{ xworkmate_bridge_binary_path }}"
owner: root
group: root
mode: "0755"
notify: Restart xworkmate bridge
- name: Include Codex ACP provider role
ansible.builtin.import_role:
name: roles/vhosts/acp_codex
vars:
acp_codex_manage_caddy: false
when:
- deploy_acp_codex | bool
- name: Include OpenCode ACP provider role
ansible.builtin.import_role:
name: roles/vhosts/acp_opencode
vars:
acp_opencode_manage_caddy: false
when:
- deploy_acp_opencode | bool
- name: Include Gemini ACP provider role
ansible.builtin.import_role:
name: roles/vhosts/acp_gemini
when:
- deploy_acp_gemini | bool
- name: Ensure Caddy fragment directory exists for ACP ingress
ansible.builtin.file:
path: "{{ xworkmate_bridge_caddy_conf_dir }}"
state: directory
owner: root
group: root
mode: "0755"
- name: Deploy ACP Caddy main file
ansible.builtin.copy:
content: "import {{ xworkmate_bridge_caddy_conf_dir }}/*.caddy\n"
dest: "{{ xworkmate_bridge_caddyfile_path }}"
owner: root
group: root
mode: "0644"
notify: Reload caddy
- name: Deploy unified ACP Caddy site
ansible.builtin.template:
src: acp-site.caddy.j2
dest: "{{ xworkmate_bridge_caddy_fragment_path }}"
owner: root
group: root
mode: "0644"
notify: Reload caddy
- name: Remove deprecated ACP Caddy fragments
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop: "{{ xworkmate_bridge_obsolete_caddy_fragment_paths }}"
notify: Reload caddy
- name: Deploy xworkmate-bridge systemd service
ansible.builtin.template:
src: xworkmate-bridge.service.j2
dest: "/etc/systemd/system/{{ xworkmate_bridge_service_name }}.service"
owner: root
group: root
mode: "0644"
notify: Restart xworkmate bridge
- name: Ensure xworkmate-bridge is enabled and running
ansible.builtin.systemd:
name: "{{ xworkmate_bridge_service_name }}"
enabled: true
state: started
daemon_reload: true
- name: Ensure Caddy is enabled and running for ACP ingress
ansible.builtin.systemd:
name: caddy
enabled: true
state: started
- name: Include ACP ingress validation tasks
ansible.builtin.import_tasks: validate.yml
tags: [deploy_acp_vhosts, deploy_acp_vhosts_validate]

View File

@ -0,0 +1,108 @@
---
- name: Validate Caddy config for unified ACP ingress
ansible.builtin.command: caddy validate --config "{{ xworkmate_bridge_caddyfile_path }}"
changed_when: false
- name: Read deployed ACP Caddy fragment
ansible.builtin.command:
cmd: cat "{{ xworkmate_bridge_caddy_fragment_path }}"
changed_when: false
register: xworkmate_bridge_fragment
- name: Assert Codex route exists in deployed ACP fragment
ansible.builtin.assert:
that:
- "'handle_path /codex*' in xworkmate_bridge_fragment.stdout"
fail_msg: "Missing /codex route in {{ xworkmate_bridge_caddy_fragment_path }}"
when:
- deploy_acp_codex | bool
- name: Assert OpenCode route exists in deployed ACP fragment
ansible.builtin.assert:
that:
- "'handle_path /opencode*' in xworkmate_bridge_fragment.stdout"
fail_msg: "Missing /opencode route in {{ xworkmate_bridge_caddy_fragment_path }}"
when:
- deploy_acp_opencode | bool
- name: Assert Gemini route exists in deployed ACP fragment
ansible.builtin.assert:
that:
- "'handle_path /gemini*' in xworkmate_bridge_fragment.stdout"
fail_msg: "Missing /gemini route in {{ xworkmate_bridge_caddy_fragment_path }}"
when:
- deploy_acp_gemini | bool
- name: Check Codex route through unified ACP ingress
ansible.builtin.uri:
url: "http://127.0.0.1/codex"
method: GET
headers:
Host: "{{ xworkmate_bridge_domain }}"
follow_redirects: none
status_code: [200, 301, 302, 307, 308, 401, 403, 404, 502]
changed_when: false
register: xworkmate_bridge_codex_redirect
when:
- deploy_acp_codex | bool
- name: Check OpenCode route through unified ACP ingress
ansible.builtin.uri:
url: "http://127.0.0.1/opencode"
method: GET
headers:
Host: "{{ xworkmate_bridge_domain }}"
follow_redirects: none
status_code: [200, 301, 302, 307, 308, 401, 403, 404, 502]
changed_when: false
register: xworkmate_bridge_opencode_redirect
when:
- deploy_acp_opencode | bool
- name: Check Gemini route through unified ACP ingress
ansible.builtin.uri:
url: "http://127.0.0.1/gemini"
method: GET
headers:
Host: "{{ xworkmate_bridge_domain }}"
follow_redirects: none
status_code: [200, 301, 302, 307, 308, 401, 403, 404, 502]
changed_when: false
register: xworkmate_bridge_gemini_redirect
when:
- deploy_acp_gemini | bool
- name: Check deprecated ACP fragments are absent
ansible.builtin.stat:
path: "{{ item }}"
changed_when: false
loop: "{{ xworkmate_bridge_obsolete_caddy_fragment_paths }}"
register: xworkmate_bridge_obsolete_fragments
- name: Assert deprecated ACP fragments were removed
ansible.builtin.assert:
that:
- not item.stat.exists
fail_msg: "Deprecated ACP fragment still exists: {{ item.stat.path }}"
loop: "{{ xworkmate_bridge_obsolete_fragments.results }}"
loop_control:
label: "{{ item.stat.path }}"
- name: Show xworkmate-bridge service status
ansible.builtin.command: systemctl status "{{ xworkmate_bridge_service_name }}" --no-pager
register: xworkmate_bridge_status
changed_when: false
failed_when: false
- name: Summarize unified ACP ingress state
ansible.builtin.debug:
msg:
- "Unified domain: {{ xworkmate_bridge_domain }}"
- "Codex public base URL: https://{{ xworkmate_bridge_domain }}/codex"
- "OpenCode public base URL: https://{{ xworkmate_bridge_domain }}/opencode"
- "Gemini public base URL: https://{{ xworkmate_bridge_domain }}/gemini"
- "Bridge service: {{ xworkmate_bridge_status.stdout | default('N/A') }}"
- "Codex route: /codex -> {{ xworkmate_bridge_codex_upstream_host }}:{{ xworkmate_bridge_codex_upstream_port }}"
- "OpenCode route: /opencode -> {{ xworkmate_bridge_opencode_upstream_host }}:{{ xworkmate_bridge_opencode_upstream_port }}"
- "Gemini route: /gemini -> {{ xworkmate_bridge_gemini_upstream_host }}:{{ xworkmate_bridge_gemini_upstream_port }}"
- "Deployed fragment: {{ xworkmate_bridge_fragment.stdout | default('N/A') }}"

View File

@ -0,0 +1,19 @@
{{ xworkmate_bridge_domain }} {
{% if deploy_acp_codex | bool %}
handle_path /codex* {
reverse_proxy {{ xworkmate_bridge_codex_upstream_host }}:{{ xworkmate_bridge_codex_upstream_port }}
}
{% endif %}
{% if deploy_acp_opencode | bool %}
handle_path /opencode* {
reverse_proxy {{ xworkmate_bridge_opencode_upstream_host }}:{{ xworkmate_bridge_opencode_upstream_port }}
}
{% endif %}
{% if deploy_acp_gemini | bool %}
handle_path /gemini* {
reverse_proxy {{ xworkmate_bridge_gemini_upstream_host }}:{{ xworkmate_bridge_gemini_upstream_port }}
}
{% endif %}
}

View File

@ -0,0 +1,16 @@
[Unit]
Description=XWorkmate Bridge ACP Service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User={{ xworkmate_bridge_service_user }}
Group={{ xworkmate_bridge_service_group }}
WorkingDirectory={{ xworkmate_bridge_workdir }}
ExecStart={{ xworkmate_bridge_binary_path }} serve --listen {{ xworkmate_bridge_listen_host }}:{{ xworkmate_bridge_listen_port }}
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target

View File

@ -25,3 +25,6 @@ nextjs_private_tmp: true
nextjs_install_deps: true
nextjs_build: "{{ nextjs_run_mode == 'prod' }}"
nextjs_env_file_path: "{{ nextjs_app_dir }}/.env"
nextjs_env_src: ""
nextjs_env_content: ""

View File

@ -1,7 +1,9 @@
---
- name: Ensure git is installed
ansible.builtin.apt:
name: git
name:
- git
- rsync
state: present
update_cache: true
@ -21,6 +23,28 @@
update: true
notify: Restart nextjs service
- name: Copy Next.js environment file from source
ansible.builtin.copy:
src: "{{ nextjs_env_src }}"
dest: "{{ nextjs_env_file_path }}"
owner: "{{ nextjs_service_user }}"
group: "{{ nextjs_service_group }}"
mode: "0600"
when: nextjs_env_src | length > 0
notify: Restart nextjs service
- name: Render inline Next.js environment file
ansible.builtin.copy:
content: "{{ nextjs_env_content }}"
dest: "{{ nextjs_env_file_path }}"
owner: "{{ nextjs_service_user }}"
group: "{{ nextjs_service_group }}"
mode: "0600"
when:
- nextjs_env_src | length == 0
- nextjs_env_content | length > 0
notify: Restart nextjs service
- name: Install Next.js dependencies
ansible.builtin.command: yarn install
args:

View File

@ -6,6 +6,7 @@ After=network.target
Type=simple
WorkingDirectory={{ nextjs_app_dir }}
EnvironmentFile=-{{ nextjs_env_file_path }}
Environment=NODE_ENV={{ nextjs_node_env }}
Environment=PORT={{ nextjs_port }}

View File

@ -0,0 +1,23 @@
---
xray_exporter_source_dir: "{{ playbook_dir }}/../xray-exporter"
xray_exporter_app_dir: "/opt/xray-exporter"
xray_exporter_go_version: "1.25.1"
xray_exporter_go_root: "/usr/local/go"
xray_exporter_go_bin: "{{ xray_exporter_go_root }}/bin/go"
xray_exporter_binary_name: "xray-exporter"
xray_exporter_binary_path: "/usr/local/bin/{{ xray_exporter_binary_name }}"
xray_exporter_service_name: "xray-exporter"
xray_exporter_service_description: "xray-exporter service"
xray_exporter_user: "root"
xray_exporter_group: "root"
xray_exporter_env_dir: "/etc/default"
xray_exporter_env_path: "{{ xray_exporter_env_dir }}/xray-exporter"
xray_exporter_listen_addr: "127.0.0.1:8080"
xray_exporter_scrape_interval: "1m"
xray_exporter_node_id: "node-xhttp.svc.plus"
xray_exporter_env_name: "prod"
xray_exporter_stats_url: "http://127.0.0.1:49227/debug/vars"
xray_exporter_stats_token: ""
xray_exporter_accounts_base_url: "https://accounts.svc.plus"
xray_exporter_internal_service_token: ""

View File

@ -0,0 +1,6 @@
---
- name: Restart xray-exporter
ansible.builtin.systemd:
name: "{{ xray_exporter_service_name }}"
state: restarted
daemon_reload: true

View File

@ -0,0 +1,102 @@
---
- name: Map Go architecture
ansible.builtin.set_fact:
xray_exporter_go_arch: >-
{{ 'arm64' if ansible_architecture in ['aarch64', 'arm64'] else 'amd64' }}
- name: Ensure xray-exporter build prerequisites are installed
ansible.builtin.apt:
name:
- curl
- ca-certificates
- tar
- rsync
state: present
update_cache: true
- name: Check installed Go version
ansible.builtin.command: "{{ xray_exporter_go_bin }} version"
register: xray_exporter_go_version_check
changed_when: false
failed_when: false
- name: Download Go toolchain archive
ansible.builtin.get_url:
url: "https://go.dev/dl/go{{ xray_exporter_go_version }}.linux-{{ xray_exporter_go_arch }}.tar.gz"
dest: "/tmp/go{{ xray_exporter_go_version }}.linux-{{ xray_exporter_go_arch }}.tar.gz"
mode: "0644"
when: xray_exporter_go_version_check.rc != 0 or ('go' ~ xray_exporter_go_version) not in xray_exporter_go_version_check.stdout
- name: Remove previous Go installation
ansible.builtin.file:
path: "{{ xray_exporter_go_root }}"
state: absent
when: xray_exporter_go_version_check.rc != 0 or ('go' ~ xray_exporter_go_version) not in xray_exporter_go_version_check.stdout
- name: Install Go toolchain
ansible.builtin.unarchive:
src: "/tmp/go{{ xray_exporter_go_version }}.linux-{{ xray_exporter_go_arch }}.tar.gz"
dest: "/usr/local"
remote_src: true
when: xray_exporter_go_version_check.rc != 0 or ('go' ~ xray_exporter_go_version) not in xray_exporter_go_version_check.stdout
- name: Create xray-exporter directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ xray_exporter_user }}"
group: "{{ xray_exporter_group }}"
mode: "0755"
loop:
- "{{ xray_exporter_app_dir }}"
- "{{ xray_exporter_env_dir }}"
- name: Sync xray-exporter source tree
ansible.posix.synchronize:
src: "{{ xray_exporter_source_dir }}/"
dest: "{{ xray_exporter_app_dir }}/"
delete: true
notify: Restart xray-exporter
- name: Build xray-exporter binary
ansible.builtin.command: >-
{{ xray_exporter_go_bin }} build -buildvcs=false -o {{ xray_exporter_binary_path }} ./cmd/xray-exporter
args:
chdir: "{{ xray_exporter_app_dir }}"
environment:
GOROOT: "{{ xray_exporter_go_root }}"
PATH: "{{ xray_exporter_go_root }}/bin:{{ ansible_env.PATH }}"
GOTOOLCHAIN: local
notify: Restart xray-exporter
- name: Ensure xray-exporter binary permissions
ansible.builtin.file:
path: "{{ xray_exporter_binary_path }}"
owner: "{{ xray_exporter_user }}"
group: "{{ xray_exporter_group }}"
mode: "0755"
- name: Deploy xray-exporter environment file
ansible.builtin.template:
src: xray-exporter.env.j2
dest: "{{ xray_exporter_env_path }}"
owner: root
group: root
mode: "0600"
notify: Restart xray-exporter
- name: Install xray-exporter systemd service
ansible.builtin.template:
src: xray-exporter.service.j2
dest: "/etc/systemd/system/{{ xray_exporter_service_name }}.service"
owner: root
group: root
mode: "0644"
notify: Restart xray-exporter
- name: Enable and start xray-exporter
ansible.builtin.systemd:
name: "{{ xray_exporter_service_name }}"
enabled: true
state: started
daemon_reload: true

View File

@ -0,0 +1,8 @@
XRAY_STATS_URL={{ xray_exporter_stats_url }}
XRAY_STATS_TOKEN={{ xray_exporter_stats_token }}
ACCOUNTS_BASE_URL={{ xray_exporter_accounts_base_url }}
INTERNAL_SERVICE_TOKEN={{ xray_exporter_internal_service_token }}
EXPORTER_NODE_ID={{ xray_exporter_node_id }}
EXPORTER_ENV={{ xray_exporter_env_name }}
SCRAPE_INTERVAL={{ xray_exporter_scrape_interval }}
LISTEN_ADDR={{ xray_exporter_listen_addr }}

View File

@ -0,0 +1,15 @@
[Unit]
Description={{ xray_exporter_service_description }}
After=network.target
[Service]
Type=simple
WorkingDirectory={{ xray_exporter_app_dir }}
EnvironmentFile={{ xray_exporter_env_path }}
ExecStart={{ xray_exporter_binary_path }}
Restart=always
User={{ xray_exporter_user }}
Group={{ xray_exporter_group }}
[Install]
WantedBy=multi-user.target

View File

@ -45,6 +45,11 @@ cloudflare_dns_records:
content: 46.250.251.132
ttl: 1
proxied: false
- type: A
name: xworkmate-bridge.svc.plus
content: 46.250.251.132
ttl: 1
proxied: false
- type: CNAME
name: console-8fa9cd3-contabo.svc.plus
content: jp-xhttp-contabo.svc.plus
@ -85,3 +90,8 @@ cloudflare_dns_records:
content: vps-preview-accounts.svc.plus
ttl: 1
proxied: false
- type: CNAME
name: zitadel.svc.plus
content: jp-xhttp-contabo.svc.plus
ttl: 1
proxied: false