-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathdocker-compose.yml.example
More file actions
173 lines (167 loc) · 5.84 KB
/
Copy pathdocker-compose.yml.example
File metadata and controls
173 lines (167 loc) · 5.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
services:
mt5:
image: dockurr/windows:5.14
environment:
RAM_SIZE: "4G"
RAM_CHECK: "N"
CPU_CORES: "2"
DISK_SIZE: "32G"
devices:
- /dev/kvm
cap_add:
- NET_ADMIN
ports:
- "${NOVNC_PORT:-8006}:8006"
volumes:
- ./data/storage:/storage
- ./data/oem:/oem
- ./data/shared:/shared
- ./assets:/shared/assets:ro
- ./data/win.iso:/boot.iso
deploy:
resources:
limits:
memory: 512M
memswap_limit: 5G
healthcheck:
test: ["CMD", "sh", "/shared/scripts/healthcheck.sh"]
interval: 30s
timeout: 30s
retries: 10
start_period: 120s
restart: unless-stopped
stop_grace_period: 2m
# Wickworks TA sidecar — shares mt5's net namespace so it's reachable
# ONLY from the mt5 container (and from the Windows VM via the dockurr
# gateway 20.20.20.1:8000). No ports published; nothing else on the
# docker network can talk to it.
wickworks:
image: psyb0t/wickworks:v0.3.1
restart: unless-stopped
network_mode: "service:mt5"
environment:
LOG_LEVEL: INFO
MAX_BARS: "5000"
MIN_BARS: "50"
depends_on:
- mt5
# Daily log rotator. Rotates data/shared/logs/*.log to *.log.YYYYMMDD
# at the day boundary and prunes archives older than RETAIN_DAYS.
# Truncate-in-place so the Python API's open log handles keep working
# without reopening. Hourly check, idempotent (keyed on yesterday's
# archive existing).
log-rotator:
image: alpine:3.20
restart: unless-stopped
environment:
LOG_DIR: /logs
RETAIN_DAYS: "7"
INTERVAL: "3600"
volumes:
- ./data/shared/logs:/logs
- ./scripts/rotate-logs.sh:/rotate.sh:ro
command: ["sh", "/rotate.sh"]
# nginx is the single entry point for all terminal APIs. Routes
# /<broker>/<account>/... to mt5:<terminal_port> (per-terminal Python
# API process inside the Windows VM, reachable via mt5 container's
# iptables DNAT). Auto-generated from config/config.yaml by run.sh.
# Bound to 127.0.0.1:8888 so it's loopback-only by default — LAN
# exposure is opt-in (change the host bind), tailnet exposure is via
# the optional tailscale sidecar below.
# Unified MCP endpoint. One MCP session that reaches every terminal, with
# broker/account as tool parameters, instead of one endpoint per terminal.
# The per-terminal /<broker>/<account>/mcp endpoints keep working unchanged;
# nginx routes /mcp/ here.
#
# Reads the same config/config.yaml that generates the nginx routing, so it
# cannot route somewhere nginx does not. It never waits for terminals: a
# terminal that is down fails only the calls naming it.
mcpunifier:
build:
context: .
dockerfile: Dockerfile.mcpunifier
restart: unless-stopped
environment:
MT5_HOST: mt5
LOG_LEVEL: ${MCP_LOG_LEVEL:-info}
volumes:
# Read-only: this service only ever reads the terminal table.
- ./config/config.yaml:/app/config/config.yaml:ro
security_opt:
- "no-new-privileges:true"
cap_drop:
- ALL
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,size=16m
- /var/log/mcpunifier:rw,noexec,nosuid,size=64m
init: true
deploy:
resources:
limits:
memory: 256M
cpus: "0.5"
pids: 128
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:6600/health', timeout=3).status == 200 else 1)"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
depends_on:
- mt5
nginx:
image: nginx:1.30.0-alpine3.23
restart: unless-stopped
ports:
- "127.0.0.1:${API_HOST_PORT:-8888}:80"
volumes:
- ./.data/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- mt5
- mcpunifier
# ── Cloudflare Tunnel (uncomment to expose API publicly) ──────────
# Point your cloudflared ingress to http://nginx:80 (single backend,
# nginx routes per-terminal paths). Drop creds + config in
# ./.data/cloudflared/.
# cloudflared:
# image: cloudflare/cloudflared:2026.3.0
# restart: unless-stopped
# command: tunnel --config /etc/cloudflared/config.yml run
# volumes:
# - ./.data/cloudflared/config.yml:/etc/cloudflared/config.yml:ro
# - ./.data/cloudflared/creds.json:/etc/cloudflared/creds.json:ro
# depends_on:
# - nginx
# ── Tailscale (uncomment to expose API over tailnet HTTP) ─────────
# Set tailscale.auth_key in config/config.yaml; for Headscale, also set
# tailscale.login_server. run.sh reads both and writes .env + wires
# tailscale serve via the CLI inside the sidecar. URL scheme:
# http://mt5-httpapi/<broker>/<account>/...
# Plain HTTP — bare MagicDNS hostnames don't have matching certs, and
# the wireguard layer already encrypts everything inside the tailnet.
# The sidecar runs in its OWN netns (bridge mode, not host) so it gets
# its own tailnet identity — host's tailscale (if any) stays clean and
# ACLs scope to the container's node only.
# tailscale:
# image: tailscale/tailscale:v1.96.5
# restart: unless-stopped
# environment:
# - TS_AUTHKEY=${TS_AUTHKEY:-}
# - TS_HOSTNAME=${TS_HOSTNAME:-mt5-httpapi}
# - TS_STATE_DIR=/var/lib/tailscale
# - TS_USERSPACE=false # real tailscale0 in this netns — outbound to 100.64.x goes via SIDECAR's identity, not the host's. Requires /dev/net/tun + NET_ADMIN + NET_RAW (all set below).
# - TS_EXTRA_ARGS=${TS_EXTRA_ARGS:---accept-dns=false}
# volumes:
# - ./.data/tailscale/state:/var/lib/tailscale
# - /dev/net/tun:/dev/net/tun
# cap_add:
# - NET_ADMIN
# - NET_RAW
# depends_on:
# - nginx