Arquitectura en Docker (Multi-Container & Zero Trust)
La implementación de Mañuti en Docker empaqueta toda la complejidad de la arquitectura distribuida dentro de un ecosistema de 10 contenedores orquestados con Docker Compose, divididos en 2 redes virtuales aisladas (frontend_net y backend_net) y respaldados por 4 volúmenes persistentes.
Gracias al diseño de "Golden Image" Polimórfica, los nodos de Aplicación (web01, web02, web03), el procesador de colas (worker) y el servidor de WebSockets (reverb) utilizan exactamente la misma imagen base compilada con PHP 8.5-FPM, Node.js 20 y Python 3 + Graphviz, diferenciando su función únicamente por el comando de arranque.
1. Diagrama de la Topología Dockerizada
flowchart TD
subgraph Capa1 ["CAPA 1: ACCESO PÚBLICO (frontend_net)"]
LB["Load Balancer Nginx (framedev_lb)<br/>:80 / :443"]
end
WEB["web01..web03<br/>(PHP 8.5-FPM)<br/>[Ambas Redes]"]
WORKER["worker<br/>(Horizon)<br/>[backend_net]"]
REVERB["reverb<br/>(WebSockets)<br/>[Ambas Redes]"]
LB -->|"FastCGI :9000"| WEB
LB -->|"FastCGI :9000"| WORKER
LB -->|"Proxy WSS /app :8080"| REVERB
subgraph Capa3 ["CAPA 3: PERSISTENCIA AISLADA (backend_net)"]
direction LR
MARIADB[("MariaDB<br/>:3306")]
REDIS[("Redis<br/>:6379")]
MONGODB[("MongoDB<br/>:27017")]
MINIO[("MinIO<br/>:9000")]
end
%% Conexiones hacia la capa de persistencia
WEB --> MARIADB
WEB --> REDIS
WEB --> MONGODB
WEB --> MINIO
WORKER --> MARIADB
WORKER --> REDIS
WORKER --> MONGODB
WORKER --> MINIO
REVERB --> REDIS
Estructura de Directorios del Proyecto Docker
manuti-app/
├── docker/
│ ├── nginx/
│ │ └── default.conf # Upstream web_cluster (web01..03) y reverb_cluster
│ └── php/
│ ├── Dockerfile # Golden Image (PHP 8.5 + Node 20 + Python 3 + Graphviz)
│ └── php.ini # Tuning OPcache JIT (256M) y límites de carga
├── logs/ # Logs mapeados (nginx, php, mysql, redis, mongo, minio)
├── src/ # Código fuente de Mañuti (Laravel)
│ └── .env # Variables apuntando a nombres de servicio internos
├── .dockerignore
└── docker-compose.yml # Orquestación de los 10 servicios, redes y volúmenes
2. Preparación del Host (Ubuntu LTS) e Instalación de Docker Engine
Ejecuta estos comandos en el servidor anfitrión limpio:
# 1. Actualizar el sistema e instalar prerrequisitos
sudo apt-get update && sudo apt-get upgrade -y
sudo apt-get install -y ca-certificates curl gnupg
# 2. Agregar la llave GPG oficial de Docker
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# 3. Registrar el repositorio oficial de Docker
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 4. Instalar Docker Engine y Docker Compose Plugin
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# 5. Habilitar y verificar
sudo systemctl enable --now docker
sudo docker --version
3. La "Golden Image" (docker/php/Dockerfile)
Esta imagen incluye PHP 8.5-FPM, las extensiones obligatorias para Horizon (pcntl) y Reverb (sockets), los drivers PECL para Redis y MongoDB, Node.js 20 para Vite, y el entorno Python 3 + Graphviz + Fuentes Tipográficas (fonts-liberation y fontconfig) para que el generador de mapas SVG renderice los textos correctamente.
FROM php:8.5-fpm
# 1. DEPENDENCIAS DEL SISTEMA, NODE.JS 20 Y EXTENSIONES PHP CORE
RUN apt-get update && apt-get install -y \
git curl zip unzip supervisor \
libpng-dev libjpeg-dev libfreetype6-dev libonig-dev \
libxml2-dev libzip-dev libicu-dev libcurl4-openssl-dev \
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y nodejs \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) \
pdo_mysql mbstring exif pcntl bcmath sockets gd zip soap intl \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# 2. MOTOR PYTHON 3 + GRAPHVIZ + FUENTES PARA MAPAS SVG
RUN apt-get update && apt-get install -y \
python3 python3-pip graphviz fonts-liberation fontconfig \
&& fc-cache -f && dot -c \
&& python3 -m pip install --no-cache-dir --break-system-packages \
graphviz pathspec pyparsing boto3 python-dotenv \
&& rm -rf /var/lib/apt/lists/*
# 3. DRIVERS PECL (REDIS & MONGODB)
RUN pecl install redis mongodb \
&& docker-php-ext-enable redis mongodb
# 4. COMPOSER
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# 5. DIRECTORIO DE TRABAJO Y PERMISOS (UID 1000)
WORKDIR /var/www/html
COPY ./src /var/www/html
RUN usermod -u 1000 www-data && groupmod -g 1000 www-data && \
mkdir -p /var/www/.composer && \
chown -R www-data:www-data /var/www /var/www/html && \
chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache
USER www-data
CMD ["php-fpm"]
4. Configuración de PHP para Producción (docker/php/php.ini)
memory_limit = 512M
max_execution_time = 300
upload_max_filesize = 100M
post_max_size = 100M
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.jit_buffer_size=100M
opcache.jit=1235
date.timezone = America/Mexico_City
expose_php = Off
5. Configuración del Balanceador Nginx (docker/nginx/default.conf)
# 1. CLUSTER DE NODOS WEB (PHP-FPM sobre puerto 9000)
upstream web_cluster {
server web01:9000;
server web02:9000;
server web03:9000;
keepalive 32;
}
# 2. CLUSTER DE REVERB (WEBSOCKETS sobre puerto 8080)
upstream reverb_cluster {
server reverb:8080;
}
server {
listen 80;
server_name localhost;
root /var/www/html/public;
index index.php index.html;
error_log /var/log/nginx/error.log warn;
access_log /var/log/nginx/access.log;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
# A) TRÁFICO HTTP NORMAL
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# B) TÚNEL WEBSOCKETS (/app -> Reverb)
location ~ ^/app/ {
proxy_pass http://reverb_cluster;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
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;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
# C) BALANCEO FASTCGI HACIA LOS 3 NODOS PHP
location ~ \.php$ {
fastcgi_pass web_cluster;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
fastcgi_read_timeout 300s;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
6. Archivo Maestro docker-compose.yml
Define los 10 contenedores, sus healthchecks y la segmentación en dos subredes (172.20.0.0/16 y 172.21.0.0/16):
services:
# 1. LOAD BALANCER (Nginx)
lb:
image: nginx:alpine
container_name: framedev_lb
restart: unless-stopped
ports:
- "80:80"
volumes:
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
- ./src:/var/www/html:ro
- ./logs/nginx:/var/log/nginx
depends_on:
- web01
- web02
- web03
- reverb
networks:
- frontend_net
# 2. NODOS WEB (01, 02, 03)
web01:
image: ghcr.io/activemediacdmx/manuti:latest
build:
context: .
dockerfile: ./docker/php/Dockerfile
container_name: framedev_web01
restart: unless-stopped
working_dir: /var/www/html
volumes:
- ./src:/var/www/html
- ./docker/php/php.ini:/usr/local/etc/php/conf.d/custom.ini
- ./logs/php:/var/log/php
environment: &app_env
APP_ENV: production
APP_DEBUG: "false"
DB_HOST: mariadb
DB_PORT: 3306
DB_DATABASE: framedev_db
DB_USERNAME: framedev
DB_PASSWORD: kls2qr824
REDIS_HOST: redis
REDIS_PORT: 6379
MONGODB_DSN: "mongodb://mongo:27017/framedev_logs"
AWS_ACCESS_KEY_ID: minioadmin
AWS_SECRET_ACCESS_KEY: tCT8H83o7U7Gm2
AWS_ENDPOINT: http://minio:9000
AWS_BUCKET: framedev-bucket
REVERB_HOST: reverb
REVERB_PORT: 8080
REVERB_SCHEME: http
networks:
- frontend_net
- backend_net
expose:
- "9000"
web02:
image: ghcr.io/activemediacdmx/manuti:latest
container_name: framedev_web02
restart: unless-stopped
working_dir: /var/www/html
volumes:
- ./src:/var/www/html
- ./docker/php/php.ini:/usr/local/etc/php/conf.d/custom.ini
environment: *app_env
networks:
- frontend_net
- backend_net
expose:
- "9000"
web03:
image: ghcr.io/activemediacdmx/manuti:latest
container_name: framedev_web03
restart: unless-stopped
working_dir: /var/www/html
volumes:
- ./src:/var/www/html
- ./docker/php/php.ini:/usr/local/etc/php/conf.d/custom.ini
environment: *app_env
networks:
- frontend_net
- backend_net
expose:
- "9000"
# 5. REVERB (WebSockets)
reverb:
image: ghcr.io/activemediacdmx/manuti:latest
container_name: framedev_reverb
restart: unless-stopped
working_dir: /var/www/html
command: php artisan reverb:start --host=0.0.0.0 --port=8080
volumes:
- ./src:/var/www/html
environment: *app_env
expose:
- "8080"
networks:
- frontend_net
- backend_net
depends_on:
- redis
# 6. HORIZON (Queue Worker)
worker:
image: ghcr.io/activemediacdmx/manuti:latest
container_name: framedev_worker
restart: unless-stopped
working_dir: /var/www/html
command: php artisan horizon
volumes:
- ./src:/var/www/html
environment: *app_env
networks:
- backend_net
depends_on:
- redis
- mariadb
# 7. MARIADB
mariadb:
image: mariadb:11.2
container_name: framedev_db
restart: always
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: framedev_db
MYSQL_USER: framedev
MYSQL_PASSWORD: kls2qr824
volumes:
- mariadb_data:/var/lib/mysql
ports:
- "3307:3306"
networks:
- backend_net
# 8. REDIS
redis:
image: redis:alpine
container_name: framedev_redis
restart: always
command: redis-server --appendonly yes
volumes:
- redis_data:/data
ports:
- "6378:6379"
networks:
- backend_net
# 9. MONGODB
mongo:
image: mongo:latest
container_name: framedev_mongo
restart: always
environment:
MONGO_INITDB_DATABASE: framedev_logs
volumes:
- mongo_data:/data/db
ports:
- "27018:27017"
networks:
- backend_net
# 10. MINIO (S3 Object Storage)
minio:
image: minio/minio
container_name: framedev_minio
restart: always
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: tCT8H83o7U7Gm2
volumes:
- minio_data:/data
ports:
- "9000:9000"
- "9001:9001"
networks:
- backend_net
networks:
frontend_net:
driver: bridge
backend_net:
driver: bridge
volumes:
mariadb_data:
redis_data:
mongo_data:
minio_data:
7. Despliegue desde GitHub Container Registry (GHCR) e Inicialización
7.1. Autenticación en ghcr.io y Extracción de ./src
# 1. Iniciar sesión con tu Personal Access Token (PAT)
echo "TU_PAT_DE_GITHUB" | sudo docker login ghcr.io -u manuelemiliano --password-stdin
# 2. Clonar la estructura de orquestación
git clone https://github.com/activemediacdmx/Manuti.git manuti-app
cd manuti-app
# 3. Extraer el código compilado desde la imagen oficial hacia ./src
sudo docker pull ghcr.io/activemediacdmx/manuti:latest
id=$(sudo docker create ghcr.io/activemediacdmx/manuti:latest)
sudo docker cp $id:/var/www/html ./src
sudo docker rm -v $id
sudo chown -R 1000:1000 src
7.2. Encendido del Stack y Configuración de MinIO / Migraciones
# 1. Crear carpetas de logs y levantar contenedores
mkdir -p logs/{nginx,mysql,redis,mongo,minio,php}
sudo chmod -R 777 logs
sudo docker compose up -d
# 2. Generar APP_KEY y ejecutar Migraciones + Seeders
sudo docker compose exec -u www-data web01 php artisan key:generate
sudo docker compose exec -u www-data web01 php artisan migrate --seed --force
# 3. Inicializar Bucket y políticas públicas en MinIO (usando contenedor efímero mc)
alias dmc='sudo docker run --rm -it --network manuti-app_backend_net minio/mc'
dmc alias set miframe http://minio:9000 minioadmin tCT8H83o7U7Gm2
dmc mb miframe/framedev-bucket
dmc anonymous set download miframe/framedev-bucket/avatars
dmc anonymous set download miframe/framedev-bucket/architecture
# 4. Compilar assets con la IP del host y limpiar caché en los 3 nodos web
sudo docker exec -u www-data framedev_web01 npm run build
for node in framedev_web01 framedev_web02 framedev_web03; do
sudo docker exec -u www-data $node php artisan optimize:clear
done
sudo docker compose exec -u www-data web01 php artisan horizon:terminate
8. Escalado Horizontal en Caliente y Creación de Imagen Hermética
8.1. Agregar un 4to Nodo Web (web04) sin Downtime
- Duplica el bloque
web03comoweb04endocker-compose.yml. - Agrega
server web04:9000;al bloqueupstream web_clusterendocker/nginx/default.conf. - Ejecuta:
sudo docker compose up -d sudo docker compose exec lb nginx -s reload
8.2. Generar y Publicar una Imagen Hermética (Sin Volúmenes Externos)
Una vez validado el contenedor web01 con todas sus dependencias y assets compilados:
# 1. Inyectar carpeta src actualizada dentro del contenedor y hacer commit
sudo docker cp ./src/. framedev_web01:/var/www/html/
sudo docker commit framedev_web01 ghcr.io/activemediacdmx/manuti:v12-hermetica-final
# 2. Estrategia de Doble Tag (Versión + Latest) y Push al Registry
sudo docker tag ghcr.io/activemediacdmx/manuti:v12-hermetica-final ghcr.io/activemediacdmx/manuti:latest
sudo docker push ghcr.io/activemediacdmx/manuti:v12-hermetica-final
sudo docker push ghcr.io/activemediacdmx/manuti:latest