Recuperatorio

Trabajo Práctico · Contenedores con Docker

Virtualización con contenedores y arquitecturas dinámicas

Materia: Sistemas Operativos — Tecnicatura Universitaria en Desarrollo de Software
Cátedra: Prof. Fernando A. Mas

1 · Objetivos

Al finalizar este trabajo práctico de recuperatorio, el/la estudiante deberá ser capaz de:

Dificultad incrementada: este recuperatorio mantiene la misma estructura y criterios de evaluación del TP original, pero cambia el caso de uso (servidor web dinámico con API Node.js/Express) e incrementa un ~20% la complejidad técnica incorporando variables de entorno, archivos .env, healthchecks y comunicación entre capas.

2 · Requisitos Previos

3 · Consigna del Recuperatorio (Partes A a E)

Parte A · Diagnóstico e Inspección Avanzada (Base)

  1. Verificar la instalación ejecutando docker run hello-world. Capturar la salida y explicar brevemente en el informe los 4 pasos que ejecuta la API de Docker internamente (pull, create, start, output).
  2. Descargar y ejecutar un contenedor de diagnóstico liviano expuesto en el host:
docker run -d -p 8085:80 --name servidor-test -e PORT=80 nginx:alpine
  1. Inspeccionar el contenedor con docker inspect servidor-test:
    • Identificar la IP interna asignada por la red bridge.
    • Identificar la variable de entorno configurada (PORT=80).
  2. Extraer las estadísticas de consumo de recursos en tiempo real ejecutando docker stats servidor-test durante 10 segundos.

Parte B · Construcción de una API Backend con Node.js (+20% complejidad)

  1. Crear una carpeta de proyecto con la siguiente estructura:
recuperatorio-docker/
├── Dockerfile
├── .dockerignore
├── .env
└── app/
    ├── package.json
    └── server.js
  1. Desarrollar una mini API en server.js (Node.js/Express o equivalente) que responda en el puerto definido por la variable de entorno PORT (por defecto 3000). Debe incluir:
    • Ruta /: retorna un JSON con el nombre del alumno, legajo y el mensaje "API del Recuperatorio activa".
    • Ruta /health: retorna un JSON { "status": "OK", "timestamp": "..." }.
  2. Escribir un Dockerfile optimizado:
    • Utilizar una imagen base liviana (node:18-alpine o superior).
    • Definir el directorio de trabajo (WORKDIR /app).
    • Copiar primero los archivos de dependencias (package*.json) e instalar (npm install --production).
    • Copiar el resto del código de la aplicación.
    • Definir EXPOSE y el comando predeterminado CMD.
  3. Construcción y ejecución:
# Construir la imagen:
docker build -t <apellido>-api:v1 .

# Ejecutar pasando una variable de entorno en la línea de comandos:
docker run -d -p 3000:3000 --env PORT=3000 --name mi-api <apellido>-api:v1
  1. Desarrollo en caliente con Bind Mount: probar modificaciones al código backend montando un volumen sin reconstruir la imagen:
docker run -d -p 3001:3000 -v $(pwd)/app:/app --name mi-api-dev <apellido>-api:v1

Modificar la respuesta del JSON en server.js y verificar la actualización.

Parte C · Orquestación Multi-Servicio y Proxy Inverso (+20% complejidad)

  1. Crear un archivo docker-compose.yml que orqueste dos servicios interconectados:
    • Servicio api: basado en la imagen creada en la Parte B; configurado con variables de entorno leídas desde un archivo .env; debe incluir una instrucción healthcheck que consulte periódicamente http://localhost:3000/health.
    • Servicio proxy: servidor Nginx actuando como Reverse Proxy. Redirige el tráfico del puerto host 8080 al contenedor api utilizando el nombre del servicio como hostname interno (http://api:3000).
  2. Ejecución y redes:
    • Levantar el stack con docker compose up -d.
    • Ejecutar docker network inspect sobre la red creada automáticamente por Compose y explicar cómo se resuelven los nombres de dominio internos (DNS integrado de Docker).

Parte D · Buenas Prácticas y Limpieza

  1. .dockerignore: crear un archivo que excluya explícitamente node_modules, .git, .env y archivos .log. Explicar por qué es crítico evitar copiar node_modules en la construcción de la imagen.
  2. Comparativa de imágenes: comparar el tamaño final de la imagen Alpine generada frente a una imagen base completa (node:latest). Incluir captura de docker images.
  3. Limpieza completa: detener todos los entornos y realizar una limpieza profunda dejando el comando ejecutado y su salida:
docker stop $(docker ps -aq)
docker rm $(docker ps -aq)
docker rmi $(docker images -q)
docker system prune -f

Parte E · Publicación en Docker Hub y Verificación de Cátedra

  1. Iniciar sesión desde la terminal con docker login.
  2. Etiquetar la imagen con el formato <usuario-dockerhub>/<apellido>-api:v1 y subirla a Docker Hub:
docker tag <apellido>-api:v1 <usuario-dockerhub>/<apellido>-api:v1
docker push <usuario-dockerhub>/<apellido>-api:v1
  1. Prueba de despliegue directo: proporcionar en el informe el comando exacto para que el docente pueda verificar la entrega:
docker pull <usuario-dockerhub>/<apellido>-api:v1
docker run -d -p 3000:3000 -e PORT=3000 --name test-evaluacion <usuario-dockerhub>/<apellido>-api:v1
  1. Eliminar la imagen local (docker rmi) y volver a descargarla desde el registro público para confirmar su correcto funcionamiento fuera del entorno de desarrollo.

3.BIS · Solución de Referencia · Dockerfile Resuelto (Parte B)

A continuación se muestra el Dockerfile íntegramente resuelto para la Parte B, cumpliendo todos los puntos exigidos por la consigna: imagen base liviana, optimización por capas de caché, instalación de dependencias, EXPOSE y CMD.

Dockerfile
# =============================================================================
# Dockerfile · TP Recuperatorio - API Backend Node.js/Express
# Imagen optimizada para producción, basada en Alpine Linux.
# =============================================================================

# 1) IMAGEN BASE LIVIANA: node:18-alpine usa Alpine Linux (~5MB de base) en
#    lugar de Debian completo, reduciendo drásticamente el tamaño final
#    de la imagen (comparar luego con node:latest en la Parte D).
FROM node:18-alpine

# 2) ENTORNO: valor por defecto del puerto (puede sobreescribirse con
#    -e PORT=... en docker run o con el archivo .env en docker-compose).
ENV PORT=3000 \
    NODE_ENV=production

# 3) DIRECTORIO DE TRABAJO: todos los comandos siguientes se ejecutan
#    relativos a /app dentro del contenedor.
WORKDIR /app

# 4) CACHÉ DE CAPAS (clave): copiamos SOLO los manifiestos de dependencias
#    en primer lugar. Docker cachea esta capa: si package*.json no cambia,
#    no se vuelve a ejecutar npm install al reconstruir, aunque cambie el
#    código fuente. Esto acelera enormemente las reconstrucciones durante
#    el desarrollo.
COPY app/package*.json ./

# 5) INSTALACIÓN DE DEPENDENCIAS: --production omite las devDependencies
#    (tests, linters, etc.), dejando la imagen final más liviana.
#    npm ci garantiza builds reproducibles a partir del package-lock.json.
RUN npm install --production

# 6) CÓDIGO FUENTE: recién ahora copiamos el resto de la aplicación.
COPY app/ .

# 7) PUERTO: documenta que el contenedor escucha en el 3000.
EXPOSE 3000

# 8) ARRANQUE: proceso principal del contenedor (PID 1).
CMD ["node", "server.js"]

Explicación línea por línea

InstrucciónQué hace y por qué
FROM node:18-alpine Base oficial de Node.js sobre Alpine: imagen final de ~50-100 MB en lugar de ~1 GB de node:latest (comparativa pedida en la Parte D).
ENV PORT=3000 NODE_ENV=production Define las variables de entorno por defecto; server.js debe leer process.env.PORT || 3000.
WORKDIR /app Directorio de trabajo aislado dentro del contenedor.
COPY app/package*.json ./ Copia primero solo los manifiestos: aprovecha la caché de capas de Docker. Mientras las dependencias no cambien, npm install se salta en cada rebuild.
RUN npm install --production Instala solo las dependencias de producción, sin devDependencies.
COPY app/ . Copia el código fuente. Recordá excluir node_modules con .dockerignore (Parte D) para no pisar los módulos instalados dentro de la imagen.
EXPOSE 3000 Documenta el puerto de escucha de la API.
CMD ["node", "server.js"] Comando por defecto al iniciar el contenedor. Formato exec (JSON array): Node recibe las señales del sistema directamente (apagado limpio).

Verificación

docker build -t <apellido>-api:v1 .
docker run -d -p 3000:3000 --env PORT=3000 --name mi-api <apellido>-api:v1
curl http://localhost:3000/
curl http://localhost:3000/health

4 · Entregables

Informe en formato PDF

Código fuente

Comprimido (.zip) o enlace al repositorio Git sin la carpeta node_modules.

Envío por correo electrónico

Destinatario: coordi.tecnicatura@uccuyo.edu.ar
Asunto: [SO] RECUPERATORIO TP Contenedores — Apellido, Nombre

5 · Rúbrica de Evaluación

CriterioPuntos
Parte A: inspección, métricas de consumo (docker stats) e IP interna10 pts
Parte B: Dockerfile bien estructurado, manejo de variables de entorno y bind mount20 pts
Parte C: docker-compose.yml funcional con 2 servicios (api + proxy), DNS interno y healthcheck25 pts
Parte D: buenas prácticas (.dockerignore, caché de capas, optimización y limpieza)15 pts
Parte E: publicación exitosa en Docker Hub y verificación del comando pull + run20 pts
Informe: claridad técnica, capturas explicativas y conclusiones10 pts
Total100 pts