Práctica 8 — Proyecto Final: Servidor de Producción
Setup — ejecutar una sola vez
Sección titulada “Setup — ejecutar una sola vez"mkdir -p ~/practica8/{work,entrega}sudo apt update && sudo apt upgrade -y
# Verifica que el firewall está activo desde la práctica 7sudo ufw status | grep "Status:"# Si no está activo: sudo ufw enable (y asegúrate de haber abierto el puerto 22 antes)
# Directorios de la prácticasudo mkdir -p /var/www/misite /backup/dbTodo lo que se pida “guardar” debe ir dentro de ~/practica8/entrega/.
Ejercicio 8.1 — Servidor web nginx con Virtual Hosts
Sección titulada “Ejercicio 8.1 — Servidor web nginx con Virtual Hosts"Contexto
Sección titulada “Contexto"nginx no es Apache. Es un servidor de eventos asíncronos capaz de manejar miles de conexiones simultáneas con un consumo de memoria mínimo. En producción nunca se sirven sitios desde el virtual host default; cada dominio tiene su propio bloque server aislado. Aquí construirás esa arquitectura desde cero.
- Instala y audita nginx antes de tocarlo:
sudo apt install -y nginx
# Inicia y habilitasudo systemctl enable --now nginx
# Verifica que respondecurl -s -o /dev/null -w "HTTP: %{http_code}\n" http://localhost
# Audita la estructura de directorios de nginxls -la /etc/nginx/ls -la /etc/nginx/sites-available/ls -la /etc/nginx/sites-enabled/Guarda en ~/practica8/entrega/81_nginx_inicial.txt. Identifica con comentarios # la diferencia entre sites-available y sites-enabled y por qué se usan symlinks.
- Abre el firewall para el tráfico web:
# Permite HTTP y HTTPS con un solo perfil predefinido de UFWsudo ufw allow 'Nginx Full'sudo ufw status | grep Nginx
# Verifica desde fuera: la IP de tu VM debe responder con la página por defectocurl -s -o /dev/null -w "HTTP externo: %{http_code}\n" http://$(hostname -I | awk '{print $1}')Guarda en ~/practica8/entrega/81_ufw_web.txt.
- Crea el primer virtual host —
misite.local:
# Crea el directorio raíz del sitiosudo mkdir -p /var/www/misitesudo chown -R www-data:www-data /var/www/misitesudo chmod -R 755 /var/www/misite
# Crea una página de inicio estática para pruebassudo tee /var/www/misite/index.html > /dev/null << 'EOF'<!DOCTYPE html><html lang="es"><head><meta charset="UTF-8"><title>misite.local</title></head><body> <h1>misite.local funcionando</h1> <p>Servidor: <?php echo gethostname(); ?></p></body></html>EOF
# Crea el bloque server del virtual hostsudo nano /etc/nginx/sites-available/misiteEscribe:
server { listen 80; listen [::]:80;
server_name misite.local; root /var/www/misite; index index.php index.html;
# Logging por sitio (separado del default) access_log /var/log/nginx/misite_access.log; error_log /var/log/nginx/misite_error.log;
location / { try_files $uri $uri/ =404; }
# PHP-FPM (se activa en el ejercicio 8.2) location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/var/run/php/php8.3-fpm.sock; }
# Bloquea acceso a ficheros ocultos (.htaccess, .env, etc.) location ~ /\. { deny all; }}# Activa el sitio con un symlinksudo ln -s /etc/nginx/sites-available/misite /etc/nginx/sites-enabled/
# Desactiva el virtualhost por defectosudo rm -f /etc/nginx/sites-enabled/default
# Valida la sintaxis antes de recargar (SIEMPRE hacer esto)sudo nginx -t
# Recarga nginx (no restart — reload no corta conexiones activas)sudo systemctl reload nginxGuarda en ~/practica8/entrega/81_virtualhost.txt.
- Configura la resolución DNS local y prueba:
# Añade el dominio al /etc/hosts del servidorecho "127.0.0.1 misite.local" | sudo tee -a /etc/hosts
# Prueba de resoluciónping -c 1 misite.local
# Prueba HTTPcurl -s http://misite.local | head -10Guarda en ~/practica8/entrega/81_prueba_web.txt.
- Crea un segundo virtual host —
api.local(simula un microservicio separado):
sudo mkdir -p /var/www/apisudo chown www-data:www-data /var/www/api
sudo tee /var/www/api/index.html > /dev/null << 'EOF'{"status": "ok", "servicio": "api.local", "version": "1.0"}EOF
sudo tee /etc/nginx/sites-available/api > /dev/null << 'EOF'server { listen 80; server_name api.local; root /var/www/api; index index.html;
access_log /var/log/nginx/api_access.log; error_log /var/log/nginx/api_error.log;
location / { try_files $uri $uri/ =404; add_header Content-Type application/json; }}EOF
sudo ln -s /etc/nginx/sites-available/api /etc/nginx/sites-enabled/echo "127.0.0.1 api.local" | sudo tee -a /etc/hostssudo nginx -t && sudo systemctl reload nginx
curl -s http://api.localGuarda en ~/practica8/entrega/81_segundo_vh.txt.
Ejercicio 8.2 — Pila LEMP completa
Sección titulada “Ejercicio 8.2 — Pila LEMP completa"Contexto
Sección titulada “Contexto"Un servidor web estático sirve HTML. Pero el 90% de las aplicaciones de negocio necesitan una base de datos y un lenguaje de servidor. Aquí integrarás los tres componentes de la pila LEMP — nginx ya instalado, MariaDB y PHP-FPM — y los conectarás para que funcionen como un servidor de producción real.
Tarea 1 — Instala y asegura MariaDB
Sección titulada “Tarea 1 — Instala y asegura MariaDB"sudo apt install -y mariadb-server
sudo systemctl enable --now mariadbsystemctl is-active mariadb
# Asegura la instalación interactiva# Respuestas: Enter (sin pass actual) → n → y (contraseña: DBsegura2024!) → y → y → y → ysudo mysql_secure_installationGuarda en ~/practica8/entrega/82_mariadb_install.txt:
{ systemctl status mariadb --no-pager | head -8 sudo mysql -e "SHOW VARIABLES LIKE 'version';" 2>/dev/null} > ~/practica8/entrega/82_mariadb_install.txtTarea 2 — Crea la base de datos y el usuario de aplicación
Sección titulada “Tarea 2 — Crea la base de datos y el usuario de aplicación"sudo mysql -u root -pEjecuta dentro de MariaDB:
-- Crea la base de datos de la aplicaciónCREATE DATABASE cursodb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Crea el usuario de aplicación (nunca uses root para la app)CREATE USER 'cursouser'@'localhost' IDENTIFIED BY 'AppPass2024!';
-- Permisos solo sobre la BD de la aplicación (principio de mínimo privilegio)GRANT ALL PRIVILEGES ON cursodb.* TO 'cursouser'@'localhost';FLUSH PRIVILEGES;
-- Crea una tabla de pruebaUSE cursodb;CREATE TABLE registros ( id INT AUTO_INCREMENT PRIMARY KEY, mensaje VARCHAR(255) NOT NULL, creado_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP);INSERT INTO registros (mensaje) VALUES ('Primera entrada desde práctica LFCS');
-- VerificaSHOW DATABASES;SELECT user, host FROM mysql.user WHERE user = 'cursouser';SELECT * FROM registros;exit# Prueba la conexión con el usuario de aplicaciónmysql -u cursouser -p cursodb -e "SELECT * FROM registros;"Guarda en ~/practica8/entrega/82_mariadb_bd.txt.
Tarea 3 — Instala PHP-FPM y conéctalo a nginx
Sección titulada “Tarea 3 — Instala PHP-FPM y conéctalo a nginx"sudo apt install -y php-fpm php-mysql php-cli php-json php-curl
# Detecta la versión instaladaPHP_VER=$(php -r "echo PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION;")echo "PHP instalado: $PHP_VER"
# Arranca el servicio FPMsudo systemctl enable --now php${PHP_VER}-fpmsystemctl is-active php${PHP_VER}-fpm
# Verifica que el socket Unix está disponiblels -la /var/run/php/php${PHP_VER}-fpm.sockActualiza el virtual host de misite con la versión correcta de PHP:
# Sustituye la versión de PHP en el virtualhost (8.3 → tu versión real)sudo sed -i "s/php8\.3-fpm\.sock/php${PHP_VER}-fpm.sock/" /etc/nginx/sites-available/misite
sudo nginx -t && sudo systemctl reload nginxGuarda en ~/practica8/entrega/82_php_fpm.txt.
Tarea 4 — Crea la página PHP que conecta con la base de datos
Sección titulada “Tarea 4 — Crea la página PHP que conecta con la base de datos"sudo tee /var/www/misite/index.php > /dev/null << 'PHPEOF'<?php// index.php — Página de prueba LEMP$host = 'localhost';$db = 'cursodb';$user = 'cursouser';$pass = 'AppPass2024!';
echo "<h1>Servidor LEMP — Práctica Final LFCS</h1>";echo "<h2>Estado del servidor</h2>";echo "<pre>";echo "Hostname: " . gethostname() . "\n";echo "PHP: " . phpversion() . "\n";echo "Fecha: " . date('Y-m-d H:i:s') . "\n";echo "</pre>";
echo "<h2>Conexión a MariaDB</h2>";try { $pdo = new PDO("mysql:host=$host;dbname=$db;charset=utf8mb4", $user, $pass); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); echo "<p style='color:green'>✓ Conexión exitosa a MariaDB</p>";
$stmt = $pdo->query("SELECT * FROM registros ORDER BY id DESC LIMIT 5"); echo "<h3>Últimas entradas en la BD:</h3><ul>"; while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { echo "<li>[{$row['creado_en']}] {$row['mensaje']}</li>"; } echo "</ul>";
// Inserta una nueva entrada en cada visita $stmt = $pdo->prepare("INSERT INTO registros (mensaje) VALUES (?)"); $stmt->execute(["Visita desde " . ($_SERVER['REMOTE_ADDR'] ?? 'desconocido')]);
} catch (PDOException $e) { echo "<p style='color:red'>✗ Error de conexión: " . $e->getMessage() . "</p>";}?>PHPEOF
sudo chown www-data:www-data /var/www/misite/index.php# Prueba completa de la pila LEMPcurl -s http://misite.local | grep -E "LEMP|exitosa|Error"Guarda en ~/practica8/entrega/82_lemp_prueba.txt:
{ echo "--- Estado de los 3 servicios ---" for svc in nginx mariadb php${PHP_VER}-fpm; do printf "%-20s %s\n" "$svc" "$(systemctl is-active $svc)" done echo "" echo "--- Respuesta HTTP ---" curl -s http://misite.local | grep -E "LEMP|exitosa|Error|PHP:|Hostname" echo "" echo "--- Registros en BD ---" mysql -u cursouser -pAppPass2024! cursodb -e "SELECT COUNT(*) as total FROM registros;" 2>/dev/null} > ~/practica8/entrega/82_lemp_prueba.txtTarea 5 — Backup automático de la base de datos
Sección titulada “Tarea 5 — Backup automático de la base de datos"# Prueba manual del backupsudo mysqldump -u root cursodb > /backup/db/cursodb_$(date '+%F').sqlls -lh /backup/db/
# Verifica que el dump es válidohead -5 /backup/db/cursodb_$(date '+%F').sql
# Automatiza con cron (como root para acceso sin contraseña a mysqldump)sudo crontab -eAñade al final del crontab de root:
# Backup diario de MariaDB a las 2:00 AM0 2 * * * mysqldump -u root cursodb > /backup/db/cursodb_$(date +\%F).sql 2>/dev/null# Limpia backups de más de 30 días0 3 * * * find /backup/db -name "*.sql" -mtime +30 -delete# Verifica el crontabsudo crontab -l | grep mysqldumpGuarda en ~/practica8/entrega/82_backup.txt.
Ejercicio 8.3 — Simulacro LFCS: tres desastres de producción
Sección titulada “Ejercicio 8.3 — Simulacro LFCS: tres desastres de producción"Contexto
Sección titulada “Contexto"El examen LFCS te dará un servidor roto sin contexto. Deberás deducir los fallos leyendo logs, analizando el estado del sistema y aplicando correcciones quirúrgicas. Aquí enfrentarás tres desastres reales que integran habilidades de todos los módulos anteriores.
Desastre 1 — Disco raíz lleno: la base de datos deja de escribir
Sección titulada “Desastre 1 — Disco raíz lleno: la base de datos deja de escribir"Setup del desastre (ejecuta UNA sola vez)
Sección titulada “Setup del desastre (ejecuta UNA sola vez)"bash -lc 'set -euo pipefail
# Crea un fichero de imagen de 500 MB para simular disco loopsudo fallocate -l 500M /tmp/disco_lleno.imgLOOP=$(sudo losetup --find --show /tmp/disco_lleno.img)echo "Loop device: $LOOP"
# Formatea y monta como si fuera un disco secundariosudo mkfs.ext4 "$LOOP" -qsudo mkdir -p /mnt/simul_llenosudo mount "$LOOP" /mnt/simul_lleno
# Llena el disco al 100% dejando 0 bytes libressudo dd if=/dev/zero of=/mnt/simul_lleno/relleno.dat bs=1M \ count=490 status=progress 2>/dev/null || true
echo "DISCO_LOOP=$LOOP" > /tmp/desastre1_info.txtecho "Disco de simulación lleno al 100%"df -h /mnt/simul_lleno'Síntoma reportado
Sección titulada “Síntoma reportado"MariaDB empieza a rechazar escrituras. El log del sistema muestra errores de E/S. El equipo de desarrollo llama diciendo que la aplicación web lanza errores de base de datos.
Tu misión
Sección titulada “Tu misión"- Diagnostica cuál disco está lleno y cuánto espacio queda
- Identifica qué proceso o fichero está consumiendo el espacio
- Libera espacio sin perder datos críticos (limpia el fichero de relleno)
- Verifica que MariaDB puede volver a escribir
# Herramientas de diagnóstico disponiblesdf -h # uso de discosdu -sh /mnt/simul_lleno/* 2>/dev/null # qué ocupa espaciosudo journalctl -p err --since "10 minutes ago" --no-pager | tail -20Guarda el proceso completo en ~/practica8/entrega/83_desastre1.txt:
{ echo "=== DESASTRE 1: DIAGNÓSTICO Y SOLUCIÓN ===" echo "" echo "--- Estado de discos al descubrir el problema ---" df -h echo "" echo "--- Solución aplicada ---" echo "(documenta aquí los comandos que ejecutaste)" echo "" echo "--- Estado tras la solución ---" df -h /mnt/simul_lleno} > ~/practica8/entrega/83_desastre1.txtDesastre 2 — Servicio web caído: nginx no arranca
Sección titulada “Desastre 2 — Servicio web caído: nginx no arranca"Setup del desastre (ejecuta UNA sola vez)
Sección titulada “Setup del desastre (ejecuta UNA sola vez)"bash -lc 'set -euo pipefail
# Error 1: introduce un error de sintaxis en la configuración de nginxsudo tee /etc/nginx/sites-available/misite-roto > /dev/null << '"'"'EOF'"'"'server { listen 80; server_name roto.local; root /var/www/misite;
# Error de sintaxis: falta el punto y coma index index.php index.html
location / { try_files $uri $uri/ =404; }
# Error: directiva inexistente enable_gzip on;}EOF
sudo ln -sf /etc/nginx/sites-available/misite-roto /etc/nginx/sites-enabled/misite-roto
# Error 2: el directorio raíz del segundo sitio no existesudo tee /etc/nginx/sites-available/fantasma > /dev/null << '"'"'EOF'"'"'server { listen 8080; server_name fantasma.local; root /var/www/no_existe_este_directorio; index index.html;}EOFsudo ln -sf /etc/nginx/sites-available/fantasma /etc/nginx/sites-enabled/fantasma
# Intenta recargar nginx (fallará)sudo nginx -t 2>&1 || truesudo systemctl reload nginx 2>&1 || true
echo "Escenario desastre 2 creado"echo "nginx está en estado degradado — 2 errores de configuración sembrados"'Síntoma reportado
Sección titulada “Síntoma reportado"El monitoring alerta: nginx está caído. systemctl status nginx muestra errores. Los usuarios no pueden acceder a misite.local. El equipo de operaciones no sabe cuántos errores hay ni dónde.
Tu misión
Sección titulada “Tu misión"- Usa las herramientas de diagnóstico de nginx para encontrar todos los errores
- Corrige cada uno sin borrar los ficheros (entiende el error antes de corregirlo)
- Verifica que nginx arranca correctamente y
misite.localresponde
# Herramientas de diagnósticosudo nginx -t # valida la sintaxis completasudo journalctl -u nginx -n 30 --no-pager # logs del servicioGuarda en ~/practica8/entrega/83_desastre2.txt:
{ echo "=== DESASTRE 2: DIAGNÓSTICO Y SOLUCIÓN ===" echo "" echo "--- Diagnóstico inicial (nginx -t) ---" sudo nginx -t 2>&1 || true echo "" echo "--- Solución aplicada ---" echo "(documenta aquí los comandos de corrección)" echo "" echo "--- Estado tras la solución ---" sudo nginx -t 2>&1 curl -s -o /dev/null -w "HTTP misite.local: %{http_code}\n" http://misite.local} > ~/practica8/entrega/83_desastre2.txtDesastre 3 — Investigación forense: quién y cuándo rompió la BD
Sección titulada “Desastre 3 — Investigación forense: quién y cuándo rompió la BD"Setup del desastre (ejecuta UNA sola vez)
Sección titulada “Setup del desastre (ejecuta UNA sola vez)"bash -lc 'set -euo pipefail
# Simula actividad sospechosa en MariaDBsudo mysql -e " USE cursodb; DROP TABLE IF EXISTS registros_backup; CREATE TABLE registros_backup SELECT * FROM registros; DELETE FROM registros WHERE id > 1;" 2>/dev/null || true
# Crea entradas sospechosas en el log de autenticaciónlogger -t sshd "Failed password for invalid user admin from 203.0.113.47 port 52340 ssh2"logger -t sshd "Failed password for root from 203.0.113.47 port 52341 ssh2"logger -t sshd "Failed password for root from 203.0.113.48 port 12345 ssh2"logger -t mariadb "Access denied for user '"'"'root'"'"'@'"'"'192.168.1.200'"'"' (using password: YES)"
echo "Escenario desastre 3 creado"echo "La BD tiene datos eliminados y el log tiene actividad sospechosa"'Síntoma reportado
Sección titulada “Síntoma reportado"Un desarrollador reporta que la tabla registros tiene menos entradas de las esperadas. El equipo de seguridad quiere saber si hubo intentos de acceso no autorizado. No hay acceso a la consola web del servidor.
Tu misión
Sección titulada “Tu misión"- Determina cuántos registros faltan en la tabla y cuándo se eliminaron (usa los logs de MariaDB)
- Investiga los intentos de acceso fallidos en
/var/log/auth.log - Identifica las IPs que intentaron acceder y cuántas veces
- Recupera los registros eliminados desde la tabla de backup
# Herramientas de diagnósticosudo journalctl -p warning --since "1 hour ago" --no-pager | tail -30sudo grep "Failed\|Accepted\|Invalid" /var/log/auth.log | tail -20mysql -u cursouser -pAppPass2024! cursodb -e "SELECT COUNT(*) FROM registros; SELECT COUNT(*) FROM registros_backup;" 2>/dev/nullGuarda en ~/practica8/entrega/83_desastre3.txt:
{ echo "=== DESASTRE 3: INVESTIGACIÓN FORENSE ===" echo "" echo "--- Registros actuales vs backup ---" mysql -u cursouser -pAppPass2024! cursodb \ -e "SELECT 'actuales' as tabla, COUNT(*) as total FROM registros UNION SELECT 'backup', COUNT(*) FROM registros_backup;" 2>/dev/null echo "" echo "--- IPs sospechosas en auth.log ---" sudo grep "Failed password" /var/log/auth.log | \ awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -10 echo "" echo "--- Solución y recuperación ---" echo "(documenta aquí el proceso de recuperación)"} > ~/practica8/entrega/83_desastre3.txtEjercicio 8.4 — Contenedores Docker
Sección titulada “Ejercicio 8.4 — Contenedores Docker"Contexto
Sección titulada “Contexto"Docker es la tecnología que separó el “configurar servidores” del “desplegar aplicaciones”. Un contenedor es un proceso aislado con sus propias librerías, ficheros y red — pero comparte el kernel del host. Aquí aprenderás los comandos esenciales de Docker que aparecen en el examen LFCS y en cualquier entorno de producción moderno.
Setup — instala Docker
Sección titulada “Setup — instala Docker"# Instala Docker desde el repositorio oficialsudo apt install -y ca-certificates curl gnupgsudo install -m 0755 -d /etc/apt/keyringscurl -fsSL https://download.docker.com/linux/debian/gpg | \ sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpgsudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/debian \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt updatesudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Añade tu usuario al grupo docker (evita usar sudo en cada comando)sudo usermod -aG docker $USER
# Activa el serviciosudo systemctl enable --now dockerdocker --versionTarea 1 — Primeros pasos con imágenes y contenedores
Sección titulada “Tarea 1 — Primeros pasos con imágenes y contenedores"# Descarga (pull) la imagen oficial de nginx desde Docker Hubdocker pull nginx:alpine
# Ejecuta un contenedor en segundo plano# -d: background, -p: mapeo de puertos host:contenedor, --name: nombre del contenedordocker run -d --name web-prueba -p 8080:80 nginx:alpine
# Verifica que está corriendodocker ps
# Accede a la aplicacióncurl -s -o /dev/null -w "HTTP Docker: %{http_code}\n" http://localhost:8080
# Inspecciona el contenedordocker inspect web-prueba | grep -E "IPAddress|Status|Image"
# Logs del contenedordocker logs web-prueba
# Detén y elimina el contenedordocker stop web-pruebadocker rm web-prueba
# Limpia imágenes no usadasdocker image prune -fGuarda en ~/practica8/entrega/84_docker_basico.txt.
Tarea 2 — Construye tu propia imagen con Dockerfile
Sección titulada “Tarea 2 — Construye tu propia imagen con Dockerfile"mkdir -p ~/practica8/work/myappcd ~/practica8/work/myappCrea el Dockerfile:
tee Dockerfile > /dev/null << 'EOF'# Imagen base mínima de Alpine LinuxFROM alpine:3.19
# Metadatos de la imagenLABEL maintainer="practica-lfcs"LABEL description="Servidor HTTP mínimo para práctica LFCS"
# Instala nginx dentro de la imagenRUN apk add --no-cache nginx
# Crea el directorio web y una página de inicioRUN mkdir -p /var/www/html && \ echo '<h1>Contenedor LFCS</h1><p>Imagen personalizada Alpine+nginx</p>' \ > /var/www/html/index.html
# Configura nginx para correr en foreground (necesario en contenedores)RUN echo 'daemon off;' >> /etc/nginx/nginx.conf
# Expone el puerto 80 (documentación — no abre el puerto en el host)EXPOSE 80
# Comando que se ejecuta al arrancar el contenedorCMD ["nginx", "-g", "daemon off;"]EOF# Construye la imagen con un tag descriptivodocker build -t miapp-lfcs:v1 .
# Verifica que la imagen se creódocker images | grep miapp-lfcs
# Ejecuta un contenedor de tu imagendocker run -d --name miapp -p 8081:80 miapp-lfcs:v1curl -s http://localhost:8081docker logs miappdocker stop miapp && docker rm miappGuarda en ~/practica8/entrega/84_dockerfile.txt.
Tarea 3 — Volúmenes: persistencia de datos entre reinicios
Sección titulada “Tarea 3 — Volúmenes: persistencia de datos entre reinicios"# Sin volumen: los datos mueren con el contenedordocker run -d --name sin-vol -p 8082:80 nginx:alpinedocker exec sin-vol sh -c 'echo "dato efímero" > /usr/share/nginx/html/test.txt'curl -s http://localhost:8082/test.txtdocker stop sin-vol && docker rm sin-vol# El dato ha desaparecido
# Con volumen: los datos sobreviven al contenedordocker volume create datos-nginxdocker run -d --name con-vol \ -p 8083:80 \ -v datos-nginx:/usr/share/nginx/html \ nginx:alpine
docker exec con-vol sh -c 'echo "<h1>Dato persistente</h1>" > /usr/share/nginx/html/index.html'curl -s http://localhost:8083
# Elimina el contenedordocker stop con-vol && docker rm con-vol
# El volumen persistedocker volume ls | grep datos-nginx
# Crea un nuevo contenedor montando el mismo volumendocker run -d --name vol-nuevo -p 8083:80 -v datos-nginx:/usr/share/nginx/html nginx:alpinecurl -s http://localhost:8083 # el dato sigue ahí
docker stop vol-nuevo && docker rm vol-nuevoGuarda en ~/practica8/entrega/84_volumenes.txt. Explica con un comentario # cuándo usarías volúmenes Docker vs bind mounts.
Tarea 4 — Docker Compose: orquesta múltiples servicios
Sección titulada “Tarea 4 — Docker Compose: orquesta múltiples servicios"mkdir -p ~/practica8/work/composecd ~/practica8/work/composeCrea el fichero compose.yaml:
tee compose.yaml > /dev/null << 'EOF'# compose.yaml — Pila web+bd para práctica LFCSservices:
web: image: nginx:alpine container_name: compose-web ports: - "8090:80" volumes: - ./html:/usr/share/nginx/html:ro depends_on: - db restart: unless-stopped
db: image: mariadb:11 container_name: compose-db environment: MARIADB_ROOT_PASSWORD: rootpass123 MARIADB_DATABASE: appdb MARIADB_USER: appuser MARIADB_PASSWORD: apppass123 volumes: - db-data:/var/lib/mysql restart: unless-stopped
volumes: db-data:EOF
# Crea el contenido web de pruebamkdir -p htmlecho '<h1>Docker Compose — LFCS</h1><p>web + mariadb funcionando</p>' > html/index.html# Arranca todos los servicios en segundo planodocker compose up -d
# Estado de los contenedores del proyectodocker compose ps
# Prueba la webcurl -s http://localhost:8090
# Logs de todos los serviciosdocker compose logs --tail=10
# Para y elimina todos los contenedores (los volúmenes persisten)docker compose down
# Para y elimina también los volúmenesdocker compose down -vGuarda en ~/practica8/entrega/84_compose.txt.
📤 Bloque de entrega
Sección titulada “📤 Bloque de entrega"Guarda la verificación completa del proyecto final:
mkdir -p ~/practica8/entregaPHP_VER=$(php -r "echo PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION;" 2>/dev/null || echo "8.3")
{ echo "==========================================" echo " ENTREGA PRÁCTICA 8 — PROYECTO FINAL" echo " $(date)" echo "==========================================" echo ""
echo "--- 8.1: Virtual Hosts nginx ---" sudo nginx -t 2>&1 ls /etc/nginx/sites-enabled/ echo ""
echo "--- 8.1: Sitios respondiendo ---" curl -s -o /dev/null -w "misite.local: HTTP %{http_code}\n" http://misite.local curl -s -o /dev/null -w "api.local: HTTP %{http_code}\n" http://api.local echo ""
echo "--- 8.2: Estado servicios LEMP ---" for svc in nginx mariadb php${PHP_VER}-fpm; do printf "%-22s %s\n" "$svc" "$(systemctl is-active $svc)" done echo ""
echo "--- 8.2: Base de datos ---" sudo mysql -e "SHOW DATABASES;" 2>/dev/null | grep cursodb && echo "BD cursodb: OK" sudo mysql -e "SELECT user FROM mysql.user WHERE user='cursouser';" 2>/dev/null | grep -q cursouser && echo "Usuario cursouser: OK" echo ""
echo "--- 8.2: Backup cron ---" sudo crontab -l 2>/dev/null | grep mysqldump && echo "Cron backup: OK" || echo "Cron backup: FALTA" ls -lh /backup/db/ 2>/dev/null | tail -3 echo ""
echo "--- 8.3: Desastre 1 — disco ---" df -h /mnt/simul_lleno 2>/dev/null || echo "Volumen de simulación desmontado (OK tras solución)" echo ""
echo "--- 8.3: Desastre 2 — nginx ---" sudo nginx -t 2>&1 | tail -2 echo ""
echo "--- 8.3: Desastre 3 — BD ---" mysql -u cursouser -pAppPass2024! cursodb \ -e "SELECT COUNT(*) as registros_recuperados FROM registros;" 2>/dev/null || echo "verificar manualmente" echo ""
echo "--- 8.4: Docker ---" docker --version 2>/dev/null || echo "Docker no instalado" docker images 2>/dev/null | grep miapp-lfcs || echo "imagen miapp-lfcs no encontrada" echo ""
echo "--- Seguridad general ---" sudo ufw status | grep "Status:" grep "PermitRootLogin" /etc/ssh/sshd_config echo ""
echo "=========================================="} > ~/practica8/entrega/verificacion.txt
cat ~/practica8/entrega/verificacion.txtPara empaquetar y enviar, sigue las instrucciones comunes: Cómo entregar las prácticas (usa N = 8).
🧨 Desafío extra (MUY DIFÍCIL) — Pila LEMP rota de múltiples formas
Sección titulada “🧨 Desafío extra (MUY DIFÍCIL) — Pila LEMP rota de múltiples formas"Setup (ejecuta UNA sola vez)
Sección titulada “Setup (ejecuta UNA sola vez)"bash -lc 'set -euo pipefailPHP_VER=$(php -r "echo PHP_MAJOR_VERSION.'"'"'.'"'"'.PHP_MINOR_VERSION;" 2>/dev/null || echo "8.3")
# Error 1: detén PHP-FPMsudo systemctl stop php${PHP_VER}-fpm
# Error 2: cambia el propietario de los ficheros web (nginx no puede leerlos)sudo chown -R root:root /var/www/misitesudo chmod -R 700 /var/www/misite
# Error 3: configura MariaDB con bind-address incorrectosudo sed -i "s/^bind-address.*/bind-address = 127.0.0.2/" \ /etc/mysql/mariadb.conf.d/50-server.cnf 2>/dev/null || \ echo "bind-address = 127.0.0.2" | sudo tee -a /etc/mysql/mariadb.conf.d/50-server.cnfsudo systemctl restart mariadb 2>/dev/null || true
echo "Escenario extra creado — 3 errores sembrados en la pila LEMP"echo "Síntomas: HTTP 502 en misite.local, ficheros inaccesibles, BD no conecta"'Síntomas reportados
Sección titulada “Síntomas reportados"curl http://misite.localdevuelve HTTP 502 Bad Gateway- La página PHP muestra error de conexión a la base de datos
- Los logs de nginx muestran errores de permisos
Tu misión
Sección titulada “Tu misión"Repara la pila completa siguiendo este flujo de diagnóstico en capas (de fuera hacia dentro):
1. ¿nginx responde? → curl + systemctl status nginx2. ¿PHP-FPM está activo? → systemctl status php*-fpm + journalctl3. ¿El socket existe? → ls -la /var/run/php/4. ¿nginx puede leer los ficheros web? → ls -la /var/www/misite5. ¿MariaDB acepta conexiones? → mysql -u cursouser -p cursodb6. ¿La app funciona end-to-end? → curl http://misite.localGuarda en ~/practica8/entrega/85_solucion.txt:
{ echo "=== SOLUCIÓN DESAFÍO EXTRA 8.5 ===" echo "" echo "--- Estado final de la pila ---" PHP_VER=$(php -r "echo PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION;" 2>/dev/null || echo "8.3") for svc in nginx mariadb php${PHP_VER}-fpm; do printf "%-22s %s\n" "$svc" "$(systemctl is-active $svc)" done echo "" echo "--- Verificación HTTP end-to-end ---" curl -s -o /dev/null -w "HTTP: %{http_code}\n" http://misite.local curl -s http://misite.local | grep -E "exitosa|Error|LEMP" echo "" echo "--- Permisos /var/www/misite ---" ls -la /var/www/misite/ echo "" echo "--- MariaDB bind-address ---" sudo grep "bind-address" /etc/mysql/mariadb.conf.d/50-server.cnf 2>/dev/null} > ~/practica8/entrega/85_solucion.txt