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:
Sincronizamos los índices de los repositorios locales con los del servidor remoto.
sudo apt update && sudo apt upgrade -y
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
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
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
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. |