Práctica Guiada: Despliegue de Infraestructura Multiservicio con Docker

Audiencia: Ingeniería de Desarrollo Móvil / Sistemas Organizacionales.
Duración Estimada: 3 Horas.

Objetivo de la Práctica

Al finalizar esta sesión, el alumno será capaz de aprovisionar un entorno multi-contenedor aislado, comprender las reglas de mapeo de puertos de red de Docker y conectar arquitecturas distribuidas (Frontend, Backend, Base de Datos y Directorio Activo) garantizando compatibilidad multiplataforma nativa y validando conexiones en tiempo real.

Bloque 1: Preparación del Entorno e Instalación (45 min)

Trabajaremos directamente sobre un servidor Ubuntu limpio. Siga los pasos secuenciales para configurar el motor oficial:

Paso 1: Actualizar el Sistema 5 min

Sincronizamos los índices de los repositorios locales con los del servidor remoto.

sudo apt update && sudo apt upgrade -y
Paso 2: Herramientas de Seguridad Previas 5 min

Instalamos paquetes que permiten a apt transferir datos y verificar llaves criptográficas sobre HTTPS.

sudo apt install -y ca-certificates curl gnupg lsb-release
Paso 3: Añadir Repositorio Oficial de Docker 15 min

Agregamos la llave GPG pública de la organización de Docker para validar la integridad de las descargas.

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/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/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Paso 4: Instalación del Motor y Orquestador 15 min

Actualizamos los repositorios e instalamos Docker junto con el plugin nativo de Docker Compose v2.

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Paso 5: Gestión de Permisos y Activación del Socket 5 min

Vinculamos al usuario al grupo del motor y refrescamos los sockets del sistema para evitar requerir el prefijo sudo:

# Añadir al grupo docker
sudo usermod -aG docker $USER

# Aplicar cambios de grupo inmediatamente a la terminal activa
newgrp docker

# Asegurar permisos correctos sobre el socket de comunicación de Docker
sudo chmod 666 /var/run/docker.sock

Bloque 2: Orquestación con Docker Compose (1 hora)

Para coordinar múltiples contenedores de forma ágil, utilizaremos un archivo declarativo estructurado en formato YAML.

Estrategia Multi-Arquitectura Nativa: Para asegurar el despliegue tanto en servidores Intel/AMD de 64 bits como en plataformas ARM64 (Apple Silicon o microservidores Cloud), este entorno utiliza compilaciones cruzadas estables de nitnelave para OpenLDAP y phpLDAPadmin, evitando el uso de traducciones por software emulado que congelan los contenedores.

Cree el directorio de trabajo del proyecto: mkdir practica-inventario && cd practica-inventario e implemente el siguiente archivo:

Archivo: docker-compose.yml

networks:
  inventario_net:
    driver: bridge

volumes:
  postgres_data:
  ldap_data:
  ldap_config:

services:
  db:
    image: postgres:15-alpine
    container_name: inventario_db
    environment:
      POSTGRES_USER: admin_user
      POSTGRES_PASSWORD: SecretPassword123
      POSTGRES_DB: db_inventario
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - inventario_net

  ldap:
    image: nitnelave/openldap:latest
    container_name: inventario_ldap
    environment:
      LDAP_ORGANISATION: "Universidad"
      LDAP_DOMAIN: "clase.edu"
      LDAP_ADMIN_PASSWORD: "AdminLdapPassword"
    ports:
      - "389:389"
    volumes:
      - ldap_data:/var/lib/ldap
      - ldap_config:/etc/ldap/slapd.d
    networks:
      - inventario_net

  phpldapadmin:
    image: nitnelave/phpldapadmin:latest
    container_name: inventario_phpldapadmin
    environment:
      PHPLDAPADMIN_LDAP_HOSTS: "ldap"
      PHPLDAPADMIN_HTTPS: "false"
    ports:
      - "8080:80"
    depends_on:
      - ldap
    networks:
      - inventario_net

  backend:
    image: node:18-alpine
    container_name: inventario_api
    working_dir: /app
    ports:
      - "3000:3000"
    volumes:
      - ./backend:/app
    command: sh -c "npm install && npm start"
    depends_on:
      - db
      - ldap
    networks:
      - inventario_net

  frontend:
    image: nginx:alpine
    container_name: inventario_web
    ports:
      - "80:80"
    volumes:
      - ./frontend:/usr/share/nginx/html
    networks:
      - inventario_net

Bloque 3: Construcción de la Aplicación de Inventario (1 hora)

Daremos de alta el código fuente básico que Docker montará de forma dinámica para simular la API REST e instrumentar las comprobaciones de salud de red (Health-checks).

1. El Backend (API REST en Node.js + Test de Sockets)

Cree la carpeta de código: mkdir backend e integre los siguientes archivos dentro de ella:

Archivo: backend/package.json

{
  "name": "api-inventario",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "cors": "^2.8.5"
  }
}

Archivo: backend/server.js

const express = require('express');
const cors = require('cors');
const net = require('net'); // Librería nativa de Node para verificar puertos de red
const app = express();
const PORT = 3000;

app.use(cors());
app.use(express.json());

// Base de datos volátil en memoria para el inventario
let inventario = [
    { id: 1, item: "Dispositivos Android Test", cantidad: 5, rolRequerido: "Admin" },
    { id: 2, item: "Servidores Proxmox Nodos", cantidad: 2, rolRequerido: "Consultor" }
];

// FUNCIÓN DE DIAGNÓSTICO: Verifica si los servicios de infraestructura están respondiendo
function verificarConexion(host, puerto, nombreServicio) {
    const socket = new net.Socket();
    socket.setTimeout(2000); // 2 segundos de timeout
    
    socket.connect(puerto, host, () => {
        console.log(`[OK] Conexión establecida exitosamente con el servicio: ${nombreServicio} (${host}:${puerto})`);
        socket.destroy();
    });

    socket.on('error', (err) => {
        console.log(`[ERROR] No se pudo conectar a ${nombreServicio} (${host}:${puerto}). Detalle: ${err.message}`);
        socket.destroy();
    });

    socket.on('timeout', () => {
        console.log(`[TIMEOUT] Tiempo de espera agotado al conectar con ${nombreServicio} (${host}:${puerto})`);
        socket.destroy();
    });
}

// Endpoints de la API REST
app.get('/api/inventario', (req, res) => {
    console.log("Conexión entrante desde Frontend/Móvil a la API");
    res.json(inventario);
});

app.post('/api/inventario', (req, res) => {
    const { item, cantidad, userRole } = req.body;
    if (userRole !== 'Admin') {
        return res.status(403).json({ error: "Permisos insuficientes. Se requiere rol de Administrador." });
    }
    const newItem = { id: inventario.length + 1, item, cantidad, rolRequerido: "Admin" };
    inventario.push(newItem);
    res.status(201).json(newItem);
});

// Levantar el servidor y ejecutar las pruebas de conexión de infraestructura inmediatamente
app.listen(PORT, () => {
    console.log(`================================================================`);
    console.log(` Web Service corriendo internamente en puerto ${PORT}`);
    console.log(` Iniciando pruebas de conectividad de infraestructura interna...`);
    console.log(`================================================================`);
    
    // Docker resuelve automáticamente los nombres de los servicios usando su DNS interno
    verificarConexion('db', 5432, 'PostgreSQL DB');
    verificarConexion('ldap', 389, 'OpenLDAP Server');
});

Concepto Central de Redes en Docker: El código de Node.js no utiliza IPs físicas complejas (como 172.18.0.3). Docker Compose levanta un servidor DNS interno embebido. Cuando el backend intenta conectarse al host 'db' o 'ldap', Docker intercepta la petición de nombres de dominio y la traduce instantáneamente a la IP interna del contenedor destino correspondiente.

2. El Frontend (Panel Web de Control)

Cree la carpeta de código: mkdir frontend e integre el archivo de interfaz de usuario:

Archivo: frontend/index.html

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Consola de Inventario - Docker</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 40px; background-color: #f4f6f9; color: #333; }
        .container { max-width: 600px; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
        button { background: #007bff; color: white; border: none; padding: 10px 15px; cursor: pointer; border-radius: 4px; }
        button:hover { background: #0056b3; }
        input { padding: 8px; width: 80%; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px; }
        ul { list-style: none; padding: 0; }
        li { background: #eee; margin: 5px 0; padding: 10px; border-radius: 4px; display: flex; justify-content: space-between; }
    </style>
</head>
<body>
    <div class="container">
        <h2>Panel de Gestión de Inventario</h2>
        <h3>1. Rol de Acceso (Simulado LDAP)</h3>
        <select id="userRole" style="padding: 8px; margin-bottom: 20px;">
            <option value="Consultor">Consultor (Solo Ver)</option>
            <option value="Admin">Administrador (Crear e Inventariar)</option>
        </select>

        <h3>2. Agregar al Inventario</h3>
        <input type="text" id="itemName" placeholder="Nombre del artículo"><br>
        <input type="number" id="itemQty" placeholder="Cantidad" value="1"><br>
        <button onclick="addItem()">Registrar en Inventario</button>

        <h3>3. Existencias actuales</h3>
        <button onclick="loadInventario()" style="background:#28a745">Actualizar Lista</button>
        <ul id="lista-inventario"></ul>
    </div>

    <script>
        const API_URL = `http://${window.location.hostname}:3000/api/inventario`;

        async function loadInventario() {
            try {
                const response = await fetch(API_URL);
                const data = await response.json();
                const lista = document.getElementById('lista-inventario');
                lista.innerHTML = '';
                data.forEach(i => {
                    lista.innerHTML += `<li><span><strong>${i.item}</strong> (${i.cantidad} unidades)</span> <small style="color:gray">Rol: ${i.rolRequerido}</small></li>`;
                });
            } catch (err) {
                alert("Error al conectar con el Web Service (Backend)");
            }
        }

        async function addItem() {
            const item = document.getElementById('itemName').value;
            const cantidad = parseInt(document.getElementById('itemQty').value);
            const userRole = document.getElementById('userRole').value;

            try {
                const response = await fetch(API_URL, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ item, cantidad, userRole })
                });
                if(response.status === 403) {
                    alert("Error 403: Tu rol de LDAP no permite registrar datos.");
                } else {
                    loadInventario();
                }
            } catch (err) {
                alert("Error al enviar datos");
            }
        }
        loadInventario();
    </script>
</body>
</html>

Bloque 4: Despliegue y Pruebas en Vivo (15 min)

Control de Contingencia de Caché: Si previamente se descargaron imágenes con errores de plataforma en el host, ejecute una purga de imágenes viejas antes de levantar el entorno final:

docker compose down
docker rmi osixia/openldap:1.5.0 osixia/phpldapadmin:0.9.0 2>/dev/null
docker compose pull
docker compose up -d

Para inicializar toda la arquitectura orquestada normal, ejecute el comando en la raíz de su proyecto:

docker compose up -d

Comandos obligatorios de inspección y comprobación de red:

Para constatar el apretón de manos (handshake) que realiza el Backend con la Base de datos y el servidor LDAP, los alumnos deben inspeccionar la salida estándar del flujo de Node:

# Verificación del estado físico de los sockets y puertos expuestos
docker compose ps

# Monitoreo activo de logs del Web Service para comprobar las líneas [OK] de red
docker compose logs -f backend

Tabla Didáctica de Referencia de Puertos

Servicio de Infraestructura Nombre del Contenedor Puerto Interno Puerto Host (Exterior) Propósito Técnico
OpenLDAP inventario_ldap 389 389 Base de datos de credenciales, roles y accesos corporativos.
phpLDAPadmin inventario_phpldapadmin 80 8080 Consola de control gráfico para la gestión de nodos jerárquicos LDAP.
PostgreSQL inventario_db 5432 5432 Base de datos de almacenamiento relacional persistente física de ítems.
Backend API inventario_api 3000 3000 Endpoints del Web Service que consumirá la App nativa en Android.
Frontend Web inventario_web 80 80 Dashboard web administrativo básico para monitoreo.