# Operación de aplicaciones Docker detrás de Nginx

> Guía saneada y reproducible. Verificada el 3 de septiembre de 2026
> (America/Santiago). No contiene credenciales ni valores de entorno.

## 1. Arquitectura vigente

Nginx se ejecuta en el host y es la única entrada HTTP/HTTPS. Moodle, PHP-FPM,
PostgreSQL de Moodle y Ollama también viven en el host. Portainer, Uptime Kuma,
SOM y PostgreSQL de SOM viven en Docker.

```text
Internet -> UFW -> Nginx
                    |-- /             -> Moodle -> PHP-FPM -> PostgreSQL host
                    |-- som.minayao.site    -> 127.0.0.1:8000 -> SOM Docker
                    |-- locker.minayao.site -> 127.0.0.1:8081 -> Learning Locker
                    |-- yet.minayao.site    -> 127.0.0.1:8080 -> Yet SQL LRS
                    |-- kuma.minayao.site   -> 127.0.0.1:3002 -> Kuma Docker
                    `-- portainer.minayao.site -> 127.0.0.1:9444 -> Portainer

Ollama: 127.0.0.1:11434, privado en el host
SOM DB: red Docker interna, sin puerto publicado
```

Nginx termina TLS. Los puertos de los contenedores se enlazan exclusivamente a
`127.0.0.1`; publicar `0.0.0.0` omitiría esta capa de control.

## 2. Inventario y orden de carpetas

### Estado encontrado

| Ruta o recurso | Uso |
| --- | --- |
| `/opt/backendsom/app` | Repositorio y Compose productivo de SOM (ubicación heredada) |
| `/opt/server-architecture` | Arquitectura publicada en `doc.minayao.site` y `/arqui/` |
| `/opt/docker-projects/locker` | Compose reproducible de Learning Locker |
| `/opt/docker-projects/yet` | Compose reproducible de Yet Analytics SQL LRS |
| `/opt/som-docs` | Documentación propia de SOM |
| `/opt/django-backend-docs` | Documentación del backend Django |
| `/var/www/moodle` | Código Moodle tradicional, fuera de Docker |
| `/var/moodledata` | Datos Moodle, fuera del árbol público |
| `/var/lib/docker` | Estado administrado por Docker; no guardar repositorios aquí |

Se encontraron manifiestos Compose para SOM, Learning Locker y Yet SQL LRS.
Portainer y Kuma tienen persistencia, pero no se localizó un Compose versionado.

### Convención para aplicaciones nuevas

```text
/opt/apps/
  <nombre-del-proyecto>/
    .git/
    README.md
    Dockerfile                 # si se construye una imagen propia
    compose.yaml               # base común
    compose.prod.yaml          # ajustes del servidor, si son necesarios
    .env.example               # nombres y ejemplos no sensibles
    .env.prod                  # secretos locales; chmod 600; nunca en Git
    config/                    # configuración no secreta versionada
    scripts/                   # backup, restore y comprobaciones
    nginx/
      <nombre>.conf.example    # ejemplo versionado, no enlace activo
```

Reglas de nombres:

- proyecto Compose: nombre corto y estable, por ejemplo `inventario`;
- contenedores: dejar que Compose los nombre; evitar `container_name` salvo una
  dependencia justificada;
- redes: `<proyecto>_internal` y `<proyecto>_egress`;
- volúmenes: `<proyecto>_<funcion>`, por ejemplo `inventario_postgres_data`;
- snippet Nginx activo: `/etc/nginx/snippets/<proyecto>-proxy.conf`;
- repositorio: un proyecto por directorio en `/opt/apps`.

No se debe mover SOM sólo para adoptar la convención. Primero hay que documentar
la migración, respaldar, comprobar nombres de proyecto/volúmenes y evitar que
Compose cree recursos nuevos por un cambio de ruta o nombre.

## 3. Tipos de servicio

### A. Aplicación web sin base de datos

- un contenedor de aplicación;
- healthcheck;
- puerto publicado sólo en `127.0.0.1`;
- Nginx por ruta (`/app/`) o por nombre DNS;
- política `restart: unless-stopped`.

### B. Aplicación completa como SOM

- contenedor web en red interna y red de salida;
- PostgreSQL/Redis sólo en red interna;
- volumen nombrado para cada dato persistente;
- `depends_on` condicionado a salud cuando corresponda;
- migraciones controladas durante el despliegue;
- ningún puerto de base de datos publicado;
- backup y restauración probados antes de producción.

### C. Servicio de plataforma como Kuma

- volumen dedicado;
- backend en loopback;
- proxy Nginx con soporte WebSocket si la aplicación lo requiere;
- versión de imagen fijada y actualización deliberada;
- exportación o respaldo de la configuración.

### D. Consola privilegiada como Portainer

- montaje del socket Docker sólo cuando sea imprescindible;
- acceso restringido por VPN, red o lista de IP;
- autenticación fuerte y volumen respaldado;
- no tratarla como una aplicación web común: comprometerla equivale a controlar
  todos los contenedores del host.

## 4. Plantilla Compose recomendada

```yaml
name: ejemplo

services:
  web:
    build:
      context: .
    restart: unless-stopped
    env_file:
      - .env.prod
    ports:
      - "127.0.0.1:8100:8000"
    networks:
      - internal
      - egress
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/health/"]
      interval: 30s
      timeout: 5s
      retries: 5

  db:
    image: postgres:16.15
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - internal
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
      interval: 10s
      timeout: 5s
      retries: 10

networks:
  internal:
    internal: true
  egress: {}

volumes:
  postgres_data:
```

La base recibe únicamente sus variables. No conviene pasar el `.env` completo a
todos los servicios. En producción se recomienda fijar imágenes por versión y,
para máxima repetibilidad, registrar también el digest aprobado.

Antes de asignar `8100`, comprobar que esté libre:

```bash
ss -lntup
docker ps --format 'table {{.Names}}\t{{.Ports}}'
```

## 5. Integración con Nginx

### Publicación bajo una ruta

Ejemplo `/ejemplo/` hacia `127.0.0.1:8100`:

```nginx
location = /ejemplo {
    return 301 /ejemplo/;
}

location ^~ /ejemplo/ {
    proxy_pass http://127.0.0.1:8100;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}
```

La aplicación debe conocer su prefijo público y generar enlaces, cookies,
redirecciones y archivos estáticos bajo `/ejemplo/`. Si no soporta subrutas, usar
preferentemente un nombre DNS independiente.

### Publicación por nombre DNS

Crear un `server` propio cuando exista un dominio, certificado y DNS destinados
al servicio. Es la opción más limpia para aplicaciones que asumen vivir en `/`.

### Publicación por puerto HTTPS

El patrón usado por Kuma y Portainer funciona, pero requiere abrir otro puerto en
UFW y mantenerlo en el certificado/configuración. Para servicios nuevos se
prefieren rutas o nombres DNS. Los puertos administrativos deben restringirse.

Después de cada cambio:

```bash
nginx -t
systemctl reload nginx
curl -k -sS -o /dev/null -w '%{http_code}\n' \
  --resolve 169.58.128.68:443:127.0.0.1 \
  https://169.58.128.68/ejemplo/health/
```

Nunca recargar Nginx si `nginx -t` falla.

## 6. Procedimiento para agregar una aplicación

1. Asignar nombre, ruta pública, puerto loopback y propietario operacional.
2. Crear o clonar el repositorio en `/opt/apps/<proyecto>`.
3. Revisar README, Dockerfile y Compose antes de ejecutar código de terceros.
4. Copiar `.env.example` a `.env.prod`, completar secretos fuera de Git y aplicar
   permisos `600`.
5. Validar sin iniciar: `docker compose --env-file .env.prod -f compose.prod.yaml config` evitando
   publicar su salida si contiene valores sensibles.
6. Construir o descargar imágenes deliberadamente.
7. Iniciar con `docker compose --env-file .env.prod -f compose.prod.yaml up -d`.
8. Confirmar contenedores, healthchecks, redes, montajes y puertos.
9. Probar el backend directamente por loopback.
10. Instalar el snippet Nginx, ejecutar `nginx -t` y recargar.
11. Probar la URL pública, autenticación, estáticos, cargas y WebSockets.
12. Incorporar monitor en Kuma y documentar responsable/alertas.
13. Ejecutar y probar backup/restore de datos persistentes.
14. Registrar el servicio en la arquitectura canónica.

## 7. Flujo para replicar desde Git

El repositorio debe contener código, Dockerfile, Compose, configuración no
secreta, `.env.example`, migraciones, healthcheck y documentación. No debe contener
`.env.prod`, certificados, volcados, volúmenes ni credenciales.

```bash
git clone <repositorio> /opt/apps/<proyecto>
cd /opt/apps/<proyecto>
cp .env.example .env.prod
chmod 600 .env.prod
# Completar valores mediante el mecanismo seguro definido por el administrador.
docker compose --env-file .env.prod -f compose.prod.yaml config --quiet
docker compose --env-file .env.prod -f compose.prod.yaml pull
docker compose --env-file .env.prod -f compose.prod.yaml build --pull
docker compose --env-file .env.prod -f compose.prod.yaml up -d
docker compose --env-file .env.prod -f compose.prod.yaml ps
```

El alta Nginx es un paso del host y no debe hacerse automáticamente desde el
contenedor. Mantener un ejemplo en el repositorio y copiarlo/revisarlo de forma
explícita evita que un repositorio pueda reemplazar la entrada web del servidor.

## 8. Cambios y actualización segura

```text
backup -> git fetch/revisión -> pull/build -> up -d -> salud -> HTTP -> logs
```

- respaldar antes de migraciones destructivas;
- revisar cambios de imagen y esquema;
- evitar etiquetas flotantes como `latest`;
- usar `docker compose up -d`, no recrear volúmenes;
- conservar una versión anterior identificable para rollback;
- no ejecutar `docker system prune` como parte del despliegue;
- comprobar espacio en `/var/lib/docker` y destino de backups.

## 9. Checklist mínimo de aceptación

- [ ] Compose versionado y validado.
- [ ] Secretos fuera de Git y separados por servicio.
- [ ] Backend publicado sólo en `127.0.0.1`.
- [ ] Base y caché sin puertos del host.
- [ ] Healthchecks y política de reinicio.
- [ ] Volúmenes con nombres y propietario conocidos.
- [ ] Nginx válido y HTTPS probado.
- [ ] UFW sólo abre entradas justificadas.
- [ ] Monitorización y logs definidos.
- [ ] Backup y restauración probados.
- [ ] Versiones/digests y procedimiento de rollback documentados.
- [ ] Arquitectura actualizada sin secretos.

## 10. Pendientes del estado actual

1. Crear proyectos Compose reproducibles para Kuma y Portainer sin reemplazar los
   contenedores actuales hasta respaldar y ensayar la migración de sus volúmenes.
2. Restringir el acceso público a Portainer y añadir una comprobación de salud.
3. Definir respaldos para Moodle, las dos bases PostgreSQL y volúmenes Docker.
4. Revisar el volumen `app_postgres_prod`, actualmente sin consumidor evidente.
5. Normalizar gradualmente los repositorios Docker nuevos bajo `/opt/apps`.
6. Diseñar una conexión controlada para SOM -> Ollama; el loopback del contenedor
   no alcanza automáticamente `127.0.0.1:11434` del host.
