/
/
/
Ansible role that deployes services on my runner machine
1# D&D session agent stack.
2#
3# One compose project so the whole system has a single on/off switch:
4# dnd_enabled: false -> every part of it goes away.
5#
6# Three services with deliberately different lifecycles:
7#
8# dnd-stt always up. A generic OpenAI-compatible speech endpoint that
9# anything on the runner can use, not just this stack.
10# dnd-agent started per session and stopped afterwards. It only makes sense
11# while people are in the room, and leaving it idling would keep a
12# participant in the conference all week.
13# dnd-post runs to completion and exits. Under a compose profile so a plain
14# `up` never starts it.
15
16services:
17 dnd-stt:
18 container_name: dnd-stt
19 image: "{{ container_images.dnd_stt }}"
20 restart: unless-stopped
21
22 ports:
23 - "{{ dnd_stt_host_port }}:8000"
24
25 env_file:
26 - stt.env
27
28 volumes:
29 - /etc/localtime:/etc/localtime:ro
30 # Whisper weights are several hundred MB and fetched on first use, so
31 # this must persist or every restart re-downloads them.
32 - {{ dnd_stt_model_dir }}:/home/ubuntu/.cache/huggingface
33
34{% if gpu_enabled %}
35 runtime: nvidia
36 deploy:
37 resources:
38 reservations:
39 devices:
40 - driver: nvidia
41 count: 1
42 capabilities: [gpu]
43{% endif %}
44
45 healthcheck:
46 test: ["CMD", "curl", "-fsS", "http://localhost:8000/health"]
47 interval: 30s
48 timeout: 10s
49 start_period: {{ dnd_stt_start_period }}
50 retries: 3
51
52 networks:
53 {{ runner_docker_network }}:
54 aliases:
55 - dnd-stt
56
57 dnd-agent:
58 container_name: dnd-agent
59 image: "{{ dnd_agent_image }}"
60
61 # Never restarted automatically. A session ends when people leave, and a
62 # restarting agent would silently rejoin an empty room and record silence.
63 restart: "no"
64 profiles: ["session"]
65
66 env_file:
67 - agent.env
68
69 depends_on:
70 dnd-stt:
71 condition: service_healthy
72
73 volumes:
74 - /etc/localtime:/etc/localtime:ro
75 - {{ dnd_media_dir }}:/media/dnd
76
77 networks:
78 {{ runner_docker_network }}:
79 aliases:
80 - dnd-agent
81
82 dnd-post:
83 container_name: dnd-post
84 image: "{{ dnd_post_image }}"
85
86 restart: "no"
87 # Only started explicitly, so `docker compose up -d` brings up the STT
88 # service alone and never kicks off an hour of summarisation.
89 profiles: ["post"]
90
91 env_file:
92 - post.env
93
94 volumes:
95 - /etc/localtime:/etc/localtime:ro
96 - {{ dnd_media_dir }}:/media/dnd
97 # pyannote weights, kept out of the image so a rebuild does not refetch
98 # them and so the gated download happens exactly once.
99 - {{ dnd_model_dir }}:/models
100
101{% if gpu_enabled %}
102 runtime: nvidia
103 deploy:
104 resources:
105 reservations:
106 devices:
107 - driver: nvidia
108 count: 1
109 capabilities: [gpu]
110{% endif %}
111
112 networks:
113 {{ runner_docker_network }}:
114 aliases:
115 - dnd-post
116{% if dnd_table_enabled %}
117
118 # The table server: renders the kiosk page with headless Chromium and
119 # streams it to the Pi, which only hardware-decodes it. Always up, because
120 # bringing a video pipeline from cold at the start of a session costs a
121 # visible moment of mode negotiation -- the projector sleeps over CEC
122 # instead, which is instant.
123 dnd-table-runner:
124 container_name: dnd-table-runner
125 image: "{{ dnd_table_runner_image }}"
126 restart: unless-stopped
127
128 # Match the media directory's owner so uploaded handouts and rendered
129 # snapshots are readable from the NAS side. Root-owned files on a shared
130 # mount are a nuisance to clean up later.
131 user: "{{ dnd_uid }}:{{ dnd_gid }}"
132
133 environment:
134 TABLE_WIDTH: "{{ dnd_table_width }}"
135 TABLE_HEIGHT: "{{ dnd_table_height }}"
136 TABLE_FPS: "{{ dnd_table_fps }}"
137 TABLE_BITRATE_KBPS: "{{ dnd_table_bitrate_kbps }}"
138 TABLE_PI_HOST: "{{ dnd_table_pi_host }}"
139 TABLE_RTP_PORT: "{{ dnd_table_rtp_port }}"
140 TABLE_HTTP_PORT: "{{ dnd_table_http_port }}"
141 TABLE_MEDIA_ROOT: "/media"
142 TABLE_MQTT_HOST: "{{ mqtt_host }}"
143 TABLE_MQTT_PORT: "{{ mqtt_port }}"
144 TABLE_MQTT_USERNAME: "{{ mqtt_username }}"
145 TABLE_MQTT_PASSWORD: "{{ mqtt_password }}"
146 TABLE_BASE_TOPIC: "dnd"
147 # Chromium, Xvfb and lgpio all want a writable HOME. As a non-root user
148 # the image's /root is unreachable, and the failure is an opaque crash
149 # rather than a permission message.
150 HOME: "/tmp"
151 XDG_RUNTIME_DIR: "/tmp"
152 XDG_CACHE_HOME: "/tmp/.cache"
153 XDG_CONFIG_HOME: "/tmp/.config"
154
155 ports:
156 - "{{ dnd_table_http_port }}:{{ dnd_table_http_port }}"
157
158 volumes:
159 # Writable: the DM uploads handouts through the web UI mid-session, and
160 # rendered URL snapshots are written here too. Read-only was the right
161 # call when the library could only be filled from the NAS side.
162 - "{{ dnd_table_media_dir }}:/media"
163
164 # Chromium's renderer wants more than the 64MB default, and it fails in
165 # confusing ways rather than cleanly when it runs out.
166 shm_size: "512m"
167
168 healthcheck:
169 test: ["CMD", "python3", "-c",
170 "import urllib.request; urllib.request.urlopen('http://localhost:{{ dnd_table_http_port }}/api/state', timeout=5)"]
171 interval: "{{ health_check_interval }}"
172 timeout: "{{ health_check_timeout }}"
173 retries: {{ health_check_retries }}
174 start_period: "60s"
175
176 networks:
177 {{ runner_docker_network }}:
178 aliases:
179 - dnd-table-runner
180{% endif %}
181
182networks:
183 {{ runner_docker_network }}:
184 external: true
185