// DevOps
Cómo se organiza el despliegue y los paneles de control: phpMyAdmin y RabbitMQ detrás de nginx
Publicado el 29.05.2026
Cómo funciona el despliegue y paneles de administración: phpMyAdmin y RabbitMQ detrás de nginx
En esta nota analizaremos un esquema práctico de despliegue de una aplicación mediante GitHub Actions y Docker Compose, así como la conexión de paneles administrativos phpMyAdmin y RabbitMQ a través de un único nginx-gateway.
La idea principal es sencilla: hacia fuera solo apunta nginx, y todos los servicios internos — API, frontend, base de datos, phpMyAdmin, RabbitMQ y workers — permanecen en una red interna de Docker. Además, el despliegue está totalmente automatizado: después de un push a main el CI construye el entorno, sincroniza el código al servidor, recompone los contenedores, ejecuta migraciones y escala los workers.
Los dominios públicos, nombres de servidores y rutas en el artículo están anonimizados. En los ejemplos se usan example.com, api.example.com, pma.example.com, rmq.example.com y la ruta condicional /srv/project.
Esquema general
GitHub (push → main)
│
▼
GitHub Actions
┌─────────────────────────────┐
│ 1. Construcción de .env │
│ 2. Generación de .htpasswd │
│ 3. rsync → servidor │
│ 4. docker compose build │
│ 5. docker compose up -d │
│ 6. Migraciones │
│ 7. nginx -s reload │
│ 8. Escalado de workers │
└─────────────────────────────┘
│
▼
Servidor de la aplicación
┌──────────────────────────────────────────┐
│ gateway (nginx) ← única entrada │
│ ├── example.com → frontend │
│ ├── api.example.com → api-php-fpm │
│ ├── pma.example.com → phpmyadmin │
│ └── rmq.example.com → rabbitmq:15672 │
└──────────────────────────────────────────┘
Aquí gateway es el contenedor con nginx, que recibe tráfico HTTP/HTTPS y proxya las peticiones a la red interna de Docker. Hacia fuera el servidor solo tiene abiertos los puertos 80 y 443. Los paneles phpMyAdmin y RabbitMQ no exponen sus puertos directamente al exterior.
Parte 1. Qué ocurre al hacer push en main
Paso 1. Construcción de .env
El archivo .env no se guarda en Git. Se genera de nuevo en cada despliegue dentro del CI. Esto reduce el riesgo de publicar secretos accidentalmente en el repositorio y facilita la gestión de entornos.
Todos los secretos viven en GitHub Actions en la sección:
Repository → Settings → Secrets and variables → Actions
La lógica de separación es la siguiente:
vars.*— ajustes no secretos: nombres de servicios, dominios, modos de funcionamiento, parámetros públicos;secrets.*— contraseñas, tokens, claves privadas y cualquier valor que no deba mostrarse en logs ni almacenarse en el código.
Ejemplo de paso en el CI:
# deploy.yml
- name: Create .env
env:
MYSQL_USER: ${{ vars.MYSQL_USER }}
MYSQL_DATABASE: ${{ vars.MYSQL_DATABASE }}
MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }}
RABBITMQ_USER: ${{ vars.RABBITMQ_USER }}
RABBITMQ_PASSWORD: ${{ secrets.RABBITMQ_PASSWORD }}
RABBITMQ_VHOST: ${{ vars.RABBITMQ_VHOST }}
run: |
echo "DB_URL=mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@db:3306/${MYSQL_DATABASE}" >> .env
echo "MESSENGER_TRANSPORT_DSN=amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672/${RABBITMQ_VHOST}" >> .env
Punto importante: GitHub oculta los valores de secrets, pero no conviene volcarlos intencionadamente en los logs. Aunque los secretos estén enmascarados, es mejor diseñar el pipeline para que contraseñas y tokens no vayan a stdout.
También hay que prestar atención a los caracteres especiales en las contraseñas. Si una contraseña contiene caracteres significativos para una URL, debe codificarse correctamente, de lo contrario el DSN puede volverse inválido. Esto es especialmente relevante para cadenas de conexión del tipo:
mysql://user:password@db:3306/database
amqp://user:password@rabbitmq:5672/vhost
Paso 2. Generación de .htpasswd para los paneles
En esta misma etapa se crean los archivos de autenticación básica para nginx. Son necesarios para proteger los paneles administrativos con un login y contraseña en el navegador.
Para phpMyAdmin se usa un login separado:
# Para phpMyAdmin — usuario separado
mkdir -p gateway/docker/production/nginx/auth
echo "${PMA_AUTH_USER}:$(openssl passwd -apr1 "${PMA_AUTH_PASSWORD}")" \
> gateway/docker/production/nginx/auth/.htpasswd
Para RabbitMQ se usan las mismas credenciales que el propio RabbitMQ:
# Para RabbitMQ — se usan las mismas credenciales que RabbitMQ
mkdir -p gateway/docker/production/nginx/auth
echo "${RABBITMQ_USER}:$(openssl passwd -apr1 "${RABBITMQ_PASSWORD}")" \
> gateway/docker/production/nginx/auth/.htpasswd_rmq
El comando:
openssl passwd -apr1 "password"
genera un hash en el formato Apache MD5, también llamado apr1. Nginx puede leer estos hashes en archivos referenciados por la directiva auth_basic_user_file.
Los archivos quedan en el directorio:
gateway/docker/production/nginx/auth/
Después se sincronizan al servidor junto con el código.
No es necesario guardar estos archivos en Git. El directorio con los archivos auth o los archivos mismos deben figurar en .gitignore:
gateway/docker/production/nginx/auth/.htpasswd
gateway/docker/production/nginx/auth/.htpasswd_rmq
Si no, se puede cometer el error de commitear los hashes. Un hash no es la contraseña en claro, pero igualmente no debe publicarse: puede intentarse crackearlo de forma offline.
Paso 3. rsync al servidor
Tras preparar .env y .htpasswd el código se sincroniza al servidor.
Ejemplo de comando:
rsync -avz --delete \
--exclude='.git' \
--exclude='api/vendor' \
--exclude='frontend/node_modules' \
./ deploy@app-server:/srv/project/
La opción --delete significa que los archivos eliminados en la copia de trabajo del CI se borrarán también en el servidor. Esto es útil para una sincronización limpia, porque el servidor no se convierte en un almacén de archivos antiguos.
Pero --delete tiene una consecuencia importante: todo lo que deba residir solo en el servidor no debe estar dentro del directorio que rsync sobrescribe completamente. Por ejemplo, datos runtime, uploads, datos de volúmenes de la base y archivos creados por la aplicación es mejor mantenerlos fuera del directorio de sincronización o montarlos mediante Docker volumes.
En este caso se excluyen de la sincronización:
--exclude='.git'
--exclude='api/vendor'
--exclude='frontend/node_modules'
Esto tiene sentido: .git no es necesario en producción, las dependencias PHP y Node.js normalmente se instalan o construyen dentro de las imágenes, y no se copian como directorios locales del desarrollador.
Pasos 4–8. Despliegue en el servidor
Tras la sincronización el CI entra al servidor y ejecuta comandos de Docker Compose.
Primero se construyen las nuevas imágenes:
# Construimos nuevas imágenes. Los contenedores antiguos siguen funcionando en este momento.
docker compose -f docker-compose-production.yml build
Este paso por sí solo no cambia la aplicación en ejecución. Solo construye nuevas versiones de las imágenes. Los contenedores antiguos siguen funcionando hasta up.
A continuación se actualizan los contenedores:
# Intercambiamos los contenedores
docker compose -f docker-compose-production.yml up -d --remove-orphans
up -d arranca los contenedores en segundo plano. Si la configuración del servicio o la imagen han cambiado, Compose recreará los contenedores correspondientes. La opción --remove-orphans elimina contenedores de servicios que antes estaban en el compose pero que ahora ya no aparecen en la configuración.
El downtime suele ser mínimo, pero no conviene llamar a este esquema un despliegue de zero-downtime completo. Si el contenedor del API se recrea siendo la única instancia, puede producirse una breve interrupción. Para un zero-downtime real hacen falta mecanismos adicionales: varias réplicas, healthchecks, conmutación de tráfico adecuada y una estrategia de actualización cuidadosa.
Tras arrancar los contenedores se ejecutan las migraciones:
# Esperamos a MySQL y ejecutamos migraciones
docker compose -f docker-compose-production.yml run --rm api-php-cli \
php bin/console doctrine:migrations:migrate -n
Aquí se usa un contenedor CLI separado de la aplicación. Es la manera correcta: las migraciones se ejecutan en el mismo entorno que la aplicación, con las mismas variables de entorno y dependencias.
Antes de recargar nginx es mejor comprobar la configuración:
# Comprobamos la configuración de nginx
docker compose -f docker-compose-production.yml exec gateway nginx -t
# Recargamos la configuración de nginx sin reiniciar completamente el contenedor
docker compose -f docker-compose-production.yml exec gateway nginx -s reload
nginx -s reload recarga la configuración sin detener totalmente el contenedor. Pero si la configuración es inválida, el reload puede causar problemas. Por eso nginx -t antes del reload es una verificación sencilla y útil.
Al final se escalan los workers:
# Escalamos los workers
docker compose -f docker-compose-production.yml up -d \
--scale api-workers=5 \
--scale log-workers=1 \
--no-recreate
La opción --scale establece el número de contenedores para un servicio concreto. Por ejemplo, aquí se levantan cinco instancias de api-workers y una de log-workers.
La opción --no-recreate indica a Compose que no recree contenedores ya existentes si no es necesario. Para los workers esto es útil: se puede cambiar el número de réplicas sin recrear innecesariamente lo que ya está en marcha.
Parte 2. Cómo están conectados phpMyAdmin y RabbitMQ
Aislamiento de red
Todos los contenedores están en una red interna de Docker, llamémosla backend.
Hacia fuera solo están abiertos los puertos 80 y 443, y ambos los escucha gateway — el contenedor con nginx.
Internet → 443 → gateway (nginx) → internal Docker network
├── frontend
├── api-php-fpm
├── phpmyadmin:80
└── rabbitmq:15672
phpMyAdmin y RabbitMQ no tienen puertos publicados al exterior. Solo es posible acceder a ellos a través de gateway.
Esto es un punto de seguridad importante. Incluso si alguien conoce el nombre interno del contenedor o el puerto estándar de RabbitMQ Management, desde Internet no podrá alcanzarlo directamente.
Configuración de nginx para phpMyAdmin
Ejemplo de archivo pma.conf:
server {
listen 443 ssl;
server_name pma.example.com;
# Auth básica — solicita usuario/contraseña antes de cualquier petición
auth_basic "Admin Area";
auth_basic_user_file /etc/nginx/auth/.htpasswd;
location / {
proxy_pass http://phpmyadmin;
}
}
Cómo funciona:
- El navegador va a
https://pma.example.com. - Nginx muestra el diálogo del navegador «introduzca usuario/contraseña».
- Nginx compara los datos introducidos con el archivo
/etc/nginx/auth/.htpasswd. - Si los datos no coinciden — nginx devuelve
401 Unauthorized. - Si coinciden — nginx proxya la petición al contenedor
phpmyadminen el puerto80.
La línea:
proxy_pass http://phpmyadmin;
funciona gracias al DNS de Docker. El nombre phpmyadmin debe coincidir con el nombre del servicio o alias en Docker Compose, y el contenedor nginx debe estar en la misma red Docker.
Si nginx no está en la misma red, el nombre del contenedor no se resolverá, y nginx emitirá un error del tipo:
host not found in upstream "phpmyadmin"
Configuración de nginx para RabbitMQ
Ejemplo de archivo rmq.conf:
server {
listen 443 ssl;
server_name rmq.example.com;
auth_basic "Admin Area";
auth_basic_user_file /etc/nginx/auth/.htpasswd_rmq;
location / {
proxy_pass http://rabbitmq:15672;
}
}
Aquí la lógica es la misma que en phpMyAdmin, pero el upstream es otro:
proxy_pass http://rabbitmq:15672;
El puerto 15672 es la interfaz HTTP de RabbitMQ Management. Se usa para la UI web y la API HTTP de administración de RabbitMQ.
En el esquema descrito se usa la versión con management de la imagen de RabbitMQ:
rabbitmq:3.13.2-management
Esa imagen trae el plugin management activado, por lo que la interfaz web de RabbitMQ está disponible dentro de la red Docker en el puerto 15672.
Por qué en RabbitMQ hay autenticación en dos capas
RabbitMQ Management tiene su propia autenticación: el usuario introduce login y contraseña en la propia forma de la UI de RabbitMQ Management.
Pero antes de eso está el basic auth de nginx.
Quedan así dos capas:
Navegador
│
▼
auth básica de nginx
│
▼
autenticación de RabbitMQ Management
│
▼
RabbitMQ Management UI
En la práctica el usuario realiza dos inicios de sesión:
- Primero el diálogo basic-auth del navegador provisto por nginx.
- Luego el formulario de autenticación de RabbitMQ Management.
¿Para qué sirve esto?
- La UI de Management no está expuesta a Internet sin una autenticación previa en nginx.
- Escáneres y visitantes casuales no ven el formulario de RabbitMQ directamente.
- Incluso si la contraseña de RabbitMQ es conocida, hay que pasar la capa externa de basic auth.
Importante: el basic auth de nginx no reemplaza la autenticación de RabbitMQ. Es una capa adicional delante del management UI.
Parte 3. Certificados SSL
Los certificados SSL de Let’s Encrypt cubren todos los nombres públicos del proyecto:
example.com
www.example.com
api.example.com
rmq.example.com
pma.example.com
Junto a los contenedores principales corre certbot. Verifica la fecha de expiración de los certificados según calendario y renueva los certificados automáticamente.
Nginx lee los certificados desde un volumen compartido, por ejemplo:
/etc/letsencrypt/live/example.com/fullchain.pem
/etc/letsencrypt/live/example.com/privkey.pem
o desde otra ruta dentro del contenedor si el volumen está montado en otro lugar.
El esquema general es:
certbot → actualiza certificados → volumen compartido → nginx lee los certificados
Después de una renovación satisfactoria nginx debe recargar los certificados. Para ello hace falta un reload de nginx. En una arquitectura con contenedores normalmente se resuelve de una de estas formas:
- hook de deploy de certbot que ejecuta reload de nginx;
- script de mantenimiento separado;
- comando de reload dentro del CI/CD;
- reload periódico y seguro de nginx tras verificar la configuración.
La verificación mínima del auto-renovado:
certbot renew --dry-run
Si certbot corre en un contenedor, el comando se adaptará a Docker Compose, por ejemplo:
docker compose -f docker-compose-production.yml run --rm certbot renew --dry-run
Tras actualizar el certificado es útil comprobar cuál certificado está sirviendo realmente nginx:
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -dates -issuer -subject
Esto ayuda a detectar situaciones en las que certbot actualizó el certificado, pero nginx sigue sirviendo el certificado antiguo en memoria.
Parte 4. Gestión manual de emergencia
El despliegue automático cubre el flujo habitual, pero siempre hace falta una vía clara de emergencia: cómo ver logs, reiniciar servicios, ejecutar migraciones o hacer un redeploy completo.
Despliegue completo (redeploy)
Si algo se ha roto, se puede lanzar un workflow separado manualmente:
GitHub → Actions → Full Redeploy → Run workflow
Un workflow así puede llamarse redeploy.yml.
Su objetivo no es solo actualizar los contenedores cambiados, sino reconstruir completamente el entorno:
# Parar los contenedores
docker compose -f docker-compose-production.yml down
# Construir las imágenes desde cero
docker compose -f docker-compose-production.yml build --no-cache
# Levantar el entorno de nuevo
docker compose -f docker-compose-production.yml up -d --remove-orphans
Este modo es útil si se sospecha de una capa dañada en la imagen Docker, conflicto con contenedores antiguos o un estado incorrecto tras un despliegue fallido.
Pero es una operación más agresiva que un deploy normal: puede causar más downtime, por lo que conviene usarla como herramienta de emergencia.
Ver logs del gateway
Para ver los logs del nginx-gateway:
docker compose -f docker-compose-production.yml logs -f gateway
Este comando es útil si:
- el frontend no carga;
- la API devuelve error a través de nginx;
- phpMyAdmin no funciona;
- la UI de RabbitMQ Management no carga;
- se sospecha un error de upstream;
- nginx no encuentra el contenedor por nombre;
- no se aplicó la nueva configuración.
Errores típicos que se ven en los logs:
host not found in upstream
connect() failed
upstream timed out
SSL_do_handshake() failed
no user/password was provided for basic authentication
user "..." was not found in "/etc/nginx/auth/.htpasswd"
Ver logs de RabbitMQ
Para RabbitMQ:
docker compose -f docker-compose-production.yml logs -f rabbitmq
Este comando ayuda a comprobar:
- si RabbitMQ arrancó;
- si las aplicaciones se conectaron al broker;
- si se creó el vhost necesario;
- si hay errores de autorización;
- si las colas funcionan;
- si hay problemas de disco o memoria.
Acceder a MySQL manualmente a través de la aplicación
Para comprobar fácilmente la conexión a la base se puede ejecutar una consulta SQL usando el contenedor CLI de Symfony:
docker compose -f docker-compose-production.yml run --rm api-php-cli \
php bin/console doctrine:query:sql "SELECT 1"
Si el comando devuelve resultado, significa que:
- el contenedor CLI de la aplicación arranca;
- las variables de entorno se leen;
- el DSN a la base es correcto;
- MySQL es accesible desde la red Docker;
- Doctrine puede ejecutar una consulta.
Para diagnóstico esto suele ser más útil que entrar directamente al contenedor de MySQL, porque se comprueba precisamente la ruta de la aplicación hasta la base.
Qué es importante comprobar antes de publicar este esquema en producción
Lo siguiente no es una arquitectura nueva, sino una checklist para el esquema descrito. Ayuda a no pasar por alto detalles menores que suelen romper el despliegue en el momento menos oportuno.
1. .env no debe aparecer en Git
Comprueba .gitignore:
.env
.env.*
Si necesitas plantillas, mejor guardar solo un ejemplo:
.env.example
En él deben figurar los nombres de las variables sin secretos reales.
2. Los archivos de auth no deben aparecer en Git
Revisa:
gateway/docker/production/nginx/auth/.htpasswd
gateway/docker/production/nginx/auth/.htpasswd_rmq
Puedes excluir todo el directorio:
gateway/docker/production/nginx/auth/
Pero entonces asegúrate de que el CI crea el directorio antes de escribir los archivos:
mkdir -p gateway/docker/production/nginx/auth
3. Nginx debe estar en la misma red Docker que phpMyAdmin y RabbitMQ
Si en la configuración de nginx aparece:
proxy_pass http://phpmyadmin;
proxy_pass http://rabbitmq:15672;
entonces el contenedor gateway debe poder resolver los servicios phpmyadmin y rabbitmq mediante el DNS de Docker.
Puedes comprobarlo así:
docker compose -f docker-compose-production.yml exec gateway getent hosts phpmyadmin
docker compose -f docker-compose-production.yml exec gateway getent hosts rabbitmq
Si los nombres no se resuelven, normalmente es un problema de redes en Docker Compose: los servicios están en redes diferentes o nginx no está conectado a la red adecuada.
4. Antes de recargar nginx ejecutar nginx -t
Orden segura:
docker compose -f docker-compose-production.yml exec gateway nginx -t
docker compose -f docker-compose-production.yml exec gateway nginx -s reload
Si nginx -t falla, no hacer reload. Primero corregir la configuración.
5. Certbot debe probarse con dry-run
El comando:
certbot renew --dry-run
o la variante en contenedor:
docker compose -f docker-compose-production.yml run --rm certbot renew --dry-run
debe pasar correctamente tras la configuración inicial y después de cambios en nginx, DNS, firewall o reverse proxy.
6. Mejor no publicar RabbitMQ Management directamente
En este esquema la UI de management solo es accesible mediante nginx y basic auth. Es preferible a exponer el puerto 15672 al exterior con ports.
En producción conviene evitar algo como:
ports:
- "15672:15672"
Si el puerto está publicado al exterior, la UI de RabbitMQ Management será accesible directamente, por fuera del basic auth de nginx.
Resumen
El esquema se basa en la idea simple: todo el tráfico exterior pasa por un único nginx-gateway, y los servicios internos permanecen cerrados en la red Docker.
Al hacer push a main, GitHub Actions:
- Crea
.enva partir devarsysecrets. - Genera
.htpasswdpara phpMyAdmin y RabbitMQ. - Sincroniza el código al servidor vía
rsync. - Construye las imágenes Docker.
- Actualiza contenedores con Docker Compose.
- Ejecuta migraciones.
- Recarga nginx.
- Escala los workers.
phpMyAdmin y RabbitMQ no se exponen directamente a Internet. El acceso pasa solo por nginx:
pma.example.com → nginx basic auth → phpMyAdmin
rmq.example.com → nginx basic auth → RabbitMQ Management auth → RabbitMQ UI
Para producción es un esquema normal y sensato: los secretos no están en Git, los paneles administrativos están protegidos con una capa adicional de autenticación, SSL se renueva automáticamente y la gestión de emergencia sigue accesible mediante comandos y workflows manuales.
Lo esencial es no olvidar comprobaciones técnicas: .gitignore para los secretos, nginx -t antes del reload, certbot renew --dry-run, no publicar 15672 al exterior y asegurar la red Docker entre gateway, phpmyadmin y rabbitmq.
// Reviews
Reseñas relacionadas
La experiencia de colaboración dejó una impresión sumamente positiva, principalmente por el profesionalismo y el enfoque para resolver los problemas que surgieron.
La experiencia de colaboración dejó una impresión sumamente positiva, principalmente por el profesionalismo y el enfoque para resolver los problemas que surgieron.
Jitsi Meet: Zoom personal, configuración de Jitsi Meet en Docker y en VPS
11.11.2025 · ★ 5/5
Había que hacer funcionar n8n, Redis y la base de datos. Lo había encargado antes a otro proveedor; todo se rompía constantemente. Se lo encargué a Mijaíl y al día siguiente todo funcionó rápido, ¡como un reloj!
Había que poner en marcha n8n, redis y la base de datos. Contraté antes a otro proveedor, y todo se rompía constantemente. Lo encargué a Mikhail, y al día siguiente ¡todo empezó a funcionar rápido, como un reloj!
Instalación de n8n en su servidor VPS. Configuración de n8n, Docker, IA, Telegram
24.09.2025 · ★ 5/5
Gracias por el trabajo rápido y bien hecho. Todo se hizo de manera ágil y como debía ser!
Gracias por el trabajo rápido y bien hecho. Todo se hizo con prontitud y tal como se necesitaba!
Instalación de n8n en su servidor VPS. Configuración de n8n, Docker, IA, Telegram
06.09.2025 · ★ 5/5
Solución rápida al problema, ¡recomiendo a Mijaíl como profesional a todo el mundo! Intenté montar una configuración similar por mi cuenta y siguiendo consejos de IA, y acabé gastando mucho tiempo y dinero (por el tiempo de inactividad del servidor). Así que mi consejo final: acudan a profesionales, sale más barato =) Gracias a Mijaíl por su profesionalidad.
Solución rápida al problema, ¡recomiendo a Mikhail como profesional! Intenté montar una configuración similar por mi cuenta y siguiendo consejos de redes neuronales, y acabó siendo una pérdida de mucho esfuerzo y dinero …
Instalación de n8n en su servidor VPS. Configuración de n8n, Docker, IA, Telegram
25.08.2025 · ★ 5/5
Mijaíl configuró otro VPS. Rápida y profesionalmente sorteó ciertas limitaciones de los proveedores de hosting.
Mijaíl completó la configuración de otro VPS. De forma rápida y profesional, sorteando ciertas limitaciones impuestas por los proveedores de hosting.
Instalación de N8n en su servidor VPS. Configuración de n8n, Docker, IA, Telegram
12.08.2025 · ★ 5/5
¡Excelente trabajo, gracias! ¡Mijaíl es un profesional, lo recomiendo!
¡Excelente trabajo, gracias! Mikhail es un profesional en lo suyo, ¡lo recomiendo!
Instalación de n8n en su servidor VPS. Configuración de n8n, Docker, IA, Telegram
03.07.2025 · ★ 5/5
// Contact
¿Necesitas ayuda?
Escríbeme y te ayudaré a resolver el problema
// Related