Ir al contenido

Práctica 8 — Proyecto Final: Servidor de Producción


ventana terminal
mkdir -p ~/practica8/{work,entrega}
sudo apt update && sudo apt upgrade -y
# Verifica que el firewall está activo desde la práctica 7
sudo 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áctica
sudo mkdir -p /var/www/misite /backup/db

Todo 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"

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.

  1. Instala y audita nginx antes de tocarlo:
ventana terminal
sudo apt install -y nginx
# Inicia y habilita
sudo systemctl enable --now nginx
# Verifica que responde
curl -s -o /dev/null -w "HTTP: %{http_code}\n" http://localhost
# Audita la estructura de directorios de nginx
ls -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.

  1. Abre el firewall para el tráfico web:
ventana terminal
# Permite HTTP y HTTPS con un solo perfil predefinido de UFW
sudo ufw allow 'Nginx Full'
sudo ufw status | grep Nginx
# Verifica desde fuera: la IP de tu VM debe responder con la página por defecto
curl -s -o /dev/null -w "HTTP externo: %{http_code}\n" http://$(hostname -I | awk '{print $1}')

Guarda en ~/practica8/entrega/81_ufw_web.txt.

  1. Crea el primer virtual host — misite.local:
ventana terminal
# Crea el directorio raíz del sitio
sudo mkdir -p /var/www/misite
sudo chown -R www-data:www-data /var/www/misite
sudo chmod -R 755 /var/www/misite
# Crea una página de inicio estática para pruebas
sudo 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 host
sudo nano /etc/nginx/sites-available/misite

Escribe:

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;
}
}
ventana terminal
# Activa el sitio con un symlink
sudo ln -s /etc/nginx/sites-available/misite /etc/nginx/sites-enabled/
# Desactiva el virtualhost por defecto
sudo 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 nginx

Guarda en ~/practica8/entrega/81_virtualhost.txt.

  1. Configura la resolución DNS local y prueba:
ventana terminal
# Añade el dominio al /etc/hosts del servidor
echo "127.0.0.1 misite.local" | sudo tee -a /etc/hosts
# Prueba de resolución
ping -c 1 misite.local
# Prueba HTTP
curl -s http://misite.local | head -10

Guarda en ~/practica8/entrega/81_prueba_web.txt.

  1. Crea un segundo virtual host — api.local (simula un microservicio separado):
ventana terminal
sudo mkdir -p /var/www/api
sudo 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/hosts
sudo nginx -t && sudo systemctl reload nginx
curl -s http://api.local

Guarda en ~/practica8/entrega/81_segundo_vh.txt.


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.

ventana terminal
sudo apt install -y mariadb-server
sudo systemctl enable --now mariadb
systemctl is-active mariadb
# Asegura la instalación interactiva
# Respuestas: Enter (sin pass actual) → n → y (contraseña: DBsegura2024!) → y → y → y → y
sudo mysql_secure_installation

Guarda en ~/practica8/entrega/82_mariadb_install.txt:

ventana terminal
{
systemctl status mariadb --no-pager | head -8
sudo mysql -e "SHOW VARIABLES LIKE 'version';" 2>/dev/null
} > ~/practica8/entrega/82_mariadb_install.txt

Tarea 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"
ventana terminal
sudo mysql -u root -p

Ejecuta dentro de MariaDB:

-- Crea la base de datos de la aplicación
CREATE 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 prueba
USE 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');
-- Verifica
SHOW DATABASES;
SELECT user, host FROM mysql.user WHERE user = 'cursouser';
SELECT * FROM registros;
exit
ventana terminal
# Prueba la conexión con el usuario de aplicación
mysql -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"
ventana terminal
sudo apt install -y php-fpm php-mysql php-cli php-json php-curl
# Detecta la versión instalada
PHP_VER=$(php -r "echo PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION;")
echo "PHP instalado: $PHP_VER"
# Arranca el servicio FPM
sudo systemctl enable --now php${PHP_VER}-fpm
systemctl is-active php${PHP_VER}-fpm
# Verifica que el socket Unix está disponible
ls -la /var/run/php/php${PHP_VER}-fpm.sock

Actualiza el virtual host de misite con la versión correcta de PHP:

ventana terminal
# 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 nginx

Guarda 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"
ventana terminal
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
ventana terminal
# Prueba completa de la pila LEMP
curl -s http://misite.local | grep -E "LEMP|exitosa|Error"

Guarda en ~/practica8/entrega/82_lemp_prueba.txt:

ventana terminal
{
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.txt

Tarea 5 — Backup automático de la base de datos

Sección titulada “Tarea 5 — Backup automático de la base de datos"
ventana terminal
# Prueba manual del backup
sudo mysqldump -u root cursodb > /backup/db/cursodb_$(date '+%F').sql
ls -lh /backup/db/
# Verifica que el dump es válido
head -5 /backup/db/cursodb_$(date '+%F').sql
# Automatiza con cron (como root para acceso sin contraseña a mysqldump)
sudo crontab -e

Añade al final del crontab de root:

# Backup diario de MariaDB a las 2:00 AM
0 2 * * * mysqldump -u root cursodb > /backup/db/cursodb_$(date +\%F).sql 2>/dev/null
# Limpia backups de más de 30 días
0 3 * * * find /backup/db -name "*.sql" -mtime +30 -delete
ventana terminal
# Verifica el crontab
sudo crontab -l | grep mysqldump

Guarda 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"

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"
ventana terminal
bash -lc 'set -euo pipefail
# Crea un fichero de imagen de 500 MB para simular disco loop
sudo fallocate -l 500M /tmp/disco_lleno.img
LOOP=$(sudo losetup --find --show /tmp/disco_lleno.img)
echo "Loop device: $LOOP"
# Formatea y monta como si fuera un disco secundario
sudo mkfs.ext4 "$LOOP" -q
sudo mkdir -p /mnt/simul_lleno
sudo mount "$LOOP" /mnt/simul_lleno
# Llena el disco al 100% dejando 0 bytes libres
sudo 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.txt
echo "Disco de simulación lleno al 100%"
df -h /mnt/simul_lleno'

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.

  1. Diagnostica cuál disco está lleno y cuánto espacio queda
  2. Identifica qué proceso o fichero está consumiendo el espacio
  3. Libera espacio sin perder datos críticos (limpia el fichero de relleno)
  4. Verifica que MariaDB puede volver a escribir
ventana terminal
# Herramientas de diagnóstico disponibles
df -h # uso de discos
du -sh /mnt/simul_lleno/* 2>/dev/null # qué ocupa espacio
sudo journalctl -p err --since "10 minutes ago" --no-pager | tail -20

Guarda el proceso completo en ~/practica8/entrega/83_desastre1.txt:

ventana terminal
{
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.txt

Desastre 2 — Servicio web caído: nginx no arranca

Sección titulada “Desastre 2 — Servicio web caído: nginx no arranca"
ventana terminal
bash -lc 'set -euo pipefail
# Error 1: introduce un error de sintaxis en la configuración de nginx
sudo 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 existe
sudo 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;
}
EOF
sudo ln -sf /etc/nginx/sites-available/fantasma /etc/nginx/sites-enabled/fantasma
# Intenta recargar nginx (fallará)
sudo nginx -t 2>&1 || true
sudo systemctl reload nginx 2>&1 || true
echo "Escenario desastre 2 creado"
echo "nginx está en estado degradado — 2 errores de configuración sembrados"'

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.

  1. Usa las herramientas de diagnóstico de nginx para encontrar todos los errores
  2. Corrige cada uno sin borrar los ficheros (entiende el error antes de corregirlo)
  3. Verifica que nginx arranca correctamente y misite.local responde
ventana terminal
# Herramientas de diagnóstico
sudo nginx -t # valida la sintaxis completa
sudo journalctl -u nginx -n 30 --no-pager # logs del servicio

Guarda en ~/practica8/entrega/83_desastre2.txt:

ventana terminal
{
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.txt

Desastre 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"
ventana terminal
bash -lc 'set -euo pipefail
# Simula actividad sospechosa en MariaDB
sudo 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ón
logger -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"'

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.

  1. Determina cuántos registros faltan en la tabla y cuándo se eliminaron (usa los logs de MariaDB)
  2. Investiga los intentos de acceso fallidos en /var/log/auth.log
  3. Identifica las IPs que intentaron acceder y cuántas veces
  4. Recupera los registros eliminados desde la tabla de backup
ventana terminal
# Herramientas de diagnóstico
sudo journalctl -p warning --since "1 hour ago" --no-pager | tail -30
sudo grep "Failed\|Accepted\|Invalid" /var/log/auth.log | tail -20
mysql -u cursouser -pAppPass2024! cursodb -e "SELECT COUNT(*) FROM registros; SELECT COUNT(*) FROM registros_backup;" 2>/dev/null

Guarda en ~/practica8/entrega/83_desastre3.txt:

ventana terminal
{
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.txt

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.

ventana terminal
# Instala Docker desde el repositorio oficial
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo 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 update
sudo 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 servicio
sudo systemctl enable --now docker
docker --version

Tarea 1 — Primeros pasos con imágenes y contenedores

Sección titulada “Tarea 1 — Primeros pasos con imágenes y contenedores"
ventana terminal
# Descarga (pull) la imagen oficial de nginx desde Docker Hub
docker pull nginx:alpine
# Ejecuta un contenedor en segundo plano
# -d: background, -p: mapeo de puertos host:contenedor, --name: nombre del contenedor
docker run -d --name web-prueba -p 8080:80 nginx:alpine
# Verifica que está corriendo
docker ps
# Accede a la aplicación
curl -s -o /dev/null -w "HTTP Docker: %{http_code}\n" http://localhost:8080
# Inspecciona el contenedor
docker inspect web-prueba | grep -E "IPAddress|Status|Image"
# Logs del contenedor
docker logs web-prueba
# Detén y elimina el contenedor
docker stop web-prueba
docker rm web-prueba
# Limpia imágenes no usadas
docker image prune -f

Guarda 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"
ventana terminal
mkdir -p ~/practica8/work/myapp
cd ~/practica8/work/myapp

Crea el Dockerfile:

ventana terminal
tee Dockerfile > /dev/null << 'EOF'
# Imagen base mínima de Alpine Linux
FROM alpine:3.19
# Metadatos de la imagen
LABEL maintainer="practica-lfcs"
LABEL description="Servidor HTTP mínimo para práctica LFCS"
# Instala nginx dentro de la imagen
RUN apk add --no-cache nginx
# Crea el directorio web y una página de inicio
RUN 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 contenedor
CMD ["nginx", "-g", "daemon off;"]
EOF
ventana terminal
# Construye la imagen con un tag descriptivo
docker build -t miapp-lfcs:v1 .
# Verifica que la imagen se creó
docker images | grep miapp-lfcs
# Ejecuta un contenedor de tu imagen
docker run -d --name miapp -p 8081:80 miapp-lfcs:v1
curl -s http://localhost:8081
docker logs miapp
docker stop miapp && docker rm miapp

Guarda 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"
ventana terminal
# Sin volumen: los datos mueren con el contenedor
docker run -d --name sin-vol -p 8082:80 nginx:alpine
docker exec sin-vol sh -c 'echo "dato efímero" > /usr/share/nginx/html/test.txt'
curl -s http://localhost:8082/test.txt
docker stop sin-vol && docker rm sin-vol
# El dato ha desaparecido
# Con volumen: los datos sobreviven al contenedor
docker volume create datos-nginx
docker 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 contenedor
docker stop con-vol && docker rm con-vol
# El volumen persiste
docker volume ls | grep datos-nginx
# Crea un nuevo contenedor montando el mismo volumen
docker run -d --name vol-nuevo -p 8083:80 -v datos-nginx:/usr/share/nginx/html nginx:alpine
curl -s http://localhost:8083 # el dato sigue ahí
docker stop vol-nuevo && docker rm vol-nuevo

Guarda 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"
ventana terminal
mkdir -p ~/practica8/work/compose
cd ~/practica8/work/compose

Crea el fichero compose.yaml:

ventana terminal
tee compose.yaml > /dev/null << 'EOF'
# compose.yaml — Pila web+bd para práctica LFCS
services:
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 prueba
mkdir -p html
echo '<h1>Docker Compose — LFCS</h1><p>web + mariadb funcionando</p>' > html/index.html
ventana terminal
# Arranca todos los servicios en segundo plano
docker compose up -d
# Estado de los contenedores del proyecto
docker compose ps
# Prueba la web
curl -s http://localhost:8090
# Logs de todos los servicios
docker compose logs --tail=10
# Para y elimina todos los contenedores (los volúmenes persisten)
docker compose down
# Para y elimina también los volúmenes
docker compose down -v

Guarda en ~/practica8/entrega/84_compose.txt.


Guarda la verificación completa del proyecto final:

ventana terminal
mkdir -p ~/practica8/entrega
PHP_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.txt

Para 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"
ventana terminal
bash -lc 'set -euo pipefail
PHP_VER=$(php -r "echo PHP_MAJOR_VERSION.'"'"'.'"'"'.PHP_MINOR_VERSION;" 2>/dev/null || echo "8.3")
# Error 1: detén PHP-FPM
sudo 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/misite
sudo chmod -R 700 /var/www/misite
# Error 3: configura MariaDB con bind-address incorrecto
sudo 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.cnf
sudo 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"'
  • curl http://misite.local devuelve 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

Repara la pila completa siguiendo este flujo de diagnóstico en capas (de fuera hacia dentro):

1. ¿nginx responde? → curl + systemctl status nginx
2. ¿PHP-FPM está activo? → systemctl status php*-fpm + journalctl
3. ¿El socket existe? → ls -la /var/run/php/
4. ¿nginx puede leer los ficheros web? → ls -la /var/www/misite
5. ¿MariaDB acepta conexiones? → mysql -u cursouser -p cursodb
6. ¿La app funciona end-to-end? → curl http://misite.local

Guarda en ~/practica8/entrega/85_solucion.txt:

ventana terminal
{
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