Skip to main content

06. Infraestructura y Despliegue: MDX-Cloud

Esta sección especifica la arquitectura física, el entorno de ejecución y la estrategia de despliegue continuo para MDX-Cloud. El diseño maximiza la seguridad y la alta disponibilidad utilizando un esquema híbrido desacoplado entre Cloudflare y contenedores Docker.


06.1 Arquitectura Física de Despliegue

El sistema separa físicamente la entrega de la interfaz gráfica del procesamiento de la lógica transaccional y las consultas a la base de datos:

Especificaciones y Dimensionamiento del Servidor (VPS)

(Última modificación: 2 de Junio de 2026)

Aunque el proveedor de infraestructura final está por definirse, las características base requeridas para garantizar el desempeño de la base de datos y la orquestación de Docker son:

  • Sistema Operativo Oficial: Ubuntu Server 24.04 LTS (Elegido por su estabilidad nativa con Docker y el firewall ufw preinstalado).
  • Opciones de Proveedor (Por Definir):
    • Opción Costo/Rendimiento: Proveedores orientados a desarrolladores (ej. DigitalOcean, Hetzner Cloud) que ofrecen discos NVMe a bajo costo.
    • Opción Corporativa: AWS (EC2) o Microsoft Azure, en caso de requerir estricto cumplimiento normativo (Compliance).
  • Dimensionamiento Mínimo (Sizing): 4 vCPUs, 8 GB de Memoria RAM y 80 GB de Disco Duro SSD NVMe (La velocidad del disco es imperativa para PostgreSQL).

06.2 Configuración del Entorno de Servidor (Docker Compose)

Para garantizar un despliegue idéntico entre desarrollo y producción, el entorno del backend en el VPS se gestiona mediante el siguiente archivo de orquestación docker-compose.yml:

version: '3.8'

services:

# ─── Proxy Inverso (Punto de entrada único al VPS) ──────────────────────────
mdx-caddy:
image: caddy:2-alpine
container_name: mdx_caddy_proxy
restart: always
ports:
- "80:80" # Redirige automáticamente a HTTPS
- "443:443" # TLS — HTTP/2
- "443:443/udp" # HTTP/3 (QUIC)
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks:
- mdx-network
depends_on:
- mdx-backend

# ─── Backend .NET 10 (Solo accesible desde la red interna Docker) ────────────
mdx-backend:
image: bitone/mdx-cloud-api:latest
container_name: mdx_backend_api
restart: always
expose:
- "8080" # Solo expone el puerto internamente, NO al host
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://+:8080
- ConnectionStrings__DefaultConnection=Host=mdx-database;Port=5432;Database=mdx_cloud;Username=db_admin_bitone;Password=${DB_PASSWORD}
networks:
- mdx-network
depends_on:
- mdx-database

# ─── Base de Datos PostgreSQL (Invisible al exterior) ───────────────────────
mdx-database:
image: postgres:16-alpine
container_name: mdx_postgres_db
restart: always
expose:
- "5432" # Solo accesible desde mdx-network, jamás desde internet
environment:
- POSTGRES_DB=mdx_cloud
- POSTGRES_USER=db_admin_bitone
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- mdx-network

volumes:
postgres_data:
driver: local
caddy_data:
driver: local
caddy_config:
driver: local

networks:
mdx-network:
driver: bridge

⚠️ Nota de Seguridad: La contraseña de la base de datos se gestiona mediante la variable de entorno ${DB_PASSWORD} definida en un archivo .env local nunca versionado en Git. El backend ya no expone el puerto 443 directamente — Caddy es el único punto de entrada al VPS.

06.3 Estrategia de Seguridad e Integridad de Red

Para blindar la operación multisucursal (Sinaloa/Sonora) contra ataques externos e intercepciones de datos, se configuran tres candados de seguridad en la infraestructura:

A. Cifrado de Extremo a Extremo (TLS 1.3)

Toda petición HTTP viaja obligatoriamente encriptada. Cloudflare gestiona el certificado SSL en el borde público, y el tráfico que viaja desde Cloudflare hacia el VPS se autentica mediante un Certificado de Origen Criptográfico, impidiendo que cualquier atacante brinque la protección apuntando directo a la IP del servidor.

B. Firewall e Invisibilidad de la Base de Datos

El firewall del sistema operativo Linux VPS (ufw) se configura de forma restrictiva:

  • Puerto 80/443 (Abiertos): Únicamente para recibir el tráfico web seguro filtrado por Cloudflare.

  • Puerto 5432 (PostgreSQL - CERRADO AL EXTERIOR): La base de datos no expone puertos al internet público. Al estar amarrada a la red de Docker mdx-network de tipo bridge, el único ente en el universo digital capaz de hablarle a PostgreSQL es el contenedor de .NET 10.

C. Aislamiento de Almacenamiento Persistente

Los datos de PostgreSQL se escriben en un Volumen Docker Persistente (postgres_data) mapeado en el disco duro del VPS. Si el contenedor se detiene, se actualiza o se destruye para subir una nueva versión del backend desarrollada por Ángel o por ti, los medicamentos, lotes y estados de cuenta permanecen intactos y seguros en el almacenamiento físico del host Linux.


06.4 Estrategia de Almacenamiento de Recursos (Object Storage)

Para mantener el host VPS libre de carga masiva de archivos y proteger su ancho de banda, MDX-Cloud implementa un desacoplamiento absoluto de almacenamiento utilizando Cloudflare R2 (Almacenamiento compatible con el estándar API de S3).

⚠️ Regla de Rendimiento (Bypass del Servidor): Queda estrictamente prohibido utilizar el servidor de .NET como "proxy" o "puente" para subir o descargar archivos binarios (Imágenes, PDFs, XMLs). El servidor solo procesará metadatos JSON.

Para lograr esto de forma segura, el sistema implementa el patrón de URLs Pre-firmadas (Pre-Signed URLs):

  1. Flujo de Subida (Upload Directo):
    • La aplicación React solicita a la Web API (.NET 10) permiso para subir un archivo.
    • .NET valida la sesión y genera una URL Pre-firmada criptográficamente (válida por 5 minutos) usando el SDK de S3 apuntando a Cloudflare R2.
    • React utiliza esa URL para hacer un PUT binario directamente hacia Cloudflare R2, saltándose al servidor VPS. Finalmente, avisa a .NET para que guarde el nombre del archivo en la base de datos.
  2. Flujo de Descarga (Download Directo):
    • Cuando el usuario requiere visualizar una factura o imagen privada, React consulta a .NET.
    • .NET devuelve una URL Pre-firmada de Descarga válida por tiempo limitado.
    • React descarga el archivo directamente desde Cloudflare R2 hacia la memoria del navegador del cliente.
  3. Persistencia en Base de Datos: PostgreSQL jamás almacenará archivos en formato BYTEA o rutas locales del servidor. Únicamente guardará la cadena de texto (String) correspondiente a la clave (Key) del objeto almacenado en la nube.

Diagrama de Flujo: Upload Directo (Pre-Signed URL)


06.5 Disaster Recovery: PITR y Regla 3-2-1

Para un ERP, perder los datos de las operaciones diarias es inaceptable. Para erradicar el riesgo de perder transacciones (como sucede con los respaldos nocturnos tradicionales), PostgreSQL se configurará bajo el estándar de Point-in-Time Recovery (PITR) mediante WAL Archiving (Respaldos Transaccionales), gobernado por la doctrina internacional de ciberseguridad 3-2-1.

A. El Motor Transaccional (WAL Archiving)

El contenedor de base de datos utilizará una herramienta de Enterprise Backup (como pgBackRest o WAL-G) configurada para realizar dos acciones concurrentes:

  1. Base Backup (Respaldo Completo): Una instantánea total de la base de datos que se ejecuta cada domingo a las 02:00 AM.
  2. Streaming Transaccional (WAL): Cada Insert, Update o Delete es empaquetado en un archivo de log y enviado a la bóveda de respaldos cada 5 minutos.

💡 Beneficio de Negocio (Cero Pérdida de Datos): Si el servidor se destruye a las 4:37 PM, el sistema levantará el respaldo del domingo, le inyectará los logs transaccionales y "reproducirá" los movimientos hasta las 4:35 PM. Pérdida máxima garantizada: 5 minutos.

B. Topología de Distribución (La Regla 3-2-1)

La infraestructura distribuirá los respaldos en 3 capas para asegurar su supervivencia contra desastres naturales, ataques ransomware o bloqueos de cuentas en la nube:

  • 1. Medio Local (Copia Ultrasónica): Los archivos WAL recientes se retienen en una partición protegida del SSD del VPS. Esto permite recuperar una tabla borrada por error casi al instante sin depender del ancho de banda de internet.
  • 2. Nube Principal Off-Site (Copia de Supervivencia): El motor envía el streaming de transacciones en tiempo real hacia un bucket privado en Cloudflare R2 (mdx-backups-vault). Si el servidor completo se incendia o es secuestrado por Ransomware, los datos están a salvo geográficamente.
  • 3. Medio Frío / On-Premise (Copia de Inmunidad): Una tarea programada descarga mensualmente un condensado de la base de datos hacia un servidor físico NAS (Network Attached Storage) ubicado en la oficina corporativa de MDX, o hacia un servicio de preservación como Amazon S3 Glacier. Esto previene catástrofes si la cuenta de Cloudflare llegase a ser comprometida.

C. Simulacros de Restauración Obligatorios

Un respaldo que nunca se ha restaurado es solo un deseo. El calendario operativo del área de TI incluye un simulacro trimestral donde se levantará un clon del servidor de producción y se comprobará la integridad criptográfica de la reconstrucción de los logs transaccionales (PITR) hasta un segundo exacto en el tiempo.


06.6 Observabilidad y Telemetría

(Última modificación: 2 de Junio de 2026)

Para evitar operar el sistema a ciegas y anticiparse a interrupciones del servicio, la arquitectura integra una estrategia formal de observabilidad basada en el estándar PLG (Prometheus, Loki, Grafana) y apoyada por instrumentación de OpenTelemetry.

  1. Métricas de Infraestructura (Prometheus): Un recolector se encargará de raspar (scrape) información vital del servidor y contenedores en tiempo real, midiendo el consumo de CPU, saturación de Memoria RAM, uso de disco y la cantidad de conexiones activas hacia PostgreSQL.
  2. Logs Estructurados (Loki + Serilog): Los contenedores de .NET 10 emitirán sus logs forenses y errores no controlados hacia Loki mediante la librería Serilog, estructurados en formato JSON. Esto erradica la dependencia de leer archivos de texto locales.
  3. Visualización y Alertas (Grafana): Se configurará un panel central que actúa como el "tablero de control". Grafana evaluará continuamente la telemetría y disparará alertas tempranas a través de canales como Webhooks o Correo Electrónico si se detectan anomalías (ej. la memoria RAM supera el 85% o las consultas a la base de datos exceden los tiempos de tolerancia).

06.7 Políticas de Exposición de APIs (CORS y Rate Limiting)

(Última modificación: 2 de Junio de 2026)

Derivado de la topología de múltiples Frontends desacoplados (ERP Interno vs Portal B2B), la API de .NET 10 debe implementar políticas estrictas de control de tráfico a nivel de middleware para asegurar su resiliencia pública:

1. Control de Orígenes Cruzados (CORS)

Dado que las aplicaciones de React residirán en dominios distintos (ej. app.mdx-cloud.com para el ERP y clientes.mdx-cloud.com para el B2B), la API debe configurarse con políticas explícitas de CORS.

  • Regla de Negocio: El backend rechazará cualquier petición HTTP (OPTIONS, GET, POST, PUT, DELETE) cuyo encabezado Origin no provenga exactamente de las URLs oficiales registradas (ERP o B2B). Se prohíbe el uso de comodines (*) en el origen en producción.

2. Prevención de Ataques y Scrapeo (Rate Limiting)

Mientras que el ERP opera en un entorno de confianza (empleados), el Portal B2B expone endpoints al internet público (/api/b2b/*). Para mitigar ataques de fuerza bruta (ej. adivinar contraseñas de clientes) o robo automatizado de catálogo (Scraping), se implementará un candado de estrangulamiento.

  • Regla de Negocio: Todo endpoint expuesto al público debe estar protegido por un middleware de Rate Limiting (ej. AspNetCore.RateLimiting), limitando el número de peticiones por segundo por dirección IP. Si un actor supera el umbral (ej. 5 peticiones/segundo), el servidor retornará automáticamente el código HTTP 429 Too Many Requests protegiendo los recursos de la base de datos central.

06.8 Proxy Inverso: Caddy

(Última modificación: 9 de Junio de 2026)

El proxy inverso es el único punto de entrada al VPS desde el exterior. Actúa como guardia entre Cloudflare y los contenedores internos de Docker, gestionando la terminación TLS de origen, los encabezados de seguridad HTTP, el enrutamiento y el control de tráfico.

Decisión Arquitectónica: Por qué Caddy

Se seleccionó Caddy 2 sobre las alternativas Nginx y Traefik por las siguientes razones técnicas, alineadas con el perfil del proyecto:

CriterioCaddyNginxTraefik
TLS de Origen Cloudflare✅ Automático❌ Manual (certbot)✅ Automático
HTTP/3 (QUIC) nativo
Rate Limiting integrado✅ (v2.7+)❌ (módulo externo)
Complejidad de configBajaMediaAlta
Ajuste a monolito DockerIdealBuenoOver-engineering

HTTP/3 sobre QUIC es particularmente relevante para los Agentes de Ventas en campo operando con conexiones celulares inestables: QUIC mantiene la sesión de red ante cambios de IP móvil (handoff entre torres), reduciendo la latencia de reconexión frente a HTTP/2+TCP.


Configuración del Caddyfile

El archivo Caddyfile se monta como volumen de solo lectura (:ro) en el contenedor de Caddy. Define las reglas de enrutamiento, seguridad y control de tráfico para todos los dominios de MDX-Cloud:

# ──────────────────────────────────────────────────────────────────────────────
# MDX-Cloud — Caddyfile (Proxy Inverso Oficial)
# Caddy 2 — HTTP/2 + HTTP/3 habilitado por defecto
# ──────────────────────────────────────────────────────────────────────────────

# Snippet reutilizable: Headers de Seguridad HTTP (aplicados a todos los dominios)
(security_headers) {
header {
# Evita que el navegador interprete archivos con tipo MIME incorrecto
X-Content-Type-Options "nosniff"
# Impide que la app sea incrustada en iframes externos (Clickjacking)
X-Frame-Options "DENY"
# Fuerza HTTPS durante 1 año, incluyendo subdominios
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# Política de recursos permitidos por la SPA
Content-Security-Policy "default-src 'self'; script-src 'self'; connect-src 'self' https://api.mdx-cloud.com; img-src 'self' data: https://pub-*.r2.dev;"
# No filtrar el Referer a sitios externos
Referrer-Policy "strict-origin-when-cross-origin"
# Desactiva cabeceras que revelan información del servidor
-Server
-X-Powered-By
}
}

# ─── API Backend (ERP Interno) ────────────────────────────────────────────────
api.mdx-cloud.com {
import security_headers

# Rate Limiting: máximo 20 peticiones/segundo por IP para endpoints autenticados
# Los picos breves (bursts) de hasta 40 peticiones son tolerados
rate_limit {
zone api_zone {
key {remote_host}
events 20
window 1s
}
}

# Proxy inverso al contenedor .NET 10 (red interna Docker)
reverse_proxy mdx_backend_api:8080 {
# Propaga la IP real del cliente a .NET para auditoría
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
header_up X-Real-IP {remote_host}

# Health check interno: si .NET no responde, Caddy retorna 502
health_uri /health
health_interval 15s
health_timeout 5s
}
}

# ─── Redireccion HTTP → HTTPS (Automática en Caddy) ──────────────────────────
# Caddy redirige automáticamente el puerto 80 a 443. No requiere configuración adicional.

Flujo de Red Completo (Cloudflare → Caddy → .NET)


Terminación TLS: Dos Capas

El sistema opera con dos capas de TLS independientes, lo que garantiza que el tráfico nunca viaja en texto plano, ni siquiera entre Cloudflare y el VPS:

  1. TLS Público (Cloudflare → Cliente): Gestionado íntegramente por Cloudflare. El certificado es renovado automáticamente. El cliente final ve un candado verde con el dominio mdx-cloud.com.

  2. TLS de Origen (VPS → Cloudflare): Caddy gestiona automáticamente el certificado de origen emitido por Cloudflare (vía API Token), autenticando que el tráfico que llega al VPS proviene exclusivamente de Cloudflare y no de un atacante que apunte directamente a la IP del servidor.

💡 Resultado: Un atacante que descubra la IP pública del VPS e intente conectarse directamente verá rechazada su conexión, ya que el servidor solo acepta el certificado de origen de Cloudflare.


Variables de Entorno y Segredos

Caddy requiere el Token de API de Cloudflare para obtener el certificado de origen automáticamente. Este token nunca debe escribirse en el Caddyfile. Se gestiona mediante una variable de entorno en el docker-compose.yml:

# Fragmento del servicio mdx-caddy en docker-compose.yml
mdx-caddy:
environment:
- CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}

El archivo .env en el VPS (fuera del repositorio Git) contiene:

# .env — NO versionar, agregar a .gitignore
CLOUDFLARE_API_TOKEN=tu_token_de_cloudflare_aqui
DB_PASSWORD=contrasena_segura_aqui

Verificación de Instalación

Para validar que Caddy está funcionando correctamente después del despliegue:

# Verificar que el contenedor está corriendo
docker ps | grep mdx_caddy_proxy

# Ver logs en tiempo real
docker logs -f mdx_caddy_proxy

# Verificar la configuración del Caddyfile activo
docker exec mdx_caddy_proxy caddy validate --config /etc/caddy/Caddyfile

# Comprobar soporte HTTP/3 desde exterior
curl -I --http3 https://api.mdx-cloud.com/health