fase 3: stack docker-compose local del sistema modernizado
Compose con los 5 servicios: mongo:7 (replica set rs0 + mongo-init one-shot), redis (AOF), web Meteor 3.1 (Dockerfile multi-stage: builder debian con meteor-tool 3.1 -> server-deps alpine -> runtime node:22-alpine), notifications (dry-run) y node-red 4. Healthchecks (127.0.0.1, no localhost: busybox wget prefiere IPv6 y Meteor escucha IPv4), restart unless-stopped, logging rotado 3x10MB, secretos montados desde ./secrets (gitignored, solo .example versionado). .npmrc legacy-peer-deps para el ERESOLVE de las libs react viejas. Validado: los 5 servicios healthy y smoke REST byte-identico (12/12) contra la web dockerizada en :3200. RUNBOOK.md documenta arranque, smoke, backups y rollback. No toca infra de Comunes.
This commit is contained in:
parent
e3f3f4aa20
commit
197a59f641
10 changed files with 341 additions and 1 deletions
15
.dockerignore
Normal file
15
.dockerignore
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Contexto de build de la imagen web. El builder hace su propio
|
||||||
|
# `meteor npm install`; nada del host debe colarse.
|
||||||
|
.git
|
||||||
|
**/.git
|
||||||
|
.meteor/local
|
||||||
|
node_modules
|
||||||
|
smoke/node_modules
|
||||||
|
smoke/snapshots
|
||||||
|
plan-modernizacion
|
||||||
|
secrets
|
||||||
|
docker-compose.yml
|
||||||
|
RUNBOOK.md
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -17,3 +17,8 @@ output.json
|
||||||
settings-production-*.json
|
settings-production-*.json
|
||||||
settings-tests.json
|
settings-tests.json
|
||||||
*.json~
|
*.json~
|
||||||
|
|
||||||
|
# fase-3: secretos del stack docker-compose local (solo se versionan los .example)
|
||||||
|
secrets/*
|
||||||
|
!secrets/README.md
|
||||||
|
!secrets/*.example
|
||||||
|
|
|
||||||
5
.npmrc
Normal file
5
.npmrc
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Las libs react-* antiguas (react-bootstrap 0.31, react-confirm 0.1.16, ...)
|
||||||
|
# declaran peers de react 15/16 pero funcionan con react 18 (ver UPGRADE.md).
|
||||||
|
# npm >= 7 fallaría con ERESOLVE sin esto — aplica igual en dev y en el build
|
||||||
|
# de la imagen Docker.
|
||||||
|
legacy-peer-deps=true
|
||||||
|
|
@ -39,7 +39,9 @@ COPY --chown=node:node docker/web-entrypoint.sh /web-entrypoint.sh
|
||||||
RUN chmod +x /web-entrypoint.sh
|
RUN chmod +x /web-entrypoint.sh
|
||||||
USER node
|
USER node
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
# 127.0.0.1 (not localhost): busybox wget resolves localhost to ::1 first, but
|
||||||
|
# the Meteor server listens on IPv4 only -> the check would false-negative.
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=5 \
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=5 \
|
||||||
CMD wget -qO /dev/null "http://localhost:${PORT}/" || exit 1
|
CMD wget -qO /dev/null "http://127.0.0.1:${PORT}/" || exit 1
|
||||||
ENTRYPOINT ["/web-entrypoint.sh"]
|
ENTRYPOINT ["/web-entrypoint.sh"]
|
||||||
CMD ["node", "main.js"]
|
CMD ["node", "main.js"]
|
||||||
|
|
|
||||||
117
RUNBOOK.md
Normal file
117
RUNBOOK.md
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
# RUNBOOK — stack local dockerizado (fase 3, validación)
|
||||||
|
|
||||||
|
Stack Docker Compose del sistema modernizado de "Todos contra el fuego":
|
||||||
|
web Meteor 3.1 (Node 22), MongoDB 7 (replica set de un nodo), Redis (AOF),
|
||||||
|
`tcef-notifications` y node-red. **Es el entorno de validación local previo a
|
||||||
|
producción** — el despliegue real irá al ansible de Comunes (fase 3 del plan);
|
||||||
|
este compose no toca el proxy ni la infra compartida.
|
||||||
|
|
||||||
|
## Requisitos
|
||||||
|
|
||||||
|
- Docker + Docker Compose v2.
|
||||||
|
- El repo hermano `../tcef-notifications` (el compose construye su imagen desde ahí).
|
||||||
|
|
||||||
|
## Primer arranque
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Secretos (gitignorados; ver secrets/README.md)
|
||||||
|
cp settings-development.json secrets/meteor-settings.json
|
||||||
|
cp secrets/notifications.env.example secrets/notifications.env # MODE=dry-run
|
||||||
|
|
||||||
|
# 2. Construir y levantar (el primer build de la web tarda: descarga meteor-tool
|
||||||
|
# y compila el bundle; ~10-20 min en frío)
|
||||||
|
docker compose build
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# 3. Comprobar salud
|
||||||
|
docker compose ps # todo "healthy" / mongo-init "exited (0)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Servicios y puertos en el host:
|
||||||
|
|
||||||
|
| Servicio | Contenedor | URL / puerto |
|
||||||
|
|---|---|---|
|
||||||
|
| web Meteor | `tcef-stack-web` | http://localhost:3200 |
|
||||||
|
| node-red | `tcef-stack-node-red` | http://localhost:1880 |
|
||||||
|
| MongoDB 7 | `tcef-stack-mongo` | solo red interna (usar `docker exec`) |
|
||||||
|
| Redis | `tcef-stack-redis` | solo red interna |
|
||||||
|
| notificaciones | `tcef-stack-notifications` | sin puerto (worker) |
|
||||||
|
|
||||||
|
`mongo-init` es un one-shot: inicia el replica set `rs0` y fija
|
||||||
|
`db.migrations` a la versión 18 (los `up()` históricos ya son async, pero se
|
||||||
|
pinta v18 porque los datos reales de producción llegan pre-migrados).
|
||||||
|
|
||||||
|
> Nota healthcheck: los checks usan `127.0.0.1`, no `localhost`. El `wget` de
|
||||||
|
> busybox (alpine) resuelve `localhost` a `::1` (IPv6) primero, pero los
|
||||||
|
> servidores Meteor/node-red escuchan solo en IPv4 → un `localhost` daría
|
||||||
|
> unhealthy aunque la app responda perfectamente por el puerto mapeado.
|
||||||
|
|
||||||
|
## Smoke test REST (red de seguridad del contrato Flutter)
|
||||||
|
|
||||||
|
Debe ser **byte-idéntico** a los snapshots committeados:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
MONGO_CONTAINER=tcef-stack-mongo MONGO_SHELL=mongosh MONGO_PORT=27017 \
|
||||||
|
BASE_URL=http://localhost:3200 ./smoke/smoke.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Operación diaria
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose logs -f web # logs de un servicio (rotados: 3×10MB)
|
||||||
|
docker compose ps # estado + healthchecks
|
||||||
|
docker compose restart notifications # reiniciar un servicio
|
||||||
|
docker compose down # parar todo (los volúmenes persisten)
|
||||||
|
docker compose down -v # ⚠️ borra también mongo/redis/node-red
|
||||||
|
```
|
||||||
|
|
||||||
|
## Desplegar una nueva versión
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# web (tras cambios en el repo)
|
||||||
|
docker compose build web && docker compose up -d web
|
||||||
|
|
||||||
|
# notificaciones (tras cambios en ../tcef-notifications)
|
||||||
|
docker compose build notifications && docker compose up -d notifications
|
||||||
|
```
|
||||||
|
|
||||||
|
Verificar tras cada despliegue de la web: `docker compose ps` healthy + smoke
|
||||||
|
REST byte-idéntico (arriba).
|
||||||
|
|
||||||
|
## Backups y restauración (volúmenes locales)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Mongo: dump / restore de la db fuegos
|
||||||
|
docker exec tcef-stack-mongo mongodump --db fuegos --archive > fuegos.dump
|
||||||
|
docker exec -i tcef-stack-mongo mongorestore --archive --drop < fuegos.dump
|
||||||
|
|
||||||
|
# node-red: tar del volumen de datos
|
||||||
|
docker run --rm -v tcef-stack_nodered-data:/data -v "$PWD:/backup" alpine \
|
||||||
|
tar czf /backup/nodered-data.tgz -C /data .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
- **web**: `git checkout <commit-anterior>` + `docker compose build web && docker compose up -d web`.
|
||||||
|
(En producción se conservará la imagen anterior etiquetada; aquí basta git.)
|
||||||
|
- **notificaciones**: igual, desde `../tcef-notifications`. `KILL_SWITCH=1` en
|
||||||
|
`secrets/notifications.env` + `docker compose up -d notifications` detiene
|
||||||
|
todo envío al instante sin parar el servicio.
|
||||||
|
- **datos**: restaurar el último dump (arriba).
|
||||||
|
|
||||||
|
## Salvaguardas de notificaciones
|
||||||
|
|
||||||
|
`secrets/notifications.env` arranca en `MODE=dry-run` (no envía nada). La
|
||||||
|
progresión dry-run → shadow → canary → full está documentada en
|
||||||
|
`../tcef-notifications/docs/rollout.md` y **nunca** se salta pasos. En este
|
||||||
|
stack local no hay credenciales reales salvo que las pongas tú.
|
||||||
|
|
||||||
|
## Deuda conocida de este compose
|
||||||
|
|
||||||
|
- `tcef-notifications` no expone endpoint HTTP de salud → healthcheck por
|
||||||
|
proceso (`pgrep`). Añadir `/healthz` al servicio cuando toque.
|
||||||
|
- node-red corre la imagen oficial vacía (los flows de los bots viven en
|
||||||
|
shiva; su migración de datos es parte del cutover real de fase 3).
|
||||||
|
- El driver mongodb 3.7 de `tcef-notifications` (elegido por el Mongo 3.2 de
|
||||||
|
producción) conecta con Mongo 7 en pruebas locales, pero al hacer el cutover
|
||||||
|
real a Mongo 7 debe subirse a driver 6.x (y habilitar change streams).
|
||||||
125
docker-compose.yml
Normal file
125
docker-compose.yml
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
# Stack local modernizado de "Todos contra el fuego" (fase 3 — validación local).
|
||||||
|
# NO es el despliegue de producción: eso irá al ansible de Comunes (proxies,
|
||||||
|
# firewall, monitorización). Aquí se valida que todo el stack corre en Docker.
|
||||||
|
#
|
||||||
|
# Uso: ver RUNBOOK.md. Secretos: ./secrets/ (montados como ficheros, gitignored).
|
||||||
|
|
||||||
|
name: tcef-stack
|
||||||
|
|
||||||
|
x-logging: &default-logging
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
mongo:
|
||||||
|
image: mongo:7
|
||||||
|
container_name: tcef-stack-mongo
|
||||||
|
command: ["--replSet", "rs0", "--bind_ip_all"]
|
||||||
|
volumes:
|
||||||
|
- mongo-data:/data/db
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 12
|
||||||
|
start_period: 20s
|
||||||
|
restart: unless-stopped
|
||||||
|
logging: *default-logging
|
||||||
|
|
||||||
|
# One-shot: inicia el replica set (un nodo) y marca las migraciones de Meteor
|
||||||
|
# como aplicadas (v18) para que los up() históricos (aún síncronos) no corran.
|
||||||
|
mongo-init:
|
||||||
|
image: mongo:7
|
||||||
|
depends_on:
|
||||||
|
mongo:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: "no"
|
||||||
|
entrypoint:
|
||||||
|
- bash
|
||||||
|
- -ec
|
||||||
|
- |
|
||||||
|
mongosh --host mongo --quiet --eval 'try { rs.status().ok } catch (e) { rs.initiate({_id: "rs0", members: [{_id: 0, host: "mongo:27017"}]}) }'
|
||||||
|
# espera a que el nodo sea PRIMARY antes de escribir
|
||||||
|
for i in $$(seq 1 30); do
|
||||||
|
state=$$(mongosh --host mongo --quiet --eval 'try { rs.isMaster().ismaster } catch (e) { false }')
|
||||||
|
[ "$$state" = "true" ] && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
mongosh --host mongo --quiet fuegos --eval 'db.migrations.replaceOne({_id: "control"}, {_id: "control", version: 18, locked: false}, {upsert: true})'
|
||||||
|
echo "mongo-init: replica set + migrations OK"
|
||||||
|
logging: *default-logging
|
||||||
|
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: tcef-web:meteor3
|
||||||
|
container_name: tcef-stack-web
|
||||||
|
depends_on:
|
||||||
|
mongo-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
environment:
|
||||||
|
PORT: "3000"
|
||||||
|
ROOT_URL: http://localhost:3200
|
||||||
|
MONGO_URL: mongodb://mongo:27017/fuegos?replicaSet=rs0
|
||||||
|
METEOR_SETTINGS_FILE: /run/secrets/meteor-settings.json
|
||||||
|
volumes:
|
||||||
|
- ./secrets/meteor-settings.json:/run/secrets/meteor-settings.json:ro
|
||||||
|
ports:
|
||||||
|
- "3200:3000"
|
||||||
|
restart: unless-stopped
|
||||||
|
logging: *default-logging
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: tcef-stack-redis
|
||||||
|
command: ["redis-server", "--appendonly", "yes"]
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 6
|
||||||
|
restart: unless-stopped
|
||||||
|
logging: *default-logging
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
build:
|
||||||
|
context: ../tcef-notifications
|
||||||
|
image: tcef-notifications:local
|
||||||
|
container_name: tcef-stack-notifications
|
||||||
|
depends_on:
|
||||||
|
mongo-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
# Copia secrets/notifications.env.example -> secrets/notifications.env.
|
||||||
|
# Por defecto MODE=dry-run: no envía nada aunque haya credenciales.
|
||||||
|
env_file:
|
||||||
|
- ./secrets/notifications.env
|
||||||
|
restart: unless-stopped
|
||||||
|
logging: *default-logging
|
||||||
|
|
||||||
|
node-red:
|
||||||
|
image: nodered/node-red:4.0
|
||||||
|
container_name: tcef-stack-node-red
|
||||||
|
volumes:
|
||||||
|
- nodered-data:/data
|
||||||
|
ports:
|
||||||
|
- "1880:1880"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO", "/dev/null", "http://127.0.0.1:1880/"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
restart: unless-stopped
|
||||||
|
logging: *default-logging
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongo-data:
|
||||||
|
redis-data:
|
||||||
|
nodered-data:
|
||||||
14
docker/web-entrypoint.sh
Normal file
14
docker/web-entrypoint.sh
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Carga METEOR_SETTINGS desde un fichero montado (secretos fuera de la imagen).
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -n "${METEOR_SETTINGS_FILE:-}" ]; then
|
||||||
|
if [ ! -r "$METEOR_SETTINGS_FILE" ]; then
|
||||||
|
echo "ERROR: METEOR_SETTINGS_FILE=$METEOR_SETTINGS_FILE no existe o no es legible" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
METEOR_SETTINGS="$(cat "$METEOR_SETTINGS_FILE")"
|
||||||
|
export METEOR_SETTINGS
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
15
secrets/README.md
Normal file
15
secrets/README.md
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# secrets/ — secretos del stack local (docker-compose)
|
||||||
|
|
||||||
|
Los ficheros reales de este directorio están **gitignorados** — solo se
|
||||||
|
versionan los `*.example`. Los valores de producción viven en el repo privado
|
||||||
|
`tcef-private-config` / en shiva, nunca aquí.
|
||||||
|
|
||||||
|
Para la validación local:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# settings de la web Meteor (el compose lo monta como fichero en el contenedor)
|
||||||
|
cp ../settings-development.json meteor-settings.json
|
||||||
|
|
||||||
|
# entorno del microservicio de notificaciones (dry-run por defecto: no envía nada)
|
||||||
|
cp notifications.env.example notifications.env
|
||||||
|
```
|
||||||
14
secrets/meteor-settings.json.example
Normal file
14
secrets/meteor-settings.json.example
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"gmaps": { "key": "GMAPS_BROWSER_KEY", "serverKey": "GMAPS_SERVER_KEY" },
|
||||||
|
"public": {},
|
||||||
|
"private": {
|
||||||
|
"internalApiToken": "CAMBIA-ESTE-TOKEN (la API REST solo se registra si existe)",
|
||||||
|
"testMailer": false,
|
||||||
|
"debugMailer": false,
|
||||||
|
"isMailServer": true,
|
||||||
|
"fireIconUrl": "https://fuegos.comunes.org/fire-marker.png",
|
||||||
|
"ironPassword": "IRON_PASSWORD",
|
||||||
|
"cron": { "debug": false },
|
||||||
|
"proxies_count": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
28
secrets/notifications.env.example
Normal file
28
secrets/notifications.env.example
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# tcef-notifications en el stack local (docker-compose). Copia a
|
||||||
|
# notifications.env. Referencia completa: ../../tcef-notifications/.env.example
|
||||||
|
#
|
||||||
|
# SEGURO POR DEFECTO: MODE=dry-run no envía nada por ningún canal.
|
||||||
|
|
||||||
|
MODE=dry-run
|
||||||
|
KILL_SWITCH=0
|
||||||
|
OWNED_CHANNELS=fcm,email
|
||||||
|
MATCHER_MODE=off
|
||||||
|
|
||||||
|
# Infra del compose (nombres de servicio de docker-compose.yml)
|
||||||
|
MONGO_URL=mongodb://mongo:27017/fuegos?replicaSet=rs0
|
||||||
|
MONGO_DB=fuegos
|
||||||
|
REDIS_HOST=redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
POLL_INTERVAL_MS=10000
|
||||||
|
POLL_BATCH_SIZE=500
|
||||||
|
|
||||||
|
# Solo necesarios en canary/full (el dry-run no construye los clientes):
|
||||||
|
# FCM_SERVICE_ACCOUNT=/run/secrets/firebase-service-account.json
|
||||||
|
# FCM_PROJECT_ID=angular-cosmos-108908
|
||||||
|
# MAIL_URL=smtps://USER:PASS@smtp.host:465
|
||||||
|
# IRON_PASSWORD=
|
||||||
|
|
||||||
|
ROOT_URL=http://localhost:3200/
|
||||||
|
LOG_LEVEL=info
|
||||||
|
SERVICE_NAME=tcef-notifications
|
||||||
Loading…
Add table
Add a link
Reference in a new issue