1-3-Nginx + MySQL con Dockerfile y Docker Compose
Nginx + MySQL con Dockerfile y Docker Compose (continuación)
Este tutorial es la continuación directa del anterior. Antes servíamos una web estática con Nginx usando un Dockerfile. Ahora daremos un paso más: levantaremos dos servicios con Docker Compose:
Nginx (con nuestra web personalizada, construida con
Dockerfile)MySQL (para guardar una cadena de texto llamada
tarea)
La web de Nginx mostrará la tarea almacenada en MySQL, demostrando cómo dos contenedores se comunican dentro de una red interna de Docker.
Requisitos previos
Haber completado el tutorial anterior (Dockerfile con Nginx).
Docker instalado (
docker --version).Docker Compose disponible (
docker compose version).
Objetivo del proyecto
Al final tendrás:
Una imagen propia de Nginx construida con
Dockerfileque sirve tuindex.html.Un contenedor MySQL con una base de datos y una tabla que guarda la cadena
tarea.Un archivo
docker-compose.ymlque orquesta ambos servicios.La web accesible en
http://localhost:8080.
⚠️ Nota: Como
index.htmles estático, no puede consultar MySQL directamente desde el navegador. Para que la web muestre la tarea, usaremos un pequeño script en el arranque del contenedor Nginx que genere el HTML con el valor leído de MySQL. Lo veremos paso a paso.
Estructura final del proyecto
mi-stack-nginx-mysql/
├── docker-compose.yml
├── web/
│ ├── Dockerfile
│ ├── index.html
│ └── entrypoint.sh
└── init/
└── init.sqlPaso 1: Crear la estructura de carpetas
mkdir mi-stack-nginx-mysql
cd mi-stack-nginx-mysql
mkdir web initPaso 2: Crear el script de inicialización de MySQL
Este script se ejecuta la primera vez que arranca MySQL. Crea la base de datos, la tabla e inserta la cadena tarea.
nano init/init.sqlContenido:
CREATE DATABASE IF NOT EXISTS tareas_db;
USE tareas_db;
CREATE TABLE IF NOT EXISTS tareas (
id INT AUTO_INCREMENT PRIMARY KEY,
descripcion VARCHAR(255) NOT NULL,
creada_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO tareas (descripcion) VALUES ('tarea');Paso 3: Crear la web (HTML + Dockerfile + entrypoint)
3.1. El archivo index.html
Creamos un HTML base que luego el entrypoint.sh reemplazará con el valor real de la tarea.
nano web/index.htmlContenido:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Mi Web con Nginx + MySQL</title>
<style>
body { font-family: sans-serif; text-align: center; margin-top: 80px; }
h1 { color: #0db7ed; }
.tarea { font-size: 1.5em; color: #333; background: #f4f4f4; padding: 20px; display: inline-block; border-radius: 8px; }
</style>
</head>
<body>
<h1>¡Mi Web con Nginx + MySQL!</h1>
<p>La tarea guardada en la base de datos es:</p>
<div class="tarea">__TAREA__</div>
</body>
</html>💡 Fíjate en el marcador
__TAREA__. Elentrypoint.shlo reemplazará al arrancar el contenedor.
3.2. El script entrypoint.sh
Este script se ejecuta al iniciar el contenedor Nginx. Espera a que MySQL esté disponible, consulta la tarea y genera el HTML final.
nano web/entrypoint.shContenido:
#!/bin/sh
set -e
echo "Esperando a MySQL..."
until mysql -h db -u usuario -puserpass -e "SELECT 1" >/dev/null 2>&1; do
sleep 2
done
echo "MySQL listo. Consultando tarea..."
TAREA=$(mysql -h db -u usuario -puserpass -N -B -e "SELECT descripcion FROM tareas_db.tareas LIMIT 1;")
echo "Tarea obtenida: $TAREA"
# Reemplazar el marcador en el HTML
sed -i "s/__TAREA__/${TAREA}/g" /usr/share/nginx/html/index.html
echo "Arrancando Nginx..."
exec nginx -g "daemon off;"Dale permisos de ejecución:
chmod +x web/entrypoint.sh3.3. El Dockerfile
Ahora construimos una imagen basada en Nginx que:
Copia el
index.htmly elentrypoint.sh.Instala el cliente
mysql(para poder consultar la base).Usa el
entrypoint.shcomo punto de entrada.
nano web/DockerfileContenido:
FROM nginx:latest
# Instalamos el cliente de MySQL (para consultar la BD al arrancar)
RUN apt-get update && \
apt-get install -y default-mysql-client && \
rm -rf /var/lib/apt/lists/*
# Copiamos el HTML y el entrypoint
COPY index.html /usr/share/nginx/html/index.html
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 80
ENTRYPOINT ["/entrypoint.sh"]Explicación de cada instrucción
| Instrucción | Descripción |
|---|---|
FROM nginx:latest | Imagen base oficial de Nginx. |
RUN apt-get install default-mysql-client | Instala el cliente mysql dentro del contenedor. |
COPY index.html ... | Copia el HTML a la ruta que Nginx sirve. |
COPY entrypoint.sh ... | Copia el script de arranque. |
ENTRYPOINT ["/entrypoint.sh"] | Ejecuta nuestro script al iniciar el contenedor. |
Paso 4: Crear el docker-compose.yml
nano docker-compose.ymlContenido:
services:
db:
image: mysql:8.0
container_name: mysql_tareas
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: tareas_db
MYSQL_USER: usuario
MYSQL_PASSWORD: userpass
volumes:
- mysql_data:/var/lib/mysql
- ./init:/docker-entrypoint-initdb.d
networks:
- red_app
web:
build: ./web
container_name: nginx_web
restart: unless-stopped
ports:
- "8080:80"
depends_on:
- db
networks:
- red_app
volumes:
mysql_data:
networks:
red_app:Explicación de las partes clave
| Elemento | Descripción |
|---|---|
services.db | Servicio de MySQL. |
environment | Configura contraseñas, usuario y base de datos. |
volumes: ./init:/docker-entrypoint-initdb.d | Ejecuta init.sql en el primer arranque. |
services.web | Servicio de Nginx, construido desde ./web. |
build: ./web | En lugar de image:, aquí usamos build para construir desde el Dockerfile. |
ports: "8080:80" | Expone Nginx al host en el puerto 8080. |
depends_on: db | Arranca web después de db (no espera a que esté listo, por eso el entrypoint.sh reintenta). |
networks: red_app | Ambos servicios comparten la misma red y se ven por nombre (db, web). |
volumes: mysql_data | Persistencia de los datos de MySQL. |
💡 Clave: Dentro de la red de Docker, el servicio
webse conecta a MySQL usando el hostnamedb(el nombre del servicio), nolocalhost.
Paso 5: Levantar el stack
Desde la carpeta raíz del proyecto:
docker compose up -d --buildExplicación
| Parámetro | Descripción |
|---|---|
up | Crea y arranca los servicios. |
-d | En segundo plano. |
--build | Fuerza la construcción de la imagen de web (útil si cambiaste el Dockerfile). |
Salida esperada:
[+] Building 25.3s (10/10) FINISHED
[+] Running 4/4
✔ Network mi-stack-nginx-mysql_red_app Created
✔ Volume "mi-stack-nginx-mysql_mysql_data" Created
✔ Container mysql_tareas Started
✔ Container nginx_web StartedPaso 6: Verificar que todo funciona
6.1. Ver el estado de los servicios
docker compose psSalida esperada:
NAME IMAGE STATUS PORTS
mysql_tareas mysql:8.0 Up 30 seconds 3306/tcp, 33060/tcp
nginx_web mi-stack-nginx-mysql-web Up 28 seconds 0.0.0.0:8080->80/tcp6.2. Ver los logs del servicio web
docker compose logs webDeberías ver algo como:
Esperando a MySQL...
MySQL listo. Consultando tarea...
Tarea obtenida: tarea
Arrancando Nginx...6.3. Acceder a la web
Abre tu navegador en:
http://localhost:8080Deberías ver:
¡Mi Web con Nginx + MySQL!
La tarea guardada en la base de datos es:
tarea
🎉 ¡Has conectado Nginx con MySQL usando Docker Compose!
Paso 7: Verificar directamente en MySQL (opcional)
Si quieres comprobar que la tarea está en la base de datos:
docker compose exec db mysql -u usuario -puserpass -e "SELECT * FROM tareas_db.tareas;"Salida esperada:
+----+-------------+---------------------+
| id | descripcion | creada_en |
+----+-------------+---------------------+
| 1 | tarea | 2026-09-14 10:00:00 |
+----+-------------+---------------------+Paso 8: Cambiar la tarea y ver el cambio reflejado
Opción A: Cambiar el valor en MySQL y reiniciar web
docker compose exec db mysql -u usuario -puserpass -e "UPDATE tareas_db.tareas SET descripcion='nueva tarea' WHERE id=1;"
docker compose restart webRefresca http://localhost:8080 y verás la nueva tarea.
Opción B: Cambiar el init.sql y reiniciar todo (⚠️ borra datos)
# Editar init/init.sql con la nueva tarea
nano init/init.sql
docker compose down -v
docker compose up -d --build💡 Recuerda:
init.sqlsolo se ejecuta si el volumen está vacío.
Paso 9: Detener y limpiar
| Comando | Efecto |
|---|---|
docker compose stop | Detiene los contenedores (los datos persisten). |
docker compose down | Detiene y elimina contenedores y redes (los datos persisten en el volumen). |
docker compose down -v | Además elimina el volumen (⚠️ pierdes la base de datos). |
docker compose down --rmi all | Además elimina las imágenes construidas. |
Estructura final del proyecto (resumen)
mi-stack-nginx-mysql/
├── docker-compose.yml # Orquesta los servicios db y web
├── web/
│ ├── Dockerfile # Construye la imagen de Nginx personalizada
│ ├── index.html # HTML con marcador __TAREA__
│ └── entrypoint.sh # Consulta MySQL y reemplaza el marcador
└── init/
└── init.sql # Crea BD, tabla e inserta "tarea"Comparativa con el tutorial anterior
| Aspecto | Tutorial anterior | Este tutorial |
|---|---|---|
| Servicios | Solo Nginx | Nginx + MySQL |
| Imagen de Nginx | Dockerfile simple con COPY | Dockerfile con entrypoint y cliente MySQL |
| Orquestación | docker run manual | docker compose |
| Persistencia | No aplica | Volumen mysql_data |
| Red | Por defecto | Red red_app compartida |
| Contenido dinámico | HTML estático | HTML generado desde MySQL |
Bonus: Añadir Adminer para ver la BD desde el navegador
Agrega este servicio a tu docker-compose.yml:
adminer:
image: adminer:latest
container_name: adminer_tareas
restart: unless-stopped
ports:
- "8081:8080"
depends_on:
- db
networks:
- red_appLuego:
docker compose up -dAccede a http://localhost:8081 y conéctate con:
| Campo | Valor |
|---|---|
| Sistema | MySQL |
| Servidor | db |
| Usuario | usuario |
| Contraseña | userpass |
| Base de datos | tareas_db |
Comandos útiles de Docker Compose
| Comando | Descripción |
|---|---|
docker compose up -d --build | Levanta todo y reconstruye imágenes. |
docker compose ps | Estado de los servicios. |
docker compose logs -f web | Logs en tiempo real del servicio web. |
docker compose exec db bash | Shell dentro del contenedor de MySQL. |
docker compose restart web | Reinicia solo el servicio web. |
docker compose down -v | Elimina todo incluyendo volúmenes. |
docker compose config | Valida el YAML. |
Conclusión
Has aprendido a:
Construir una imagen propia de Nginx con un
Dockerfileque instala el cliente MySQL.Orquestar dos servicios (Nginx + MySQL) con Docker Compose.
Comunicar contenedores a través de una red interna usando el nombre del servicio.
Persistir datos en MySQL con un volumen.
Inicializar la base de datos con un script SQL automático.
Generar contenido dinámico en Nginx leyendo la tarea desde MySQL.
Gestionar el ciclo de vida del stack con
docker compose.
Este patrón (frontend + base de datos) es la base de prácticamente cualquier aplicación web moderna en contenedores. A partir de aquí puedes añadir un backend (Node, Python, PHP), cache (Redis), etc. 🚀
Próximos pasos sugeridos
Añadir un backend (Node.js/Python) que exponga una API REST y reemplace el
entrypoint.shpor una llamadafetchdesde el navegador.Usar un archivo
.envpara no hardcodear contraseñas.Añadir healthchecks en
docker-compose.ymlpara quedepends_onespere de verdad a que MySQL esté listo.Explorar Docker Compose Watch para desarrollo en caliente.
Migrar a Kubernetes cuando necesites escalar
Limpieza completa del stack Nginx + MySQL
Para dejar tu computadora limpia después de este tutorial (contenedores, imágenes, volúmenes, redes y la carpeta del proyecto), sigue estos pasos en orden.
Paso 1: Ir a la carpeta del proyecto
cd mi-stack-nginx-mysqlSi ya no estás en ella, ubícala con
cd ruta/a/mi-stack-nginx-mysql.
Paso 2: Detener y eliminar todo con Docker Compose
El comando más potente es:
docker compose down -v --rmi all --remove-orphans¿Qué hace cada bandera?
| Bandera | Efecto |
|---|---|
down | Detiene y elimina contenedores y redes creadas por el compose. |
-v | Elimina los volúmenes (⚠️ borra los datos de MySQL: mysql_data). |
--rmi all | Elimina todas las imágenes usadas por los servicios (incluida la construida mi-stack-nginx-mysql-web). |
--remove-orphans | Elimina contenedores huérfanos de servicios que ya no están en el compose (por ejemplo, si agregaste adminer y luego lo quitaste). |
💡 Si no quieres borrar la imagen
mysql:8.0(porque la usas en otros proyectos), usa--rmi localen lugar de--rmi all. Eso solo borra las imágenes construidas localmente (la de Nginx), no las descargadas.
Paso 3: Verificar que no queda nada
# Contenedores (incluyendo detenidos)
docker ps -a
# Imágenes
docker images
# Volúmenes
docker volume ls
# Redes
docker network lsNo deberías ver:
Contenedores:
mysql_tareas,nginx_web,adminer_tareasImagen:
mi-stack-nginx-mysql-webVolumen:
mi-stack-nginx-mysql_mysql_dataRed:
mi-stack-nginx-mysql_red_app
Paso 4: Eliminar la carpeta del proyecto
Regresa al directorio padre y borra la carpeta completa:
Linux / macOS:
cd ..
rm -rf mi-stack-nginx-mysqlWindows (PowerShell):
cd ..
Remove-Item -Recurse -Force mi-stack-nginx-mysqlWindows (CMD):
cd ..
rmdir /s /q mi-stack-nginx-mysqlPaso 5: Verificación final
# ¿Quedó alguna carpeta del proyecto?
ls | grep mi-stack
# ¿Quedó algún contenedor o imagen relacionados?
docker ps -a | grep -E "mysql_tareas|nginx_web|adminer"
docker images | grep -E "mi-stack|adminer"
docker volume ls | grep mysql_data
docker network ls | grep red_appSi ningún comando devuelve resultados, tu sistema quedó limpio. ✅
🧾 Resumen rápido (todo en uno)
cd mi-stack-nginx-mysql
docker compose down -v --rmi all --remove-orphans
cd ..
rm -rf mi-stack-nginx-mysql🧠 Extra: Limpieza total de Docker (opcional)
Si además quieres liberar todo el espacio que Docker esté ocupando sin usar (caché de build, imágenes huérfanas, redes, contenedores detenidos):
docker system prune -a --volumes⚠️ Cuidado: esto elimina todo lo que no esté en uso activo, incluyendo imágenes de otros proyectos que hayas descargado. Úsalo solo si estás seguro.
Para ver cuánto espacio estás recuperando antes de borrar:
docker system dfSalida típica:
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 5 0 1.2GB 1.2GB (100%)
Containers 0 0 0B 0B
Local Volumes 1 0 150MB 150MB (100%)
Build Cache 20 0 300MB 300MB⚠️ Notas importantes
El volumen
mysql_datacontiene tu base de datos. Si algún día quieres conservarla, no uses-vendocker compose down.La imagen
mysql:8.0puede que la quieras conservar si la reutilizas en otros proyectos. En ese caso usa--rmi localen lugar de--rmi all.Si agregaste Adminer (el bonus del tutorial), el
--remove-orphansse encargará de limpiarlo si ya no está en el compose; si aún está,downlo elimina igual.Los datos en
init/init.sqldesaparecen con la carpeta del proyecto, pero MySQL ya los había copiado al volumen; al borrar el volumen se pierden definitivamente.
Comentarios
Publicar un comentario