// DevOps

Cómo se organiza el despliegue y los paneles de control: phpMyAdmin y RabbitMQ detrás de nginx

Publicado el 22.09.2026

Cómo funciona el despliegue y los paneles de administración: phpMyAdmin y RabbitMQ detrás de nginx

La elección entre phpMyAdmin y Adminer para gestionar la base de datos vía navegador se analiza en el artículo «phpMyAdmin, Adminer o pgAdmin».

En esta nota desglosaremos el esquema práctico de despliegue de una aplicación mediante GitHub Actions y Docker Compose, así como la conexión de los paneles administrativos phpMyAdmin y RabbitMQ mediante un único nginx-gateway.

La idea principal es simple: hacia afuera solo apunta nginx, y todos los servicios internos — API, frontend, base de datos, phpMyAdmin, RabbitMQ y workers — permanecen en la red interna de Docker. Además, el despliegue está completamente automatizado: tras un push a main, el CI monta el entorno, sincroniza el código al servidor, reconstruye los contenedores, ejecuta migraciones y escala los workers.

Los dominios públicos, nombres de servidores y rutas en el artículo están anonimados. En los ejemplos se usan example.com, api.example.com, pma.example.com, rmq.example.com y la ruta condicionada /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 el tráfico HTTP/HTTPS y lo proxy hacia la red interna de Docker. Hacia fuera el servidor expone solo los puertos 80 y 443. Los paneles phpMyAdmin y RabbitMQ no publican sus puertos directamente hacia el exterior.


Parte 1. Qué sucede al hacer push en main

Paso 1. Construcción de .env

El archivo .env no se guarda en Git. Se crea de nuevo en cada despliegue directamente en el CI. Esto disminuye el riesgo de publicar accidentalmente secretos 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 operación, parámetros públicos;
  • secrets.* — contraseñas, tokens, claves privadas y cualquier valor que no deba mostrarse en los logs ni guardarse en el código.

Ejemplo de paso en el CI:

yaml
# 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

Un punto importante: GitHub enmascara los valores de secrets, pero no conviene imprimirlos intencionadamente en los logs. Incluso si los secretos se ocultan, es mejor diseñar el pipeline para que contraseñas y tokens no lleguen al stdout.

También hay que vigilar los caracteres especiales en las contraseñas. Si la contraseña contiene símbolos 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 como:

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 basic-auth para nginx. Son necesarios para proteger los paneles administrativos con un inicio de sesión y contraseña del navegador.

Para phpMyAdmin se usa un usuario separado:

bash
# 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 las del propio RabbitMQ:

bash
# Para RabbitMQ — se usan las mismas credenciales que el propio 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:

bash
openssl passwd -apr1 "password"

genera un hash en formato Apache MD5, también llamado apr1. Nginx puede leer esos 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 propios archivos deben añadirse a .gitignore:

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 de las contraseñas. Un hash no es la contraseña en claro, pero aun así no debe publicarse: puede ser objeto de ataques offline.


Paso 3. rsync al servidor

Tras preparar .env y .htpasswd el código se sincroniza al servidor.

Ejemplo de comando:

bash
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 eliminará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 vivir solo en el servidor no debe entrar en el directorio que rsync sobrescribe por completo. Por ejemplo, datos de runtime, uploads, datos de volúmenes de bases y archivos generados por la aplicación conviene almacenarlos fuera del directorio de sincronización o montarlos mediante Docker volumes.

En este caso se excluyen de la sincronización:

bash
--exclude='.git'
--exclude='api/vendor'
--exclude='frontend/node_modules'

Es lógico: .git no es necesario en producción, las dependencias de PHP y Node.js normalmente se instalan o se construyen dentro de las imágenes, y no se copian las carpetas locales del desarrollador.


Pasos 4–8. Despliegue en el servidor

Tras la sincronización, el CI entra en el servidor y ejecuta comandos de Docker Compose.

Primero se construyen las nuevas imágenes:

bash
# Construimos las nuevas imágenes. Los contenedores antiguos siguen corriendo en este momento.
docker compose -f docker-compose-production.yml build

Este paso no cambia la aplicación por sí mismo. Solo construye nuevas versiones de las imágenes. Los contenedores antiguos continúan funcionando hasta el comando up.

Luego se actualizan los contenedores:

bash
# Cambiamos 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 recrea los contenedores correspondientes. La opción --remove-orphans elimina contenedores de servicios que antes estaban en el compose file pero ya han sido eliminados de la configuración.

El downtime aquí suele ser mínimo, pero no hay que llamar a este esquema un despliegue con cero downtime completo. Si el contenedor del API se recrea y solo hay una instancia, puede producirse una breve interrupción. Para un verdadero zero-downtime hacen falta mecanismos adicionales: varias réplicas, healthchecks, switching de tráfico correcto y una estrategia cuidadosa de actualización.

Después de arrancar los contenedores se ejecutan las migraciones:

bash
# 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 el enfoque correcto: las migraciones se ejecutan en el mismo entorno que la aplicación, con las mismas variables de entorno y dependencias.

Antes de recargar nginx conviene comprobar la configuración:

bash
# Comprobamos la configuración de nginx
docker compose -f docker-compose-production.yml exec gateway nginx -t

# Volvemos a leer la configuración de nginx sin reiniciar el contenedor
docker compose -f docker-compose-production.yml exec gateway nginx -s reload

nginx -s reload recarga la configuración sin detener todo el contenedor. Pero si la configuración es inválida, el reload puede causar problemas. Por eso nginx -t antes del reload es una protección simple y útil.

Al final se escalan los workers:

bash
# 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 conveniente: se puede cambiar el número de réplicas sin recrear innecesariamente lo que ya funciona.


Parte 2. Cómo están conectados phpMyAdmin y RabbitMQ

Aislamiento de red

Todos los contenedores están en la red interna de Docker, que llamaremos backend por abreviar.

Hacia fuera están abiertos solo los puertos 80 y 443, y ambos los atiende 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 se accede a ellos a través de gateway.

Esto es un aspecto importante de seguridad. Incluso si alguien conoce el nombre interno del contenedor o el puerto estándar de RabbitMQ Management, no podrá acceder directamente desde internet.


Configuración de nginx para phpMyAdmin

Ejemplo de archivo pma.conf:

nginx
server {
    listen 443 ssl;
    server_name pma.example.com;

    # Basic auth — 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:

  1. El navegador va a https://pma.example.com.
  2. Nginx muestra el diálogo del navegador «introduzca usuario/contraseña».
  3. Nginx verifica los datos con el archivo /etc/nginx/auth/.htpasswd.
  4. Si los datos no coinciden — nginx devuelve 401 Unauthorized.
  5. Si los datos coinciden — nginx proxifica la petición al contenedor phpmyadmin en el puerto 80.

La directiva:

nginx
proxy_pass http://phpmyadmin;

funciona gracias al DNS de Docker. El nombre phpmyadmin debe coincidir con el nombre del servicio o alias de red 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 mostrará un error del tipo:

host not found in upstream "phpmyadmin"

Configuración de nginx para RabbitMQ

Ejemplo de archivo rmq.conf:

nginx
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;
    }
}

La lógica aquí es la misma que para phpMyAdmin, pero el upstream es otro:

nginx
proxy_pass http://rabbitmq:15672;

El puerto 15672 es la interfaz HTTP de RabbitMQ Management. Se usa para la interfaz web y la API HTTP de administración de RabbitMQ.

En el esquema descrito se usa la variante management de la imagen de RabbitMQ:

rabbitmq:3.13.2-management

Esa imagen ya trae activado el plugin de management, por lo que la interfaz web de RabbitMQ está disponible dentro de la red Docker en el puerto 15672.


Por qué para RabbitMQ hay autenticación en dos capas

RabbitMQ Management tiene su propia autenticación: el usuario introduce usuario y contraseña en el formulario de la UI de RabbitMQ Management.

Pero antes de eso está el basic auth de nginx.

Quedan, por tanto, dos capas:

Navegador
   │
   ▼
nginx basic auth
   │
   ▼
RabbitMQ Management auth
   │
   ▼
RabbitMQ Management UI

En la práctica el usuario hace dos inicios de sesión:

  1. Primero el diálogo basic-auth del navegador provisto por nginx.
  2. Después el formulario de autenticación del RabbitMQ Management.

¿Por qué?:

  • La UI de management de RabbitMQ no queda expuesta a internet sin una autorización previa de nginx.
  • Los escáneres y visitantes aleatorios no ven el formulario de RabbitMQ directamente.
  • Incluso si la contraseña de RabbitMQ es conocida, todavía hace falta pasar la capa externa de basic auth.

Importante: el basic auth de nginx no sustituye la autenticación de RabbitMQ. Es exactamente una capa adicional frente a la UI de management.


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 hay un certbot. Este comprueba la vigencia del certificado según un horario y actualiza los certificados automáticamente.

Nginx lee los certificados desde un volume 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 volume está montado de forma distinta.

El esquema general es:

certbot → actualiza certificados → shared volume → nginx lee certificados

Tras la renovación exitosa del certificado nginx debe volver a leer los certificados. En el esquema con contenedores esto suele resolverse de una de estas maneras:

  • hook de deploy en certbot que llama a reload de nginx;
  • script de mantenimiento separado;
  • comando de reload desde CI/CD;
  • reload periódico seguro de nginx tras comprobar la configuración.

La comprobación mínima del auto-renovado:

bash
certbot renew --dry-run

Si certbot corre en un contenedor, el comando se ejecutará con Docker Compose, por ejemplo:

bash
docker compose -f docker-compose-production.yml run --rm certbot renew --dry-run

Tras actualizar el certificado es útil comprobar qué certificado está sirviendo realmente nginx:

bash
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 ha actualizado el certificado pero nginx sigue sirviendo el certificado antiguo desde memoria.


Parte 4. Gestión manual de emergencia

El despliegue automático cubre el escenario habitual, pero siempre hace falta una vía clara de emergencia: cómo ver logs, reiniciar servicios, ejecutar migraciones o hacer un redeploy completo.

Redeploy completo

Si algo se ha roto, se puede ejecutar un workflow independiente manualmente:

GitHub → Actions → Full Redeploy → Run workflow

Ese workflow puede llamarse redeploy.yml.

Su objetivo no es solo actualizar los contenedores cambiados, sino reconstruir completamente el entorno:

bash
# 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 hay 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 el despliegue normal: puede causar mayor downtime, por lo que es mejor usarla solo como herramienta de emergencia.


Ver logs del gateway

Para ver los logs de nginx-gateway:

bash
docker compose -f docker-compose-production.yml logs -f gateway

Este comando sirve si:

  • el frontend no carga;
  • la API devuelve error a través de nginx;
  • phpMyAdmin no funciona;
  • la UI de RabbitMQ Management no abre;
  • hay sospecha de error de upstream;
  • nginx no puede resolver el contenedor por nombre;
  • no se aplicó la nueva configuración.

Errores típicos que se pueden ver 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:

bash
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 autenticación;
  • si las colas funcionan;
  • si hay problemas de disco o memoria.

Conectarse a MySQL manualmente desde la aplicación

Para una comprobación simple de la conexión a la base se puede ejecutar una consulta SQL desde el contenedor CLI de Symfony:

bash
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 la consulta.

Para diagnóstico esto suele ser más cómodo que entrar directamente en el contenedor de MySQL, porque se comprueba precisamente la vía de la aplicación hasta la base.


Qué es importante comprobar antes de publicar este esquema en producción

Esto no es una arquitectura nueva, sino una checklist para el esquema descrito. Ayuda a no pasar por alto detalles por los que el despliegue suele fallar en el peor momento.

1. .env no debe estar en Git

Comprueba el .gitignore:

gitignore
.env
.env.*

Si hacen falta plantillas, es mejor guardar solo un ejemplo:

.env.example

En él deben aparecer los nombres de las variables sin secretos reales.


2. Los archivos auth no deben llegar a Git

Comprueba:

gitignore
gateway/docker/production/nginx/auth/.htpasswd
gateway/docker/production/nginx/auth/.htpasswd_rmq

Se puede excluir todo el directorio:

gitignore
gateway/docker/production/nginx/auth/

Pero entonces hay que asegurarse de que el CI crea el directorio antes de escribir los archivos:

bash
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:

nginx
proxy_pass http://phpmyadmin;
proxy_pass http://rabbitmq:15672;

entonces el contenedor gateway debe ver los servicios phpmyadmin y rabbitmq mediante el DNS de Docker.

Se puede comprobar así:

bash
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 el problema está en las redes de Docker Compose: los servicios están en redes distintas o nginx no está conectado a la red necesaria.


4. Antes de recargar nginx hay que ejecutar nginx -t

Orden seguro:

bash
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 se debe hacer el reload. Primero hay que corregir la configuración.


5. Comprobar certbot con dry-run

El comando:

bash
certbot renew --dry-run

o la variante en contenedor:

bash
docker compose -f docker-compose-production.yml run --rm certbot renew --dry-run

debe ejecutarse con éxito después de la configuración inicial y tras cambios en nginx, DNS, firewall o reverse proxy.


6. Mejor no publicar RabbitMQ Management directamente

En este esquema la UI de management está disponible solo a través de nginx y basic auth. Esto es mejor que abrir el puerto 15672 al exterior mediante Docker ports.

Es decir, para entornos de producción conviene evitar una configuración como:

yaml
ports:
  - "15672:15672"

Si el puerto está publicado hacia fuera, la UI de RabbitMQ Management quedará accesible directamente, saltándose el basic auth de nginx.


Conclusión

El esquema gira alrededor de una idea simple: todo el tráfico externo pasa por un único nginx-gateway, y los servicios internos permanecen cerrados en la red de Docker.

Al hacer push a main, GitHub Actions:

  1. Crea .env a partir de vars y secrets.
  2. Genera .htpasswd para phpMyAdmin y RabbitMQ.
  3. Sincroniza el código al servidor mediante rsync.
  4. Construye las imágenes Docker.
  5. Actualiza los contenedores con Docker Compose.
  6. Ejecuta las migraciones.
  7. Vuelve a leer la configuración de nginx.
  8. Escala los workers.

phpMyAdmin y RabbitMQ no quedan expuestos directamente a internet. El acceso es únicamente a través de nginx:

pma.example.com → nginx basic auth → phpMyAdmin
rmq.example.com → nginx basic auth → RabbitMQ Management auth → RabbitMQ UI

Para producción es una solución clara y razonable: los secretos no están en Git, los paneles administrativos están protegidos con una capa adicional de autenticación, el SSL se renueva automáticamente y la gestión de emergencia sigue siendo posible mediante comandos y workflows manuales.

Lo principal es no olvidar las comprobaciones técnicas: .gitignore para secretos, nginx -t antes del reload, certbot renew --dry-run, no publicar 15672 al exterior y garantizar la red Docker correcta entre gateway, phpmyadmin y rabbitmq.

// Reviews

Reseñas relacionadas

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!

christ_media

Instalación de n8n en su servidor VPS. Configuración de n8n, Docker, IA, Telegram

24.09.2025 · ★ 5/5

Comprador experimentado

ladohinpy

Instalación de n8n en su servidor VPS. Configuración de n8n, Docker, IA, Telegram

25.08.2025 · ★ 5/5

// Contact

¿Necesitas ayuda?

Escríbeme y te ayudaré a resolver el problema

Respondo en un día laborable (03:00-13:00 GMT)

Или оставьте заявку здесь:

Confirme que no es un bot.

Escribir y recibir una respuesta rápida