Skip to content

Commit 0ac722d

Browse files
committed
added example docker compose
1 parent 3a5e9ec commit 0ac722d

1 file changed

Lines changed: 226 additions & 0 deletions

File tree

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
volumes:
2+
cert-data:
3+
shared_conf:
4+
5+
services:
6+
dstack-ingress:
7+
build:
8+
context: .
9+
dockerfile_inline: |
10+
FROM dstack-ingress:latest
11+
RUN apt-get update && \
12+
apt-get install -y --no-install-recommends inotify-tools && \
13+
rm -rf /var/lib/apt/lists/*
14+
restart: unless-stopped
15+
volumes:
16+
- /var/run/dstack.sock:/var/run/dstack.sock
17+
- cert-data:/etc/letsencrypt
18+
- shared_conf:/shared/conf.d
19+
ports:
20+
- 443:443
21+
environment:
22+
DNS_PROVIDER: route53
23+
AWS_REGION: ${ROUTE53_AWS_REGION}
24+
AWS_ROLE_ARN: ${ROUTE53_AWS_ROLE_ARN}
25+
AWS_ACCESS_KEY_ID: ${ROUTE53_AWS_ACCESS_KEY_ID}
26+
AWS_SECRET_ACCESS_KEY: ${ROUTE53_AWS_SECRET_ACCESS_KEY}
27+
GATEWAY_DOMAIN: npw.${DSTACK_GATEWAY_DOMAIN}
28+
CERTBOT_EMAIL: ${CERTBOT_EMAIL}
29+
CERTBOT_STAGING: 'false'
30+
SET_CAA: 'true'
31+
PROXY_BUFFER_SIZE: 128k
32+
PROXY_BUFFERS: 4 256k
33+
PROXY_BUSY_BUFFERS_SIZE: 256k
34+
DOMAIN: ${MACHINE_NAME}.example.com
35+
ALIAS_DOMAIN: app.example.com
36+
TARGET_ENDPOINT: http://dynamic_backends
37+
ROUTE53_INITIAL_WEIGHT: 0
38+
configs:
39+
- source: nginx_master_config
40+
target: /etc/nginx/nginx.conf
41+
- source: nginx_fallback
42+
target: /etc/nginx/conf.d/fallback.conf
43+
- source: nginx_entrypoint
44+
target: /nginx_entrypoint.sh
45+
mode: 0755
46+
- source: nginx_mainloop
47+
target: /nginx_mainloop.sh
48+
mode: 0755
49+
networks:
50+
- default
51+
- my-tasks
52+
entrypoint: ["/nginx_entrypoint.sh"]
53+
command: []
54+
55+
watcher:
56+
build:
57+
context: .
58+
dockerfile_inline: |
59+
FROM python:3.11-alpine
60+
RUN pip install --no-cache-dir docker
61+
CMD ["python", "-u", "/watcher.py"]
62+
volumes:
63+
- /var/run/docker.sock:/var/run/docker.sock
64+
- shared_conf:/shared/conf.d
65+
configs:
66+
- source: watcher_py
67+
target: /watcher.py
68+
69+
configs:
70+
71+
nginx_master_config:
72+
content: |
73+
# /etc/nginx/nginx.conf
74+
worker_processes auto;
75+
76+
events {
77+
worker_connections 1024;
78+
}
79+
80+
http {
81+
include /etc/nginx/mime.types;
82+
default_type application/octet-stream;
83+
84+
# Pull in upstream definitions
85+
include /shared/conf.d/upstreams.conf;
86+
87+
# Pull in server blocks
88+
include /etc/nginx/conf.d/*.conf;
89+
}
90+
91+
92+
nginx_fallback:
93+
content: |
94+
# Internal Maintenance Fallback Server
95+
server {
96+
listen 127.0.0.1:9998;
97+
location / {
98+
add_header Content-Type text/plain;
99+
return 503 "System is scaling or deploying. Please refresh in a few seconds.\n";
100+
}
101+
}
102+
103+
nginx_entrypoint:
104+
content: |
105+
#!/bin/sh
106+
echo "setting up upstreams.conf" >&2
107+
/bin/ls -lah /shared/conf.d
108+
109+
# starting - so clear out any old upstreams
110+
echo 'upstream dynamic_backends { server 127.0.0.1:9998; }' > /shared/conf.d/upstreams.conf
111+
cat /shared/conf.d/upstreams.conf >&2
112+
113+
echo "execute phalas container setup" >&2
114+
exec /scripts/entrypoint.sh /nginx_mainloop.sh
115+
116+
nginx_mainloop:
117+
content: |
118+
#!/bin/sh
119+
120+
echo "phalas container setup COMPLETE!" >&2
121+
echo "start Nginx in background with server config: " >&2
122+
cat /shared/conf.d/upstreams.conf >&2
123+
nginx -c /etc/nginx/nginx.conf -g "daemon off; error_log /dev/stderr info;" &
124+
NGINX_PID=$$!
125+
126+
echo "TEE Ingress started. Watching for upstream changes..."
127+
# Block and watch file for atomic modifications
128+
while inotifywait -q -e close_write,moved_to /shared/conf.d; do
129+
echo "[$(date)] Upstream configuration updated. Reloading Nginx..."
130+
nginx -s reload
131+
done
132+
wait $$NGINX_PID
133+
134+
135+
# --- PYTHON WATCHER SCRIPT ---
136+
watcher_py:
137+
content: |
138+
import docker
139+
import os
140+
import time
141+
142+
client = docker.from_env()
143+
SHARED_FILE = "/shared/conf.d/upstreams.conf"
144+
LABEL_KEY = "phala.ingress.target"
145+
LABEL_VAL = "true"
146+
TASK_NETWORK = "my-tasks"
147+
148+
def ensure_on_task_network(c):
149+
"""Attach container to the shared my-tasks network if not already on it."""
150+
networks = c.attrs.get("NetworkSettings", {}).get("Networks", {})
151+
# Find the network by suffix in case compose prefixes the project name
152+
matched = next((k for k in networks if k.endswith(TASK_NETWORK)), None)
153+
if matched:
154+
print(f" [net] {c.name} already on {matched}")
155+
return matched
156+
try:
157+
net = client.networks.list(filters={"name": TASK_NETWORK})
158+
if not net:
159+
print(f" [net] ERROR: network '{TASK_NETWORK}' not found")
160+
return None
161+
net[0].connect(c)
162+
c.reload()
163+
print(f" [net] attached {c.name} to {net[0].name}")
164+
# Return actual network name after attachment
165+
networks = c.attrs.get("NetworkSettings", {}).get("Networks", {})
166+
return next((k for k in networks if k.endswith(TASK_NETWORK)), None)
167+
except Exception as e:
168+
print(f" [net] failed to attach {c.name}: {e}")
169+
return None
170+
171+
def get_task_network_ip(c):
172+
"""Get the container's IP on the my-tasks network."""
173+
networks = c.attrs.get("NetworkSettings", {}).get("Networks", {})
174+
net_name = next((k for k in networks if k.endswith(TASK_NETWORK)), None)
175+
if net_name:
176+
return networks[net_name].get("IPAddress")
177+
return None
178+
179+
def update_upstreams():
180+
print("Reconciling Nginx upstream state...")
181+
containers = client.containers.list(
182+
filters={"label": f"{LABEL_KEY}={LABEL_VAL}", "status": "running"}
183+
)
184+
print(f"found {len(containers)} containers")
185+
186+
config = "upstream dynamic_backends {\n"
187+
for c in containers:
188+
net_name = ensure_on_task_network(c)
189+
ip = get_task_network_ip(c) if net_name else None
190+
if not ip:
191+
print(f" [warn] skipping {c.name} — no IP on {TASK_NETWORK}")
192+
continue
193+
port = c.labels.get("phala.ingress.port", "8080")
194+
print(f" -> Enrolled: {c.name.lstrip('/')} on {net_name} ({ip}:{port})")
195+
# Add passive health checks (fails 3 times in 15s = temporarily dead)
196+
config += f" server {ip}:{port} max_fails=3 fail_timeout=15s;\n"
197+
198+
if containers:
199+
config += " server 127.0.0.1:9998 backup;\n}\n"
200+
else:
201+
config += " server 127.0.0.1:9998;\n}\n"
202+
203+
# Atomic write to trigger inotify cleanly
204+
temp_file = f"{SHARED_FILE}.tmp"
205+
with open(temp_file, "w") as f:
206+
f.write(config)
207+
os.replace(temp_file, SHARED_FILE)
208+
print("Nginx state successfully written.\n")
209+
print(f"{config}\n")
210+
211+
def main():
212+
print("Starting Phala Python Docker Watcher...")
213+
update_upstreams()
214+
215+
events = client.events(decode=True, filters={"type": "container", "event": ["start", "die"]})
216+
for event in events:
217+
attrs = event.get("Actor", {}).get("Attributes", {})
218+
if attrs.get(LABEL_KEY) == LABEL_VAL:
219+
action = event.get("status")
220+
print(f"App container {action} detected!")
221+
# Slight delay ensures Docker DNS has fully registered the new container IP
222+
time.sleep(1)
223+
update_upstreams()
224+
225+
if __name__ == "__main__":
226+
main()

0 commit comments

Comments
 (0)