-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
359 lines (354 loc) · 20 KB
/
Copy pathdocker-compose.yml
File metadata and controls
359 lines (354 loc) · 20 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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
services:
db:
image: postgres:16-alpine
container_name: eko-realestate-db
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-eko}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-eko_local_pass}
POSTGRES_DB: ${POSTGRES_DB:-eko_realestate}
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-eko}"]
interval: 10s
timeout: 5s
retries: 5
ports:
# 5434 — coexist with eko-db :5432 (sales prod) and eko-db-main :5433 (sales main dev)
- "127.0.0.1:5434:5432"
redis:
image: redis:7-alpine
container_name: eko-realestate-redis
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
ports:
# 6381 — coexist with eko-redis :6379 (sales prod) and eko-redis-main :6380 (sales main dev)
- "127.0.0.1:6381:6379"
# Ollama service is intentionally omitted by default — we use Kimi + MiniMax via HTTPS API
# (anthropic-messages protocol). If a future deploy wants on-prem LLM, uncomment and pull
# qwen2.5:14b. See docs/roadmap.md Phase 5 (single-customer install) for the rationale.
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: eko-realestate-backend
restart: unless-stopped
volumes:
# Uploaded clips and rendered pieces. A named volume, not a bind mount:
# the media survives image rebuilds and never lands inside the repo.
- content-media:/data/media
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
environment:
# Owns the schema: migrations, the login lookup and the background workers.
DATABASE_URL: ${DATABASE_URL}
# What the request path actually connects as — a role without BYPASSRLS, so
# the tenant policies bind. Pointing this at the owner would leave every
# isolation test green while isolating nothing.
DATABASE_URL_APP: ${DATABASE_URL_APP:?set it in .env — the RLS role, NOSUPERUSER NOBYPASSRLS, with a password that is not in this repo}
# Migration 015 creates the eko_app role and reads these HERE, because
# `alembic upgrade` runs inside this container. They were missing, so the
# role was always created with the literal in the repository while
# DATABASE_URL_APP carried whatever the installer generated — either the
# tenant-boundary role has a published password, or every request fails
# authentication while /health stays green. Both halves must agree.
APP_DB_PASSWORD: ${APP_DB_PASSWORD:?set it in .env; migration 024 sets the RLS role to this, so a default here is a password published in the repo}
APP_DB_ROLE: ${APP_DB_ROLE:-eko_app}
# The role RLS is deliberately bypassed with, for login lookups, the
# platform router and the org-list reads that run before any tenant is
# bound. Absent here, setting it in .env did nothing and the app silently
# kept using DATABASE_URL — which on this stack is a superuser, so FORCE
# ROW LEVEL SECURITY was the only thing standing between a bug and every
# tenant's rows. Same omission as APP_DB_PASSWORD, one variable over.
DATABASE_URL_BYPASS: ${DATABASE_URL_BYPASS:-}
REDIS_URL: ${REDIS_URL}
# LLM
LLM_PRIMARY: ${LLM_PRIMARY:-kimi}
LLM_FALLBACK: ${LLM_FALLBACK:-minimax}
KIMI_API_KEY: ${KIMI_API_KEY}
KIMI_BASE_URL: ${KIMI_BASE_URL:-https://api.kimi.com/coding}
KIMI_MODEL: ${KIMI_MODEL:-kimi-for-coding}
MINIMAX_API_KEY: ${MINIMAX_API_KEY}
MINIMAX_BASE_URL: ${MINIMAX_BASE_URL:-https://api.minimax.io/anthropic}
MINIMAX_MODEL: ${MINIMAX_MODEL:-MiniMax-M2.7}
GROQ_API_KEY: ${GROQ_API_KEY:-}
GROQ_BASE_URL: ${GROQ_BASE_URL:-https://api.groq.com/openai/v1}
GROQ_MODEL: ${GROQ_MODEL:-openai/gpt-oss-120b}
OLLAMA_ENABLED: ${OLLAMA_ENABLED:-false}
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://172.20.0.1:11434}
OLLAMA_MODEL: ${OLLAMA_MODEL:-gemma3:4b}
OLLAMA_TIMEOUT_SECONDS: ${OLLAMA_TIMEOUT_SECONDS:-120}
# Watchdog over the fallback above. The check is cheap and frequent; the
# alert is rare and capped. OPS_ALERT_FROM must be a sender on a domain
# verified with Resend — empty means alerts are logged, never sent.
LLM_MONITOR_INTERVAL_SECONDS: ${LLM_MONITOR_INTERVAL_SECONDS:-300}
OPS_ALERT_FROM: ${OPS_ALERT_FROM:-}
# WhatsApp Business Cloud API
WHATSAPP_ENABLED: ${WHATSAPP_ENABLED:-false}
WHATSAPP_SIMULATED: ${WHATSAPP_SIMULATED:-true}
WHATSAPP_VERIFY_TOKEN: ${WHATSAPP_VERIFY_TOKEN}
WHATSAPP_APP_SECRET: ${WHATSAPP_APP_SECRET}
WHATSAPP_ACCESS_TOKEN: ${WHATSAPP_ACCESS_TOKEN}
WHATSAPP_PHONE_NUMBER_ID: ${WHATSAPP_PHONE_NUMBER_ID}
# Email channel (Phase 3 — Resend) — SIMULATED by default; set RESEND_* for
# real delivery on Realtors' OWN dedicated subdomain (NEVER reuse Eko AI
# Main's biz.ekoaiautomation.com). See docs/setup-email.md.
EMAIL_SIMULATED: ${EMAIL_SIMULATED:-true}
RESEND_API_KEY: ${RESEND_API_KEY:-}
RESEND_FROM: ${RESEND_FROM:-Eko AI Realtors <noreply@realtors.ekoaiautomation.com>}
RESEND_WEBHOOK_SECRET: ${RESEND_WEBHOOK_SECRET:-}
# The panel's own host, so the notice can link to the lead. Same value as
# the frontend's NEXT_PUBLIC_PANEL_URL build arg below.
PANEL_URL: ${PANEL_URL:-}
# The operator's own copy of every new-lead notice. Not in any UI, so the
# agency cannot remove it by editing Settings. Empty = no copy.
OWNER_NOTICE_EMAIL: ${OWNER_NOTICE_EMAIL:-}
# SMS (Phase 9 — Twilio) — SIMULATED by default; set TWILIO_* for real SMS.
SMS_SIMULATED: ${SMS_SIMULATED:-true}
CAPTURE_REQUIRE_EMAIL: ${CAPTURE_REQUIRE_EMAIL:-true}
BOOKING_OFFERS_PAUSED: ${BOOKING_OFFERS_PAUSED:-false}
LANDING_EVENTS_ENABLED: ${LANDING_EVENTS_ENABLED:-true}
LANDING_EVENTS_RETENTION_DAYS: ${LANDING_EVENTS_RETENTION_DAYS:-90}
LANDING_SESSIONS_PER_DAY: ${LANDING_SESSIONS_PER_DAY:-20000}
TWILIO_ACCOUNT_SID: ${TWILIO_ACCOUNT_SID:-}
TWILIO_AUTH_TOKEN: ${TWILIO_AUTH_TOKEN:-}
TWILIO_API_KEY_SID: ${TWILIO_API_KEY_SID:-}
TWILIO_API_KEY_SECRET: ${TWILIO_API_KEY_SECRET:-}
TWILIO_PHONE_NUMBER: ${TWILIO_PHONE_NUMBER:-}
TWILIO_WEBHOOK_URL: ${TWILIO_WEBHOOK_URL:-}
TWILIO_MESSAGING_SERVICE_SID: ${TWILIO_MESSAGING_SERVICE_SID:-}
TWILIO_STATUS_CALLBACK_URL: ${TWILIO_STATUS_CALLBACK_URL:-}
# Voice (Phase 13 — VAPI) — SIMULATED by default; set VAPI_* for real calls.
VOICE_SIMULATED: ${VOICE_SIMULATED:-true}
VAPI_API_KEY: ${VAPI_API_KEY:-}
VAPI_WEBHOOK_SECRET: ${VAPI_WEBHOOK_SECRET:-}
VAPI_ASSISTANT_ID: ${VAPI_ASSISTANT_ID:-}
VAPI_PHONE_NUMBER_ID: ${VAPI_PHONE_NUMBER_ID:-}
# Discovery / lead search (Phase 12) — SIMULATED unless keys are set.
DISCOVERY_SIMULATED: ${DISCOVERY_SIMULATED:-true}
ATTOM_API_KEY: ${ATTOM_API_KEY:-}
YELP_API_KEY: ${YELP_API_KEY:-}
OUTSCRAPER_API_KEY: ${OUTSCRAPER_API_KEY:-}
SERPAPI_API_KEY: ${SERPAPI_API_KEY:-}
FILE_IMPORT_MAX_MB: ${FILE_IMPORT_MAX_MB:-25}
# Content Studio (v0.52+): clip uploads land on the media volume.
CONTENT_UPLOAD_MAX_MB: ${CONTENT_UPLOAD_MAX_MB:-95}
CONTENT_MEDIA_DIR: ${CONTENT_MEDIA_DIR:-/data/media}
CONTENT_STUDIO_ENABLED: ${CONTENT_STUDIO_ENABLED:-false}
CONTENT_CTA_URL: ${CONTENT_CTA_URL:-}
CONTENT_UTM_CAMPAIGN: ${CONTENT_UTM_CAMPAIGN:-video}
CONTENT_MAX_DRAFTS_PER_DAY: ${CONTENT_MAX_DRAFTS_PER_DAY:-3}
CONTENT_STUDIO_INTERVAL_SECONDS: ${CONTENT_STUDIO_INTERVAL_SECONDS:-3600}
CONTENT_RENDER_ENABLED: ${CONTENT_RENDER_ENABLED:-false}
CONTENT_RENDER_INTERVAL_SECONDS: ${CONTENT_RENDER_INTERVAL_SECONDS:-900}
# Publishing through Buffer (v0.65): YouTube, TikTok and Instagram.
BUFFER_SIMULATED: ${BUFFER_SIMULATED:-true}
BUFFER_ACCESS_TOKEN: ${BUFFER_ACCESS_TOKEN:-}
BUFFER_ORG_ID: ${BUFFER_ORG_ID:-}
BUFFER_CHANNEL_YOUTUBE: ${BUFFER_CHANNEL_YOUTUBE:-}
BUFFER_CHANNEL_TIKTOK: ${BUFFER_CHANNEL_TIKTOK:-}
BUFFER_CHANNEL_INSTAGRAM: ${BUFFER_CHANNEL_INSTAGRAM:-}
CONTENT_PUBLISH_ENABLED: ${CONTENT_PUBLISH_ENABLED:-false}
CONTENT_WINDOW_ALERT_ENABLED: ${CONTENT_WINDOW_ALERT_ENABLED:-true}
CONTENT_WINDOW_ALERT_INTERVAL_SECONDS: ${CONTENT_WINDOW_ALERT_INTERVAL_SECONDS:-3600}
CONTENT_WINDOW_ALERT_LEAD_DAYS: ${CONTENT_WINDOW_ALERT_LEAD_DAYS:-3}
CONTENT_PUBLISH_INTERVAL_SECONDS: ${CONTENT_PUBLISH_INTERVAL_SECONDS:-900}
YOUTUBE_DATA_API_KEY: ${YOUTUBE_DATA_API_KEY:-}
CONTENT_METRICS_ENABLED: ${CONTENT_METRICS_ENABLED:-false}
CONTENT_METRICS_INTERVAL_SECONDS: ${CONTENT_METRICS_INTERVAL_SECONDS:-21600}
CONTENT_PUBLISH_MAX_PER_DAY: ${CONTENT_PUBLISH_MAX_PER_DAY:-4}
CONTENT_PUBLIC_BASE_URL: ${CONTENT_PUBLIC_BASE_URL:-}
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID:-}
CONTENT_PUBLISH_MAX_SECONDS: ${CONTENT_PUBLISH_MAX_SECONDS:-90}
CONTENT_SCHEDULE_ENABLED: ${CONTENT_SCHEDULE_ENABLED:-true}
CONTENT_SLOT_YOUTUBE: "${CONTENT_SLOT_YOUTUBE:-12:30,20:30}"
CONTENT_SLOT_INSTAGRAM: "${CONTENT_SLOT_INSTAGRAM:-11:30,18:30}"
CONTENT_SLOT_TIKTOK: "${CONTENT_SLOT_TIKTOK:-08:30,17:30}"
CONTENT_SCHEDULE_LEAD_MINUTES: ${CONTENT_SCHEDULE_LEAD_MINUTES:-20}
CONTENT_ORG_ID: ${CONTENT_ORG_ID:-0}
# The render worker queue (v0.66). Empty token = the queue is closed.
RENDER_WORKER_ENABLED: ${RENDER_WORKER_ENABLED:-false}
RENDER_WORKER_TOKEN: ${RENDER_WORKER_TOKEN:-}
# Dashboard auth (Phase 11) — off by default (open demo); installer turns it on.
AUTH_ENABLED: ${AUTH_ENABLED:-false}
DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD:-}
AUTH_SECRET: ${AUTH_SECRET:-}
AUTH_TTL_HOURS: ${AUTH_TTL_HOURS:-168}
# Google Sign In (GIS) + admin-managed team access (v0.16.0).
# GOOGLE_ADMIN_EMAILS pins lockout-proof bootstrap admins; the rest of the
# access list is managed in Settings → Team (DB-backed).
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
GOOGLE_ADMIN_EMAILS: ${GOOGLE_ADMIN_EMAILS:-}
GOOGLE_ALLOWED_EMAILS: ${GOOGLE_ALLOWED_EMAILS:-}
GOOGLE_ALLOWED_DOMAIN: ${GOOGLE_ALLOWED_DOMAIN:-}
# Who runs the SaaS. The ONLY source of platform access: only these
# addresses reach /api/v1/platform. Empty means nobody can, which is the
# correct default — the shared DASHBOARD_PASSWORD is the agency's own
# password and deliberately cannot grant it.
# APP_VERSION is deliberately not passed. The application carries it in
# `backend/app/config.py`, which the release bumps. The copy that used to
# live here was thirty releases behind, and `${APP_VERSION:-}` would be
# worse still — an empty string overrides the real one.
PLATFORM_ADMIN_EMAILS: ${PLATFORM_ADMIN_EMAILS:-}
# Public signup into the demo org. The flag was advertised in /auth/me
# and read nowhere, so it could not turn signup off.
REGISTRATION_ENABLED: ${REGISTRATION_ENABLED:-true}
# Sign in with Apple — APPLE_CLIENT_ID is the Services ID, also the token
# `aud` the backend validates. Shares the same allow-list as Google.
APPLE_CLIENT_ID: ${APPLE_CLIENT_ID:-}
# Follow-ups / nurture (Phase 10) — in-process worker for post-visit + reminders.
FOLLOWUPS_ENABLED: ${FOLLOWUPS_ENABLED:-true}
FOLLOWUPS_INTERVAL_SECONDS: ${FOLLOWUPS_INTERVAL_SECONDS:-300}
# Discovery enrichment (Phase 12) — server-side worker that classifies leads.
ENRICHMENT_ENABLED: ${ENRICHMENT_ENABLED:-true}
ENRICHMENT_INTERVAL_SECONDS: ${ENRICHMENT_INTERVAL_SECONDS:-120}
# Listings / MLS (Phase 7) — SIMULATED by default; set RESO_* for a real feed.
LISTINGS_SIMULATED: ${LISTINGS_SIMULATED:-true}
LISTINGS_PROVIDER: ${LISTINGS_PROVIDER:-reso}
RESO_BASE_URL: ${RESO_BASE_URL:-}
RESO_ACCESS_TOKEN: ${RESO_ACCESS_TOKEN:-}
# MLS Grid / REcolorado replication (Phase 7b). Worker OFF until the feed exists.
RESO_ORIGINATING_SYSTEM: ${RESO_ORIGINATING_SYSTEM:-recolorado}
# 1000, matching Settings. It was 200 here from the original feature
# commit; a later fix titled "align RESO replication with the real MLS
# Grid contract" raised the Settings default to the documented $top cap
# and did not touch compose — so the alignment never reached a running
# install, which kept paging at 200 and making five times the requests
# against a quota-limited feed. Same class of bug as the eighteen above.
RESO_PAGE_SIZE: ${RESO_PAGE_SIZE:-1000}
RESO_MAX_PAGES: ${RESO_MAX_PAGES:-50}
LISTINGS_SYNC_ENABLED: ${LISTINGS_SYNC_ENABLED:-false}
LISTINGS_SYNC_INTERVAL_SECONDS: ${LISTINGS_SYNC_INTERVAL_SECONDS:-900}
APP_ENV: ${APP_ENV:-development}
DEBUG: ${DEBUG:-true}
# ─── Settings that .env.example documented and the container could not
# read. This list is enumerated by hand, which is exactly how eighteen of
# them went missing: `TURNSTILE_SECRET` (so the captcha accepted every
# submission without verifying), the entire Cal.com configuration (so a
# production install's calendar was silently simulated and could not be
# switched), CORS_ORIGINS, DEFAULT_TIMEZONE and even LOG_LEVEL, which the
# ROG had set in its .env and which was being ignored.
#
# `test_compose_env.py` now fails if a documented setting is not here, so
# the list cannot silently fall behind Settings again. Defaults below are
# generated from `Settings`, never typed from memory.
APP_NAME: ${APP_NAME:-Eko AI Realtors}
LOG_LEVEL: ${LOG_LEVEL:-INFO}
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3000,http://localhost:3004}
DEFAULT_TIMEZONE: ${DEFAULT_TIMEZONE:-America/Denver}
CALENDAR_SIMULATED: ${CALENDAR_SIMULATED:-true}
CALENDAR_PROVIDER: ${CALENDAR_PROVIDER:-calcom}
CALCOM_API_KEY: ${CALCOM_API_KEY:-}
CALCOM_BASE_URL: ${CALCOM_BASE_URL:-https://api.cal.com}
CALCOM_EVENT_TYPE_ID: ${CALCOM_EVENT_TYPE_ID:-}
DELIVERY_RETRY_ENABLED: ${DELIVERY_RETRY_ENABLED:-true}
DELIVERY_RETRY_INTERVAL_SECONDS: ${DELIVERY_RETRY_INTERVAL_SECONDS:-120}
LLM_TIMEOUT_SECONDS: ${LLM_TIMEOUT_SECONDS:-30.0}
LLM_MAX_TOKENS_DEFAULT: ${LLM_MAX_TOKENS_DEFAULT:-600}
RESO_MIN_REQUEST_INTERVAL_SECONDS: ${RESO_MIN_REQUEST_INTERVAL_SECONDS:-0.5}
VAPI_BASE_URL: ${VAPI_BASE_URL:-https://api.vapi.ai}
TURNSTILE_SECRET: ${TURNSTILE_SECRET:-}
WHATSAPP_GRAPH_API_VERSION: ${WHATSAPP_GRAPH_API_VERSION:-v20.0}
WHATSAPP_BUSINESS_ACCOUNT_ID: ${WHATSAPP_BUSINESS_ACCOUNT_ID:-}
ports:
# 8011 — coexist with eko-backend :8000 (sales prod), eko-pipeline :8002, eko-backend-main :8010 (sales main dev)
#
# Bound to 127.0.0.1, not to every interface. Docker publishes to
# 0.0.0.0 by default and, worse, writes its own iptables rules ahead of
# the host firewall — so on a public VPS `- "8011:8000"` puts the API on
# the internet even with ufw closed, and the operator has no way to see
# it from `ufw status`. On a laptop behind a home router that was
# invisible; on a machine with a public IP it is the whole surface.
#
# Nothing needs the published port from outside: cloudflared runs on the
# host and reaches this over loopback, which is exactly how the two other
# products on that VPS are served — neither publishes a single port.
# What this removes is reaching the box by its LAN or Tailscale IP. For a
# remote host the replacement is an SSH tunnel, which is what
# `docs/install.md` and `scripts/install.sh` now tell the operator:
# ssh -N -L 8011:127.0.0.1:8011 -L 3004:127.0.0.1:3004 <host>
#
# If a machine genuinely needs the port on a real interface, the escape
# hatch is a `docker-compose.override.yml` on THAT host — deliberate, and
# not something a git pull can hand to a public server by accident (it is
# gitignored). It MUST use the `!override` tag:
#
# services:
# backend:
# ports: !override
# - "8011:8000"
#
# Measured, because an earlier version of this comment got it wrong and
# said a plain override would do: without the tag Compose keeps the base
# entry and the override changes nothing at all — `docker compose config`
# still shows `host_ip: 127.0.0.1`. A comment that describes behaviour the
# tool does not have is worse than no comment.
- "127.0.0.1:8011:8000"
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
args:
# Client JS never hits this directly — it talks to a same-origin /api/...
# which next.config.js rewrites to INTERNAL_API_URL below.
# Unused by the app — `lib/api.ts` calls relative paths and
# `next.config.js` rewrites them internally — kept only so an older
# `.env` that still sets it cannot change the build.
NEXT_PUBLIC_API_URL: /api
NEXT_PUBLIC_CANONICAL_URL: ${NEXT_PUBLIC_CANONICAL_URL:-}
# Google Sign In client ID (public) — inlined into the bundle at build.
NEXT_PUBLIC_GOOGLE_CLIENT_ID: ${NEXT_PUBLIC_GOOGLE_CLIENT_ID:-}
# Sign in with Apple — Services ID + the return URL (popup origin must
# match the site origin). Both public; inlined into the bundle at build.
NEXT_PUBLIC_APPLE_CLIENT_ID: ${NEXT_PUBLIC_APPLE_CLIENT_ID:-}
NEXT_PUBLIC_APPLE_REDIRECT_URI: ${NEXT_PUBLIC_APPLE_REDIRECT_URI:-}
# Public capture form. Empty on a single-agency install; required once
# a second agency exists, or /contact posts no form key and is refused.
NEXT_PUBLIC_CAPTURE_FORM_KEY: ${NEXT_PUBLIC_CAPTURE_FORM_KEY:-}
NEXT_PUBLIC_TURNSTILE_SITE_KEY: ${NEXT_PUBLIC_TURNSTILE_SITE_KEY:-}
# Public landing content — documented in .env.example. These have to be
# listed here as well as in the Dockerfile: a NEXT_PUBLIC_* that compose
# never passes is inlined as empty at build, so the page renders blanks
# while the .env file looks perfectly filled in.
NEXT_PUBLIC_LANDING_ADVISORS: ${NEXT_PUBLIC_LANDING_ADVISORS:-}
NEXT_PUBLIC_LANDING_BROKERAGE: ${NEXT_PUBLIC_LANDING_BROKERAGE:-}
NEXT_PUBLIC_LANDING_ADDRESS: ${NEXT_PUBLIC_LANDING_ADDRESS:-}
NEXT_PUBLIC_LANDING_PHONE: ${NEXT_PUBLIC_LANDING_PHONE:-}
NEXT_PUBLIC_LANDING_SMS: ${NEXT_PUBLIC_LANDING_SMS:-}
NEXT_PUBLIC_LANDING_EMAIL: ${NEXT_PUBLIC_LANDING_EMAIL:-}
NEXT_PUBLIC_LANDING_BRAND: ${NEXT_PUBLIC_LANDING_BRAND:-}
NEXT_PUBLIC_LANDING_INSTAGRAM: ${NEXT_PUBLIC_LANDING_INSTAGRAM:-}
NEXT_PUBLIC_LANDING_YOUTUBE: ${NEXT_PUBLIC_LANDING_YOUTUBE:-}
NEXT_PUBLIC_LANDING_TIKTOK: ${NEXT_PUBLIC_LANDING_TIKTOK:-}
NEXT_PUBLIC_LANDING_YEARS: ${NEXT_PUBLIC_LANDING_YEARS:-}
NEXT_PUBLIC_LANDING_MARKETS: ${NEXT_PUBLIC_LANDING_MARKETS:-}
NEXT_PUBLIC_LANDING_PORTRAIT: ${NEXT_PUBLIC_LANDING_PORTRAIT:-}
NEXT_PUBLIC_LANDING_BOOKING_URL: ${NEXT_PUBLIC_LANDING_BOOKING_URL:-}
NEXT_PUBLIC_LANDING_TESTIMONIALS: ${NEXT_PUBLIC_LANDING_TESTIMONIALS:-}
# Host split — empty until denverhomestory.com moves to Cloudflare.
NEXT_PUBLIC_BRAND_URL: ${NEXT_PUBLIC_BRAND_URL:-}
NEXT_PUBLIC_PANEL_URL: ${NEXT_PUBLIC_PANEL_URL:-}
container_name: eko-realestate-frontend
restart: unless-stopped
depends_on:
- backend
environment:
# next.config.js reads this at server startup for its /api/* rewrite.
INTERNAL_API_URL: ${INTERNAL_API_URL:-http://backend:8000}
ports:
# 3004 — coexist with eko-frontend :3001, eko-frontend-pricing-v2 :3002, eko-frontend-main :3003
# Loopback only — same reasoning as the backend's 8011 above. The tunnel
# is the only thing that should reach this, and it lives on the host.
- "127.0.0.1:3004:3000"
volumes:
postgres-data:
content-media: