Repository navigation
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
241 lines (227 loc) · 12.1 KB
/
Copy pathdocker-compose.yml
File metadata and controls
241 lines (227 loc) · 12.1 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
# =========================================================================
# Mantra Core Technologies · ALOVIDA — rama `mockup`
# -------------------------------------------------------------------------
# DOS servicios y sólo uno por defecto:
#
# web Producción. Un contenedor, construido desde `Dockerfile`, que sirve
# el artefacto ya compilado. Es lo que se despliega. NO monta fuentes.
#
# dev Desarrollo con recarga en caliente (`Dockerfile.dev` + `ng serve`).
# Vive bajo el perfil `dev`, así que no se crea salvo que se pida:
# docker compose --profile dev up dev
#
# ─── Por qué `web` es el de producción y no el de desarrollo ──────────────
#
# Porque este archivo es el que un desplegador toma por defecto. Coolify busca
# `/docker-compose.yml` salvo que se le diga otra cosa, y hasta el 06/09/2026
# lo que encontraba era el servicio de desarrollo: bind mounts de `./src` y
# `./angular.json` para la recarga en caliente. Coolify reescribe esas rutas
# relativas contra `/data/coolify/applications/<uuid>/`, donde el repositorio
# no está, así que Docker creaba `angular.json` como directorio vacío y el
# arranque moría con:
#
# error mounting ".../angular.json" to rootfs at "/app/angular.json":
# not a directory: Are you trying to mount a directory onto a file?
#
# Un servicio que monta el árbol de fuentes no es desplegable por definición.
# Bajo perfil no se crea, y con él no se crean sus montajes.
#
# ─── En un VPS a mano ─────────────────────────────────────────────────────
#
# git clone -b mockup https://github.com/mdavila-2001/mantra-core-health.git
# cd mantra-core-health
# cp deploy/mockup.env.example .env # y poner el dominio en APP_DOMAIN
# docker compose --env-file .env up -d --build
#
# → http://127.0.0.1:8080 (HTTP_PORT lo cambia)
#
# Sólo desde la propia máquina, a propósito: el puerto ya no se publica en
# todas las interfaces. Para llegar desde fuera hay que pasar por el proxy
# con su dominio y su TLS, que es el único camino que debería existir. Para
# mirarlo desde tu equipo sin proxy: `ssh -L 8080:127.0.0.1:8080 <host>`.
#
# ─── En Coolify ───────────────────────────────────────────────────────────
#
# Nuevo recurso → Docker Compose → este repositorio, rama `mockup`
# Compose file: se deja el de por defecto (`/docker-compose.yml`)
# Dominio: asignarlo al servicio `web` como `http://<ip-del-vps>:<puerto>`
# (NO un nombre de dominio). Dejar el campo vacío hace que Coolify invente
# uno propio (`*.sslip.io`); eso es justamente lo que se quiere evitar acá
# — el enrutado sigue pasando por el Traefik de Coolify, sólo que matchea
# por IP:puerto en vez de por nombre. Al ser IP y no dominio, Coolify no
# puede emitir certificado (Let's Encrypt exige un FQDN), así que este
# camino queda en HTTP plano, no HTTPS.
# Variable: APP_DOMAIN = la MISMA IP del campo Dominio, SIN esquema ni puerto
#
# APP_DOMAIN no es opcional. `AngularNodeAppEngine` responde 400 a cualquier
# `Host` que no conozca, y la lista horneada en el artefacto sólo trae
# `localhost`, `127.0.0.1` y `frontend`: sin esa variable el despliegue queda
# verde y el dominio devuelve 400. Con IP en vez de dominio, el `Host` que
# Traefik reenvía es la IP misma (sin puerto), así que APP_DOMAIN tiene que
# ser exactamente esa IP.
#
# ─── Qué levanta ──────────────────────────────────────────────────────────
#
# La aplicación ENTERA sin API, sin base de datos y sin buscador: la rama
# `mockup` trae un backend simulado dentro del propio frontend
# (`src/app/core/mock`), así que esto es UN SOLO CONTENEDOR y ninguna petición
# sale del navegador — las contesta el interceptor.
#
# medica@alovida.mock · paciente@alovida.mock · admin@alovida.mock
# superadmin@alovida.mock · visitador@alovida.mock — cualquier contraseña.
#
# Los datos viven en memoria del navegador: lo que se crea dura lo que dura la
# pestaña, y cada visitante ve el mismo punto de partida.
# =========================================================================
services:
web:
build:
context: .
dockerfile: Dockerfile
args:
# Vacía. Con el backend simulado da igual a dónde apunte —nada llega a
# la red—, y vacía deja la CSP en `connect-src 'self'`, que es lo mismo
# que en producción.
PUBLIC_API_BASE_URL: ''
# Explícito, con el mismo valor que el Dockerfile toma si no se le pasa:
# `production` es la maqueta (`mockBackend: true`), que es lo que este
# archivo despliega. `production-api` NO sirve acá: apaga el simulador
# y este compose no trae el proxy que lleva los prefijos a la API, así
# que cada llamada caería en el SSR. El front real de `test` se
# construye con `deploy/docker-compose.coolify.yml`, que ya fija
# `production-api`. La variable queda para una prueba local a propósito.
BUILD_CONFIGURATION: ${BUILD_CONFIGURATION:-production}
image: mantra-core-health-mockup
restart: unless-stopped
environment:
PORT: '4000'
NODE_ENV: production
PUBLIC_API_BASE_URL: ''
# Ver la nota de APP_DOMAIN arriba: sin esto el dominio devuelve 400.
SSR_ALLOWED_HOSTS: ${APP_DOMAIN:-localhost}
ports:
# Publicado SOLO en el bucle local del host, y el `127.0.0.1:` de delante
# es la mitad que importa.
#
# Con Traefik/Coolify delante esto no hace falta —enruta por la red de
# Docker al puerto expuesto—, y decir que «publicado tampoco molesta» era
# falso: sin ese prefijo Docker abre el puerto en TODAS las interfaces, y
# encima lo hace POR DEBAJO del firewall. Docker escribe sus reglas antes
# en la cadena, así que un `ufw deny 8080` no lo tapa: el puerto queda
# abierto a internet, sin TLS y saltándose el proxy, creyendo que está
# cerrado.
#
# Traefik corre en esta misma máquina y alcanza el bucle local igual de
# bien, así que no se pierde nada.
#
# Si Coolify tiene activado el despliegue sin cortes —levanta el
# contenedor nuevo antes de bajar el viejo—, este bloque sobra entero:
# quitarlo y dejar sólo `expose`, como hace
# `deploy/docker-compose.coolify.yml`.
- '127.0.0.1:${HTTP_PORT:-8080}:4000'
expose:
- '4000'
deploy:
resources:
limits:
# El renderizado en servidor mide ~400 MB en marcha; 768 MB deja aire
# para picos sin dejar que una fuga se coma el VPS.
memory: 768M
# El `HEALTHCHECK` viene en la imagen: pide `/auth`, que comprueba que el
# proceso responde Y que el artefacto del navegador está donde se lo busca.
# --- Desarrollo (perfil `dev`) ---------------------------------------
dev:
# Bajo perfil a propósito: `docker compose up` NO lo crea, y por tanto no
# crea sus bind mounts. Para levantarlo:
# docker compose --profile dev up dev
profiles: ['dev']
build:
context: .
dockerfile: Dockerfile.dev
container_name: mantra-core-health-dev
# Se levanta solo si muere. Sin esto el contenedor no tenía política ninguna
# (`RestartPolicy=no`): dos veces quedó `Exited (129)` —un SIGHUP, típico de
# que Docker Desktop se reinicie o el proceso padre desaparezca— y la página
# dejó de cargar sin que nadie se enterara hasta abrirla.
#
# `unless-stopped` y no `always` a propósito: si alguien lo para a mano
# —para liberar memoria antes de una demo, por ejemplo— tiene que quedarse
# parado, no resucitar solo.
restart: unless-stopped
# Techo de memoria del servidor de desarrollo.
#
# No es para proteger al contenedor: es para proteger **al resto**. Sin
# límite, `ng serve` en modo watch crece sin techo hasta dejar la máquina sin
# RAM, y quien muere primero no es él sino PostgreSQL — que se cayó y entró
# en recuperación con la demo a un día. Con un tope, el que se queda sin aire
# es el que lo gasta, y se reinicia solo.
#
# 3 GB dejó de alcanzar (2026-09-03): el build en frío de `ng serve` —el que
# corre sin caché en `.angular`, típicamente el primer arranque tras un
# `docker compose up` sobre un volumen nuevo— mide ~3,6 GB de pico (ng serve
# + esbuild + yarn como PID 1), y el kernel mataba el contenedor antes de
# que llegara a servir nada: loop de reinicio con "Building..." como único
# log. Medido corriendo la misma imagen sin techo. 6 GB deja margen sobre
# ese pico y sigue dejando sitio para la API y la base en una máquina de 16 GB.
mem_limit: 6g
environment:
# El heap de Node, por debajo del techo del contenedor: así el recolector
# empieza a trabajar **antes** de que el kernel mate el proceso. Al revés
# —heap por encima del límite— el contenedor muere sin que Node llegue a
# intentar liberar nada.
#
# 2 GB y no más porque el heap no es lo único que vive aquí dentro: medido
# en marcha son ~1,3 GB de `ng serve` + ~0,4 GB de esbuild + ~0,14 GB del
# `yarn` que hace de PID 1. Dejar el heap en 2,5 GB sumaba por encima del
# techo de 3 GB y el kernel habría matado el contenedor — justo lo que se
# intenta evitar.
NODE_OPTIONS: --max-old-space-size=2048
# A dónde reenvía el proxy las rutas de la API. El contenedor lo sustituye
# en la plantilla `proxy.conf.docker.json` al arrancar.
BACKEND_ORIGIN: ${BACKEND_ORIGIN:-http://host.docker.internal:3000}
# Raíz de la API que compila dentro del paquete del navegador. Se pasa el
# mismo valor que ve el host para que el `env.generated.ts` que escribe el
# contenedor sobre el bind mount de ./src coincida con el del host y no se
# pisen entre sí. Vacío = rutas relativas (lo normal: resuelve el proxy).
PUBLIC_API_BASE_URL: ${PUBLIC_API_BASE_URL:-}
# Compose lee `.env` de esta carpeta solo. Los `:-` son el valor por defecto:
# sin `.env`, todo funciona igual. Ver `.env.example`.
ports:
- '${FRONTEND_PORT:-4200}:4200'
# Comprobación real: que el servidor responda, no que el proceso exista.
# Un `ng serve` que se quedó sin memoria puede seguir vivo y no atender —
# que es justo el modo de fallo que nadie ve hasta abrir el navegador.
healthcheck:
# En bloque y no en línea: el script lleva comillas de los dos tipos y en
# una sola línea el YAML deja de ser válido.
test:
- CMD-SHELL
- >-
node -e "fetch('http://localhost:4200/').then(r =>
process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
interval: 30s
timeout: 10s
start_period: 120s
retries: 3
volumes:
# Se montan SOLO las fuentes y la configuración, nunca la raíz del
# proyecto: montar `.` taparía el .pnp.cjs y el .yarn/unplugged de linux
# que vive dentro de la imagen con los de macOS, y el build no arrancaría.
- ./src:/app/src
- ./public:/app/public
- ./angular.json:/app/angular.json:ro
- ./tsconfig.json:/app/tsconfig.json:ro
- ./tsconfig.app.json:/app/tsconfig.app.json:ro
# tsconfig.spec.json y vitest.config.ts quedan solo en la imagen: los usa
# `ng test`, no el dev server, y Docker Desktop falla al bind-montearlos.
# proxy.conf.docker.json NO se monta: viaja en la imagen y el contenedor
# genera desde ella `proxy.generated.json` con BACKEND_ORIGIN sustituido.
# El proxy.conf.json del repo queda intacto para el `yarn start` del host.
# Caché de Angular en volumen: sobrevive a los reinicios y no ensucia el host.
- angular-cache:/app/.angular
extra_hosts:
# Docker Desktop ya lo resuelve solo; explícito para que también funcione
# en Docker Engine sobre Linux.
- 'host.docker.internal:host-gateway'
volumes:
angular-cache: