Saltar al contenido principal

Módulo 16 — Virtualización y contenedores

Introducción

Linux es la plataforma sobre la que nació la revolución de los contenedores —y también es el substrato invisible que ejecuta el 90% de las cargas de trabajo en la nube. Entender virtualización y contenedores no es opcional para un administrador moderno: es el conocimiento central de la infraestructura actual.

Este módulo recorre el stack completo: desde la virtualización KVM que crea máquinas virtuales completas, hasta los namespaces y cgroups del kernel que explican por qué un contenedor es tan diferente de una VM, pasando por Docker y Podman para el día a día y terminando con una introducción honesta a Kubernetes.

Las bases están sólidamente asentadas en módulos anteriores: procesos y cgroups en el Módulo 09, almacenamiento en el Módulo 12, red y namespaces de red en el Módulo 11, rendimiento y monitorización en el Módulo 15.

Objetivos de aprendizaje

Al finalizar este módulo, serás capaz de:

  • ✅ Explicar la diferencia entre hipervisores tipo 1 y 2, y entre VMs y contenedores
  • ✅ Crear y gestionar VMs con KVM/QEMU y libvirt usando virsh y virt-manager
  • ✅ Explicar qué son los namespaces y cgroups y cómo implementan los contenedores
  • ✅ Gestionar el ciclo de vida de contenedores con Docker y Podman
  • ✅ Construir imágenes optimizadas con Dockerfile usando multi-stage builds
  • ✅ Configurar redes y volúmenes de contenedores
  • ✅ Orquestar aplicaciones multi-contenedor con Docker Compose
  • ✅ Ejecutar cargas de trabajo con Kubernetes usando kubectl y minikube/k3s

16.1 — Virtualización: conceptos fundamentales

Hipervisores tipo 1 y tipo 2

Hipervisor TIPO 1 (bare-metal):
Corre directamente sobre el hardware sin SO intermedio.
El hipervisor ES el SO de base.

Hardware físico
└── Hipervisor (tipo 1)
├── VM1 (Linux)
├── VM2 (Windows)
└── VM3 (FreeBSD)

Ejemplos: VMware ESXi, Microsoft Hyper-V, Xen, KVM (*)
Ventaja: mínimo overhead, control total del hardware
Uso: centros de datos, nubes privadas y públicas

(*) KVM es técnicamente tipo 1: convierte el kernel Linux en un hipervisor.
El kernel es el bare-metal sobre el que corre KVM.

Hipervisor TIPO 2 (hosted):
Corre SOBRE un sistema operativo convencional.

Hardware físico
└── Sistema operativo anfitrión (Host OS)
└── Hipervisor (tipo 2)
├── VM1 (Linux)
└── VM2 (Windows)

Ejemplos: VirtualBox, VMware Workstation, Parallels
Ventaja: fácil de instalar y usar en un escritorio
Desventaja: doble capa de SO → más overhead, menos rendimiento
Uso: desarrollo, testing, laboratorios personales

Virtualización asistida por hardware

# Verificar soporte de virtualización en el CPU (Intel VT-x o AMD-V)
grep -E "vmx|svm" /proc/cpuinfo | head -2
# vmx → Intel VT-x (Virtual Machine eXtensions)
# svm → AMD-V (Secure Virtual Machine)

# Si no aparece nada: revisar la BIOS/UEFI y activar "Intel VT-x" o "AMD-V"

# Verificar que el módulo KVM está cargado (módulo 13)
lsmod | grep kvm
# kvm_intel o kvm_amd (según el procesador)
# kvm (el módulo base)

# Verificar que el sistema puede ejecutar VMs con KVM
sudo kvm-ok # Instalar con: sudo apt install cpu-checker
# KVM acceleration can be used → todo correcto

VMs vs. contenedores: qué aísla cada uno

Máquina Virtual (VM):
┌────────────────────────────────────────┐
│ Aplicación │
│ Bibliotecas y dependencias │
│ Sistema operativo COMPLETO (kernel) │
│ Hardware virtual (vCPU, vRAM, vNIC) │
└────────────────────────────────────────┘
↓ sobre el ↓
Hipervisor / KVM

• Aislamiento FUERTE: kernel independiente, hardware virtual
• Overhead: segundos para arrancar, MB de imagen, RAM para el SO
• Caso de uso: múltiples SOs distintos, aislamiento de seguridad fuerte,
aplicaciones legacy con dependencias del SO

Contenedor:
┌────────────────────────────────────────┐
│ Aplicación │
│ Bibliotecas y dependencias │
│ (NO kernel propio: usa el del host) │
└────────────────────────────────────────┘
↓ sobre el ↓
Kernel del host (namespaces + cgroups)

• Aislamiento LÓGICO: proceso separado que comparte el kernel
• Overhead: milisegundos para arrancar, MB de imagen, sin RAM para SO
• Caso de uso: microservicios, CI/CD, despliegues rápidos,
aplicaciones stateless que escalan horizontalmente

¿Cuándo elegir qué?
→ Necesitas un SO diferente (Windows en Linux): VM obligatoria
→ Aislamiento de seguridad máximo (untrusted code): VM preferible
→ Microservicio que escala en segundos: contenedor
→ CI/CD con decenas de pruebas paralelas: contenedores
→ Laboratorio con múltiples distros: VMs (o ambos combinados)

16.2 — KVM, QEMU y libvirt

La pila de virtualización de Linux

Pila KVM/QEMU/libvirt:

┌─────────────────────────────────────────────────────────┐
│ Herramientas de gestión │
│ virt-manager (GUI) │ virsh (CLI) │ virt-install │
└──────────────────────┴───────────────┴─────────────────┘

┌─────────────────────────────────────────────────────────┐
│ libvirt: API de abstracción de virtualización │
│ Daemon: libvirtd / virtqemud │
│ Soporta: KVM, Xen, LXC, VirtualBox, VMware │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ QEMU: emulador de hardware │
│ Emula: CPU, RAM, disco (virtio), red (virtio-net) │
│ Con KVM: la emulación de CPU la hace el hardware real │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ KVM (Kernel-based Virtual Machine) │
│ Módulos del kernel: kvm.ko + kvm_intel.ko/kvm_amd.ko │
│ Proporciona: extensiones de virtualización del CPU │
└─────────────────────────────────────────────────────────┘

Instalación y configuración

# Instalar el stack completo
sudo apt install qemu-kvm libvirt-daemon-system libvirt-clients \
bridge-utils virt-manager virtinst cpu-checker

# Añadir el usuario actual al grupo libvirt (no tener que usar sudo)
sudo usermod -aG libvirt $USER
sudo usermod -aG kvm $USER
# (cerrar sesión y volver a entrar para que tome efecto)

# Verificar que libvirtd está activo
sudo systemctl enable --now libvirtd
virsh -c qemu:///system list --all # Listar VMs del sistema
virsh -c qemu:///session list --all # Listar VMs del usuario actual

Crear VMs con virt-install

# Crear una VM Ubuntu Server desde ISO (modo texto)
sudo virt-install \
--name ubuntu-server \
--ram 2048 \
--vcpus 2 \
--disk path=/var/lib/libvirt/images/ubuntu-server.qcow2,size=20 \
--os-variant ubuntu22.04 \
--network network=default \
--graphics none \
--console pty,target_type=serial \
--cdrom /path/to/ubuntu-22.04-server.iso

# Crear una VM con imagen cloud (cloud-init, sin instalación interactiva)
# 1. Descargar imagen cloud de Ubuntu
wget https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img

# 2. Crear disco basado en la imagen (thin provisioning)
sudo qemu-img create -F qcow2 -b jammy-server-cloudimg-amd64.img \
-f qcow2 /var/lib/libvirt/images/mi-vm.qcow2 20G

# 3. Crear archivo cloud-init con usuario y clave SSH
cat > user-data << 'EOF'
#cloud-config
hostname: mi-vm
users:
- name: ubuntu
ssh-authorized-keys:
- ssh-ed25519 AAAA...tu-clave-publica... usuario@equipo
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
EOF

sudo cloud-localds /var/lib/libvirt/images/seed.iso user-data

# 4. Lanzar la VM
sudo virt-install \
--name mi-vm \
--ram 2048 \
--vcpus 2 \
--disk /var/lib/libvirt/images/mi-vm.qcow2 \
--disk /var/lib/libvirt/images/seed.iso,device=cdrom \
--os-variant ubuntu22.04 \
--network network=default \
--graphics none \
--import

Gestión con virsh

# Estado de las VMs
virsh list --all # Todas las VMs (activas e inactivas)
virsh dominfo mi-vm # Información detallada de una VM

# Ciclo de vida
virsh start mi-vm # Arrancar
virsh shutdown mi-vm # Apagar limpiamente (como shutdown -h)
virsh destroy mi-vm # Apagado forzoso (como cortar la corriente)
virsh suspend mi-vm # Pausar (guardar en RAM)
virsh resume mi-vm # Reanudar
virsh reboot mi-vm # Reiniciar

# Consola y conexión
virsh console mi-vm # Consola serie (Ctrl+] para salir)
# o SSH una vez que esté corriendo:
virsh domifaddr mi-vm # Ver la IP de la VM
ssh ubuntu@$(virsh domifaddr mi-vm | grep ipv4 | awk '{print $4}' | cut -d/ -f1)

# Snapshots: estados guardados de la VM
virsh snapshot-create-as mi-vm --name "antes-de-actualizar" --description "estado limpio"
virsh snapshot-list mi-vm # Listar snapshots
virsh snapshot-revert mi-vm --snapshotname "antes-de-actualizar" # Revertir
virsh snapshot-delete mi-vm --snapshotname "antes-de-actualizar" # Eliminar

# Clonar una VM
virsh shutdown mi-vm # La VM debe estar apagada
virt-clone --original mi-vm --name mi-vm-clon \
--file /var/lib/libvirt/images/mi-vm-clon.qcow2

# Gestión de XML (configuración raw)
virsh dumpxml mi-vm > mi-vm.xml # Exportar configuración
virsh edit mi-vm # Editar en $EDITOR
virsh define mi-vm.xml # Importar/actualizar configuración

Redes virtuales en KVM

# Red NAT por defecto (virbr0): las VMs tienen IP privada, NAT al exterior
virsh net-list --all
# Name State Autostart Persistent
# default active yes yes

virsh net-info default # Información de la red default
# Red: 192.168.122.0/24, DHCP: 192.168.122.2-254, gateway: 192.168.122.1

# Red bridge: las VMs se conectan directamente a la red LAN del host
# Requiere configurar un bridge en el host (con NetworkManager o Netplan)
# Para Netplan /etc/netplan/01-bridge.yaml:
# network:
# bridges:
# br0:
# interfaces: [eth0]
# dhcp4: true

# Crear una red virtual aislada (solo entre VMs, sin acceso exterior)
virsh net-define - << 'EOF'
<network>
<name>red-interna</name>
<bridge name='virbr1'/>
<ip address='10.0.0.1' netmask='255.255.255.0'>
<dhcp>
<range start='10.0.0.2' end='10.0.0.254'/>
</dhcp>
</ip>
</network>
EOF
virsh net-start red-interna
virsh net-autostart red-interna

16.3 — Qué es un contenedor por dentro

Los contenedores no son una tecnología nueva en el kernel: son una combinación de características del kernel Linux que llevan más de una década disponibles.

Namespaces: aislamiento de recursos

Linux namespaces: el kernel puede presentar vistas distintas del sistema
a distintos grupos de procesos.

Tipo de namespace Lo que aísla
──────────────────────────────────────────────────────────────────
PID Los procesos: el init del contenedor es el PID 1
dentro del contenedor, aunque sea otro PID en el host

Network (net) Interfaces de red, rutas, reglas de firewall.
El contenedor tiene su propia interfaz virtual (veth).

Mount (mnt) El árbol de directorios: el contenedor ve su propio
sistema de archivos raíz (imagen del contenedor).

UTS Hostname y domainname: el contenedor puede tener
su propio hostname.

IPC Colas de mensajes, semáforos y memoria compartida.
Aislados entre contenedores.

User UIDs y GIDs: el root dentro del contenedor puede
mapearse a un usuario sin privilegios en el host.
(Fundamental para contenedores rootless)

cgroup Namespace del subsistema cgroup (para ocultar la
jerarquía de cgroups del host)

Time Reloj del sistema (solo kernel ≥ 5.6)
# Crear un namespace manualmente (para entender qué hace Docker)
# Namespace de red aislado:
sudo ip netns add mi-ns # Crear network namespace
sudo ip netns exec mi-ns ip link show # Ver las interfaces DENTRO del ns
# Solo aparece 'lo' (loopback) — completamente aislado de la red del host

# Ejecutar un proceso en un namespace de PID nuevo
sudo unshare --pid --fork --mount-proc /bin/bash
# Dentro de este bash: ps aux muestra solo los procesos de este namespace
# PID 1 = bash (no systemd)
echo $$ # PID 1 dentro del namespace
ps aux # Solo ves el bash y ps

cgroups: límites de recursos

# cgroups (control groups): limitar CPU, memoria, I/O de grupos de procesos
# (ver Módulo 09 para la integración con systemd)

# Los contenedores Docker usan cgroups v2 automáticamente
# Ver los cgroups de un contenedor en ejecución:
cat /sys/fs/cgroup/system.slice/docker-CONTAINER_ID.scope/memory.current
cat /sys/fs/cgroup/system.slice/docker-CONTAINER_ID.scope/cpu.stat

# Ejemplo: el límite de memoria de un contenedor Docker es:
# cat /sys/fs/cgroup/.../memory.max → el valor configurado con --memory

Imágenes por capas: OverlayFS

Imagen de contenedor = conjunto de capas de solo lectura (read-only layers)
Contenedor en ejecución = capas R/O + una capa R/W encima

FROM ubuntu:22.04 → Capa base (SO mínimo)
RUN apt install nginx → Capa 2 (nginx instalado)
COPY index.html /var/www → Capa 3 (el contenido web)

Cuando arranca el contenedor:
┌──────────────────────────────────────┐
│ Capa de escritura (R/W) ← cambios │ Solo existe mientras corre el contenedor
│ del proceso (se pierde al parar, salvo volúmenes)
├──────────────────────────────────────┤
│ Capa 3: COPY index.html (R/O) │
├──────────────────────────────────────┤
│ Capa 2: RUN apt nginx (R/O) │
├──────────────────────────────────────┤
│ Capa 1: FROM ubuntu:22.04 (R/O) │
└──────────────────────────────────────┘

OverlayFS unifica las capas en una vista coherente del sistema de archivos.
Ventaja: múltiples contenedores comparten las capas R/O → ahorro de espacio.

LXC/LXD: contenedores de sistema

# LXC: contenedores de sistema (vs. contenedores de aplicación como Docker)
# LXC es como una VM muy ligera: tiene init (systemd), múltiples procesos, SSH...
# LXD: demonio y CLI de gestión para LXC

sudo apt install lxd
sudo lxd init # Configuración inicial (aceptar defaults)

# Crear y usar un contenedor LXC
lxc launch ubuntu:22.04 mi-lxc # Descargar imagen e iniciar
lxc list # Ver contenedores activos
lxc exec mi-lxc -- bash # Shell dentro del contenedor
lxc stop mi-lxc # Parar
lxc delete mi-lxc # Eliminar

# Snapshots en LXD
lxc snapshot mi-lxc snap1 # Tomar snapshot
lxc restore mi-lxc snap1 # Restaurar

16.4 — Docker / Podman: ciclo de vida básico

Docker vs. Podman

Docker:
• Arquitectura cliente-servidor: docker CLI → dockerd (daemon root)
• El daemon corre como root → potencial escalada de privilegios
• Registro por defecto: Docker Hub
• Docker Compose integrado (docker compose)
• Amplio ecosistema, documentación masiva

Podman:
• Sin daemon: cada contenedor es un proceso hijo directo del shell
• Rootless nativo: los contenedores corren sin privilegios de root
• Compatible con la CLI de Docker (podman ≡ docker en casi todos los comandos)
• Genera unidades systemd para contenedores (Quadlet)
• Preferido en entornos de seguridad alta (RHEL, Fedora por defecto)

Para el administrador: los comandos son intercambiables en el 95% de los casos.
Para producción: Podman rootless es más seguro; Docker tiene más ecosistema.
# Instalar Docker
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER # Añadir usuario al grupo docker (re-login)
docker --version

# Instalar Podman (Debian/Ubuntu)
sudo apt install podman
podman --version

# Verificar que funciona
docker run hello-world # Docker
podman run hello-world # Podman (misma imagen, misma salida)

Ciclo de vida de contenedores

# EJECUTAR un contenedor
docker run ubuntu:22.04 echo "Hola" # Ejecutar y salir
docker run -it ubuntu:22.04 bash # Interactivo con terminal
docker run -d nginx # En background (detached)
docker run -d -p 8080:80 nginx # Publicar puerto 80 → 8080 del host
docker run -d --name mi-nginx nginx # Con nombre personalizado
docker run --rm ubuntu:22.04 ls / # --rm: eliminar al salir automáticamente

# VER contenedores
docker ps # Contenedores en ejecución
docker ps -a # Todos (incluidos los parados)
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" # Formato personalizado

# INSPECCIONAR
docker logs mi-nginx # Ver los logs del contenedor
docker logs -f mi-nginx # Seguir los logs en tiempo real
docker inspect mi-nginx # Toda la configuración (JSON)
docker stats # Uso de recursos en tiempo real (como top)
docker top mi-nginx # Procesos dentro del contenedor

# INTERACTUAR con un contenedor en ejecución
docker exec -it mi-nginx bash # Abrir un shell dentro
docker exec mi-nginx nginx -t # Ejecutar un comando puntual
docker cp archivo.conf mi-nginx:/etc/nginx/ # Copiar archivos al contenedor

# PARAR y ELIMINAR
docker stop mi-nginx # Parar limpiamente (SIGTERM → SIGKILL)
docker kill mi-nginx # Parada forzosa (SIGKILL)
docker start mi-nginx # Arrancar un contenedor parado
docker restart mi-nginx # Reiniciar
docker rm mi-nginx # Eliminar el contenedor
docker rm -f mi-nginx # Forzar eliminación (aunque esté corriendo)

# Limpiar recursos no usados
docker system prune # Eliminar contenedores parados, imágenes sin usar, redes
docker system prune --volumes # Incluir volúmenes sin usar
docker system df # Ver cuánto espacio usa Docker

Gestión de imágenes

# Buscar y descargar imágenes
docker search nginx # Buscar en Docker Hub
docker pull nginx # Descargar la última versión (tag: latest)
docker pull nginx:1.26 # Versión específica (siempre mejor que :latest)
docker pull nginx:1.26-alpine # Versión Alpine (mucho más pequeña)

# Ver imágenes locales
docker images # o: docker image ls
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"

# Gestionar imágenes
docker rmi nginx:latest # Eliminar una imagen
docker tag nginx:1.26 mi-registro/nginx:1.26 # Renombrar/etiquetar

# Guardar y cargar imágenes (transferir sin registro)
docker save nginx:1.26 | gzip > nginx-1.26.tar.gz
docker load < nginx-1.26.tar.gz

# Publicar en un registro
docker login registry.example.com
docker tag mi-app:v1.0 registry.example.com/mi-app:v1.0
docker push registry.example.com/mi-app:v1.0

16.5 — Construir imágenes con Dockerfile

Instrucciones esenciales

# Dockerfile / Containerfile (Podman usa el mismo formato)

# FROM: imagen base (siempre la primera instrucción)
FROM ubuntu:22.04
# Preferir imágenes oficiales y específicas: no 'ubuntu', sino 'ubuntu:22.04'
# Para producción: preferir debian:12-slim, alpine:3.20 o imágenes distroless

# ARG: variables de tiempo de BUILD (no disponibles en el contenedor final)
ARG DEBIAN_FRONTEND=noninteractive
ARG APP_VERSION=1.0.0

# ENV: variables de entorno (disponibles en el contenedor en ejecución)
ENV APP_HOME=/app \
NODE_ENV=production \
PORT=3000

# RUN: ejecutar comandos durante la construcción de la imagen
# BUENA PRÁCTICA: combinar en un solo RUN para minimizar capas
RUN apt-get update && \
apt-get install -y --no-install-recommends \
nginx \
curl \
&& rm -rf /var/lib/apt/lists/* # Limpiar caché APT en la misma capa

# WORKDIR: directorio de trabajo (se crea si no existe)
WORKDIR /app

# COPY: copiar archivos del contexto de build a la imagen
# COPY origen destino
COPY package*.json ./ # Solo los manifiestos primero (optimiza caché)
RUN npm ci --only=production # Instalar dependencias

COPY src/ ./src/ # Luego el código fuente

# ADD: como COPY pero soporta URLs y descomprime TAR automáticamente
# Preferir COPY siempre que sea posible (más predecible)
ADD https://example.com/archivo.tar.gz /tmp/ # Descomprime automáticamente

# EXPOSE: documentar qué puerto usa la aplicación (no lo publica, es documentación)
EXPOSE 3000

# USER: cambiar al usuario no-root para ejecutar la aplicación (SEGURIDAD)
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
USER appuser

# CMD: comando por defecto al arrancar el contenedor (sobreescribible en docker run)
CMD ["node", "src/server.js"]

# ENTRYPOINT: comando fijo que siempre se ejecuta (CMD es sus argumentos)
# Con ENTRYPOINT + CMD:
# ENTRYPOINT ["nginx"]
# CMD ["-g", "daemon off;"]
# → docker run imagen -t = nginx -t (sobreescribe CMD pero no ENTRYPOINT)

# HEALTHCHECK: cómo verificar que el contenedor está sano
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1

Buenas prácticas y multi-stage builds

# ANTI-PATRÓN: imagen enorme con herramientas de build en producción
FROM ubuntu:22.04
RUN apt install -y build-essential golang
COPY . .
RUN go build -o /app main.go
# → La imagen incluye el compilador de Go, todo el SO, herramientas de build
# → Imagen de ~800 MB con secretos potenciales de la fase de build

# PATRÓN CORRECTO: Multi-stage build
# Fase 1: build (imagen grande, solo para compilar)
FROM golang:1.22 AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download # Cachear módulos (capa separada)
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /bin/mi-app ./cmd/server

# Fase 2: producción (imagen mínima, solo el binario)
FROM gcr.io/distroless/static:nonroot # Imagen distroless: sin shell, sin apt
COPY --from=builder /bin/mi-app /mi-app
USER nonroot:nonroot
ENTRYPOINT ["/mi-app"]
# → Imagen de ~10 MB, sin compilador, sin shell, sin herramientas
# → Superficie de ataque mínima (módulo 14)
# Construir una imagen
docker build -t mi-app:v1.0 . # Usar el Dockerfile del directorio actual
docker build -t mi-app:v1.0 -f Dockerfile.prod . # Especificar Dockerfile
docker build --no-cache -t mi-app:v1.0 . # Ignorar la caché (forzar rebuild)
docker build --build-arg APP_VERSION=2.0 -t mi-app:v2.0 . # Pasar ARG

# Ver el historial de capas (diagnóstico de tamaño)
docker history mi-app:v1.0

# .dockerignore: excluir archivos del contexto de build (como .gitignore)
cat > .dockerignore << 'EOF'
.git
node_modules
*.log
.env
.env.*
__pycache__
*.pyc
EOF

# Escaneo de vulnerabilidades de la imagen
docker scout cves mi-app:v1.0 # Docker Scout (incluido en Docker Desktop)
trivy image mi-app:v1.0 # Trivy (open source, muy completo)

16.6 — Redes y almacenamiento de contenedores

Redes de contenedores

# Tipos de red en Docker:
# bridge (por defecto): red virtual privada, NAT hacia el exterior
# host: el contenedor comparte la red del host directamente (sin NAT)
# none: sin red (contenedor completamente aislado)
# custom bridge: red bridge propia con DNS interno entre contenedores

# Ver redes disponibles
docker network ls
# NAME DRIVER SCOPE
# bridge bridge local ← red default (docker0)
# host host local
# none null local

# Publicar puertos: -p HOST_PORT:CONTAINER_PORT
docker run -d -p 80:80 -p 443:443 nginx # Publicar HTTP y HTTPS
docker run -d -p 127.0.0.1:8080:80 nginx # Solo accesible desde localhost
docker run -d -p 0.0.0.0:8080:80 nginx # Accesible desde cualquier IP del host

# Crear una red bridge personalizada
docker network create mi-red

# Contenedores en la misma red custom pueden resolverse por nombre (DNS interno)
docker run -d --network mi-red --name base-datos postgres:16
docker run -d --network mi-red --name web nginx
# El contenedor 'web' puede hacer: curl http://base-datos:5432
# La red bridge default NO tiene DNS entre contenedores

# Inspeccionar una red
docker network inspect mi-red # IPs, contenedores conectados, configuración

# Conectar un contenedor en ejecución a una red adicional
docker network connect mi-red otro-contenedor
docker network disconnect mi-red otro-contenedor

# Implementación interna: veth pairs (módulo 11)
# Cada contenedor tiene una interfaz veth que se conecta al bridge del host
ip link show | grep veth # Ver las interfaces veth en el host

Almacenamiento: volúmenes y bind mounts

# En un contenedor, la capa de escritura SE PIERDE cuando el contenedor se elimina.
# Para persistir datos: volúmenes o bind mounts.

# VOLÚMENES: gestionados por Docker/Podman (en /var/lib/docker/volumes/)
docker volume create datos-postgres # Crear volumen
docker volume ls # Listar volúmenes
docker volume inspect datos-postgres # Detalles (ruta real en el host, etc.)
docker volume rm datos-postgres # Eliminar (solo si no está en uso)
docker volume prune # Eliminar todos los no usados

# Usar un volumen en un contenedor
docker run -d \
--name postgres \
-v datos-postgres:/var/lib/postgresql/data \ # volumen:ruta_en_contenedor
-e POSTGRES_PASSWORD=secreto \
postgres:16

# BIND MOUNTS: montar un directorio del host en el contenedor
# Útil para desarrollo: el código en el host, Node/Python en el contenedor
docker run -d \
--name mi-dev \
-v $(pwd)/src:/app/src:ro \ # ruta_host:ruta_contenedor:opciones
-p 3000:3000 \
mi-app:dev
# :ro = solo lectura; :rw = lectura-escritura (por defecto)

# Diferencias clave:
# Volúmen: Docker gestiona la ruta, mejor rendimiento, más portátil
# Bind mount: el desarrollador elige la ruta, útil en desarrollo

# tmpfs mounts: en RAM, sin persistencia (datos sensibles en memoria)
docker run -d \
--name mi-app \
--tmpfs /tmp:size=100m \ # 100 MB en RAM, solo para /tmp
mi-app:latest

16.7 — Docker Compose / Podman Compose

Docker Compose permite definir y ejecutar aplicaciones multi-contenedor con un archivo YAML. Es el estándar para desarrollo local y para despliegues simples en producción.

Anatomía de un compose.yaml

# compose.yaml (o docker-compose.yml — el nuevo nombre canónico es compose.yaml)

services:
# Servicio web: la aplicación
web:
image: nginx:1.26-alpine # Imagen de Docker Hub
# build: . # Alternativa: construir desde Dockerfile local
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro # Configuración de nginx
- certbot-data:/etc/letsencrypt:ro # Certificados TLS (módulo 14)
- static-files:/var/www/html:ro
depends_on:
app:
condition: service_healthy # Esperar a que 'app' esté sano
networks:
- frontend
restart: unless-stopped

# La aplicación backend
app:
build:
context: .
dockerfile: Dockerfile
target: production # Multi-stage: usar solo la etapa 'production'
environment:
- DATABASE_URL=postgresql://app_user:${DB_PASSWORD}@db:5432/app_db
- SECRET_KEY=${SECRET_KEY} # Variables de .env (nunca hardcodear secretos)
- REDIS_URL=redis://cache:6379
env_file:
- .env # Cargar desde archivo .env
volumes:
- media-files:/app/media # Archivos subidos por usuarios
depends_on:
db:
condition: service_healthy
networks:
- frontend
- backend
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s # Dar tiempo al arranque inicial
restart: unless-stopped
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M

# Base de datos PostgreSQL
db:
image: postgres:16-alpine
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
POSTGRES_DB: app_db
POSTGRES_USER: app_user
POSTGRES_PASSWORD: ${DB_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app_user -d app_db"]
interval: 10s
timeout: 5s
retries: 5
networks:
- backend
restart: unless-stopped

# Redis para caché y colas
cache:
image: redis:7-alpine
command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru
volumes:
- redis-data:/data
networks:
- backend
restart: unless-stopped

volumes:
postgres-data: # Volumen para los datos de PostgreSQL
redis-data:
media-files:
static-files:
certbot-data:

networks:
frontend: # Red para comunicación web ↔ app
backend: # Red interna solo para app ↔ db ↔ cache

Comandos de Compose

# Arrancar todos los servicios (en background)
docker compose up -d

# Arrancar solo algunos servicios
docker compose up -d web app

# Ver el estado de los servicios
docker compose ps

# Ver los logs
docker compose logs -f # Todos los servicios
docker compose logs -f app # Solo el servicio 'app'

# Escalar un servicio (múltiples réplicas)
docker compose up -d --scale app=3 # 3 instancias del servicio 'app'

# Ejecutar un comando en un servicio
docker compose exec app bash
docker compose exec db psql -U app_user -d app_db

# Parar y eliminar
docker compose down # Parar y eliminar contenedores y redes
docker compose down -v # También eliminar los volúmenes (¡datos!)
docker compose stop # Solo parar (no eliminar)
docker compose restart app # Reiniciar un servicio específico

# Rebuild y despliegue de una nueva versión
docker compose build app # Reconstruir la imagen
docker compose up -d app # Actualizar el contenedor

# Perfiles: grupos de servicios opcionales
# En compose.yaml:
# services:
# debug-tools:
# profiles: ["debug"]
docker compose --profile debug up # Iniciar también los servicios del perfil 'debug'

16.8 — Contenedores en producción (sin orquestador)

Contenedores como servicios systemd con Podman Quadlet

# Quadlet: integración nativa de Podman con systemd (Podman >= 4.4)
# Define contenedores como unidades systemd en ~/.config/containers/systemd/
# o en /etc/containers/systemd/ para el sistema

# Crear la unidad para nginx como contenedor del sistema:
sudo tee /etc/containers/systemd/nginx.container << 'EOF'
[Unit]
Description=Nginx web server en contenedor
After=network-online.target

[Container]
Image=nginx:1.26-alpine
PublishPort=80:80
PublishPort=443:443
Volume=/etc/nginx/conf.d:/etc/nginx/conf.d:ro,Z
Volume=/var/www/html:/var/www/html:ro,Z
Environment=TZ=Europe/Madrid
AutoUpdate=registry # Actualizar la imagen automáticamente

[Service]
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

# Recargar systemd y activar el contenedor
sudo systemctl daemon-reload
sudo systemctl enable --now nginx.service
sudo systemctl status nginx

# El contenedor es gestionado exactamente igual que cualquier servicio systemd
sudo journalctl -u nginx -f # Logs del contenedor vía journald (módulo 09)

Seguridad: rootless y usuarios no-root

# Contenedores rootless con Podman (sin daemon root)
# El proceso del contenedor corre como usuario normal en el host

podman run -d --name web nginx:1.26-alpine # Corre como el usuario actual, no root

# Verificar: el contenedor no tiene privilegios de root en el host
ps aux | grep nginx | grep -v root # nginx corre como usuario normal

# Dentro del Dockerfile: usar un usuario no-root (módulo 14)
# RUN useradd -r -u 1001 appuser
# USER appuser

# NUNCA ejecutar aplicaciones dentro del contenedor como root
# si la aplicación no lo necesita.

# Límites de recursos en Docker (integración con cgroups — módulo 09)
docker run -d \
--name app \
--memory="256m" \ # Límite de RAM
--memory-swap="256m" \ # Sin swap para el contenedor
--cpus="0.5" \ # 50% de una CPU
--pids-limit=100 \ # Máximo 100 procesos (anti fork-bomb)
--read-only \ # Sistema de archivos del contenedor en R/O
--tmpfs /tmp \ # Permitir /tmp en RAM
mi-app:latest

# Política de reinicio y actualizaciones
docker run -d \
--restart=unless-stopped \ # Reiniciar siempre excepto si se paró manualmente
mi-app:latest

# Actualizaciones: usar etiquetas de versión (NO :latest en producción)
docker pull mi-app:v2.0
docker stop mi-app-prod
docker rm mi-app-prod
docker run -d --name mi-app-prod mi-app:v2.0
# → Proceso simple de actualización; rollback es trivial: volver a :v1.0

16.9 — Introducción a Kubernetes

Qué problema resuelve Kubernetes

Docker Compose en un servidor:
• Un nodo, múltiples contenedores
• Si el servidor falla → toda la aplicación cae
• Para escalar: tienes que hacerlo manualmente en cada servidor

Kubernetes (k8s): orquestador de contenedores
• Múltiples nodos (cluster), Kubernetes decide dónde van los contenedores
• Si un nodo falla → Kubernetes mueve los contenedores a otros nodos
• Escala automáticamente según la carga (HPA: Horizontal Pod Autoscaler)
• Gestiona actualizaciones sin downtime (rolling updates)
• Service discovery y load balancing integrado

¿Cuándo SÍ necesitas Kubernetes?
→ Alta disponibilidad: la aplicación no puede caer con un servidor
→ Escala dinámica: la carga varía mucho y necesitas escalar rápido
→ Decenas de microservicios que deben coordinarse
→ Equipo de plataforma dedicado para gestionarlo

¿Cuándo NO necesitas Kubernetes?
→ Aplicación pequeña en un servidor
→ Equipo sin experiencia en k8s (curva de aprendizaje muy alta)
→ Docker Compose o un proceso systemd son suficientes
→ El overhead operativo supera los beneficios

Arquitectura de Kubernetes

Control Plane (los "maestros"):
kube-apiserver → API REST de k8s (todo pasa por aquí)
etcd → Base de datos distribuida del estado del cluster
kube-scheduler → Decide en qué nodo correr cada Pod
kube-controller-manager → Bucles de control (mantener el estado deseado)

Worker Nodes (donde corren los contenedores):
kubelet → Agente que ejecuta los Pods en el nodo
kube-proxy → Reglas de red (iptables/ebpf)
Container runtime → containerd o CRI-O (el runtime real de contenedores)

Objetos fundamentales:
Pod: la unidad mínima. Uno o más contenedores que comparten red y almacenamiento.
Deployment: gestiona réplicas de Pods. Garantiza que N réplicas estén corriendo.
Service: endpoint estable (IP + DNS) para acceder a un conjunto de Pods.
ConfigMap: configuración no secreta (variables de entorno, archivos de config).
Secret: configuración sensible (contraseñas, tokens, certificados).
PersistentVolumeClaim: solicitud de almacenamiento persistente para Pods.
Namespace: aislamiento lógico dentro del cluster (como un proyecto o equipo).

Entorno local: minikube y k3s

# OPCIÓN 1: minikube — cluster de un nodo para desarrollo y aprendizaje
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube

minikube start # Crear el cluster (usa Docker como driver por defecto)
minikube status # Ver el estado
minikube dashboard # Abrir la UI web en el navegador

# OPCIÓN 2: k3s — distribución ligera de k8s para producción
# Ideal para servidores pequeños, IoT, edge computing
curl -sfL https://get.k3s.io | sh -
sudo kubectl get nodes # Verificar que el nodo está Ready
# k3s instala kubectl, helm y otras herramientas automáticamente

kubectl básico

# Información del cluster
kubectl cluster-info
kubectl get nodes # Nodos del cluster
kubectl get nodes -o wide # Con más detalle (IP, versión del kernel, etc.)

# Desplegar una aplicación con kubectl apply
# Siempre usar archivos YAML (declarativo), no comandos imperativos en producción
# nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3 # Tres réplicas de nginx
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.26-alpine
ports:
- containerPort: 80
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"
readinessProbe: # ¿Está listo para recibir tráfico?
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
# nginx-service.yaml
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector:
app: nginx # Selecciona los Pods con label app=nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
type: LoadBalancer # ClusterIP (interno), NodePort, o LoadBalancer
# Aplicar los manifiestos
kubectl apply -f nginx-deployment.yaml
kubectl apply -f nginx-service.yaml
# o aplicar todo un directorio:
kubectl apply -f ./manifests/

# Verificar el despliegue
kubectl get pods # Estado de los Pods
kubectl get deployments # Estado del Deployment
kubectl get services # Servicios y sus IPs
kubectl get all # Todo en el namespace actual

# Ver logs y depurar
kubectl logs nombre-del-pod
kubectl logs -f nombre-del-pod # Seguir los logs
kubectl logs -l app=nginx # Todos los Pods con label app=nginx

# Acceder a un Pod (como docker exec)
kubectl exec -it nombre-del-pod -- bash

# Escalar
kubectl scale deployment nginx-deployment --replicas=5

# Actualizar la imagen (rolling update — sin downtime)
kubectl set image deployment/nginx-deployment nginx=nginx:1.27-alpine
kubectl rollout status deployment/nginx-deployment # Ver el progreso
kubectl rollout history deployment/nginx-deployment # Historial de versiones
kubectl rollout undo deployment/nginx-deployment # Rollback a la versión anterior

# Eliminar recursos
kubectl delete -f nginx-deployment.yaml
kubectl delete deployment nginx-deployment
kubectl delete service nginx-service

Anexos

A. Comparativa de tecnologías

TecnologíaAislamientoOverheadTiempo de inicioCaso ideal
VM (KVM)Kernel propioAlto (SO completo)20-60sMúltiples SOs, aislamiento fuerte
LXC/LXDNamespace+cgroupsMuy bajo<1sMúltiples entornos del mismo SO
Docker/PodmanNamespace+cgroupsBajoMilisegundosMicroservicios, CI/CD
KubernetesNamespace+cgroupsMedio (control plane)SegundosClusters, alta disponibilidad

B. Interrelaciones con otros módulos

◀ Módulo 09 — Procesos y systemd
│ cgroups: la misma tecnología que usa systemd para limitar recursos
│ Namespaces de PID: el contenedor tiene su propio árbol de procesos
│ Quadlet: integración Podman ↔ systemd para contenedores en producción

◀ Módulo 11 — Redes en Linux
│ Redes de contenedores: veth pairs, bridges virtuales, iptables/nftables
│ El namespace de red es lo que crea la interfaz virtual del contenedor
│ K8s usa kube-proxy (iptables/eBPF) para el service networking

◀ Módulo 12 — Almacenamiento avanzado
│ Volúmenes de contenedores: bind mounts sobre el filesystem del host
│ Overlay2: driver de Docker que usa OverlayFS para las capas de imagen

◀ Módulo 13 — Arranque y kernel
│ Los módulos del kernel kvm.ko/kvm_intel.ko hacen posible KVM
│ Los namespaces y cgroups son funcionalidades del kernel

◀ Módulo 14 — Seguridad
│ Imágenes distroless: mínima superficie de ataque
│ Contenedores rootless (Podman): sin daemon root
│ Seccomp profiles y AppArmor para contenedores Docker

◀ Módulo 15 — Monitorización
│ cAdvisor: métricas de contenedores Docker para Prometheus
│ kubectl top pods: métricas de uso en Kubernetes

Referencias y Bibliografía

  1. KVM documentation — Linux Kernel
    https://www.linux-kvm.org/page/Documents

  2. libvirt documentation
    https://libvirt.org/docs.html

  3. Docker documentation
    https://docs.docker.com/

  4. Podman documentation
    https://docs.podman.io/

  5. Open Container Initiative (OCI) Specification
    https://opencontainers.org/

  6. Container Security — Liz Rice
    O'Reilly, 2020.

  7. Docker Deep Dive — Nigel Poulton
    LeanPub, 2023. (actualización anual)

  8. Kubernetes documentation
    https://kubernetes.io/docs/

  9. The Kubernetes Book — Nigel Poulton
    LeanPub, 2023.

  10. Kubernetes: Up and Running, 3ª ed. — Burns, Beda, Hightower
    O'Reilly, 2022.

  11. Linux Namespaces — man 7 namespaces
    https://man7.org/linux/man-pages/man7/namespaces.7.html

  12. cgroups v2 documentation — kernel.org
    https://www.kernel.org/doc/Documentation/admin-guide/cgroup-v2.rst

  13. OverlayFS — kernel.org
    https://www.kernel.org/doc/Documentation/filesystems/overlayfs.rst

  14. Podman Quadlet documentation
    https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html

  15. QEMU documentation
    https://www.qemu.org/docs/master/

  16. cloud-init documentation
    https://cloud-init.io/

  17. Trivy: container image scanner — GitHub
    https://github.com/aquasecurity/trivy

  18. k3s documentation — Rancher
    https://docs.k3s.io/

  19. minikube documentation
    https://minikube.sigs.k8s.io/docs/

  20. ArchWiki — KVM
    https://wiki.archlinux.org/title/KVM

  21. Production Kubernetes — Josh Rosso et al.
    O'Reilly, 2021.


Preguntas de autoevaluación

  1. ¿Cuál es la diferencia entre un hipervisor tipo 1 y tipo 2? ¿A qué tipo pertenece KVM y por qué?
  2. ¿Cuándo usarías una VM en lugar de un contenedor? Describe dos escenarios concretos donde una VM es la opción correcta.
  3. Explica qué son los namespaces de Linux y enumera al menos 5 tipos. ¿Cómo implementan el aislamiento del contenedor?
  4. ¿Qué son los cgroups y cómo los usan los contenedores? ¿Qué ocurre cuando un contenedor supera su límite de memoria?
  5. ¿Cómo funciona OverlayFS en las imágenes de contenedor? ¿Por qué múltiples contenedores con la misma imagen base no duplican el espacio en disco?
  6. ¿Cuál es la diferencia entre Docker y Podman en términos de arquitectura? ¿Por qué Podman es más seguro por diseño?
  7. Explica las diferencias entre CMD y ENTRYPOINT en un Dockerfile. ¿Cuándo usarías cada uno?
  8. ¿Qué es un multi-stage build en Docker y por qué reduce la superficie de ataque de la imagen final?
  9. ¿Cuál es la diferencia entre un volumen Docker y un bind mount? ¿Cuándo usarías cada uno?
  10. En compose.yaml, ¿para qué sirve depends_on: condition: service_healthy? ¿Qué diferencia tiene con depends_on simple?
  11. ¿Cuál es la diferencia entre un Pod y un contenedor en Kubernetes? ¿Por qué un Pod puede tener varios contenedores?
  12. Explica qué es un Deployment en Kubernetes. ¿Cómo funciona un rolling update? ¿Cómo harías un rollback?

Laboratorios prácticos

Lab 16.1 — Verificar soporte de KVM

# 1. Verificar soporte de virtualización en el CPU
grep -c "vmx\|svm" /proc/cpuinfo && echo "Virtualización disponible" || \
echo "Sin soporte de virtualización (puede estar desactivado en BIOS)"

# 2. Verificar módulos KVM
lsmod | grep kvm

# 3. Si está disponible, instalar herramientas básicas
sudo apt install cpu-checker -y 2>/dev/null
sudo kvm-ok 2>/dev/null || echo "kvm-ok no disponible"

# 4. Ver si libvirt está instalado
virsh version 2>/dev/null && virsh list --all 2>/dev/null || \
echo "libvirt no instalado. Instalar con: sudo apt install libvirt-clients"

Lab 16.2 — Docker: ciclo de vida básico

# 1. Verificar que Docker está instalado
docker --version || (echo "Instalar Docker: curl -fsSL https://get.docker.com | sh"; exit 1)

# 2. Ejecutar un contenedor nginx
docker run -d --name lab-nginx -p 8080:80 nginx:alpine
docker ps | grep lab-nginx

# 3. Verificar que responde
curl -s http://localhost:8080 | head -5

# 4. Ver logs
docker logs lab-nginx | head -10

# 5. Ejecutar un comando dentro del contenedor
docker exec lab-nginx nginx -v

# 6. Ver estadísticas de recursos
docker stats --no-stream lab-nginx

# 7. Limpiar
docker stop lab-nginx && docker rm lab-nginx

Lab 16.3 — Construir una imagen simple

# 1. Crear directorio de trabajo
mkdir /tmp/lab-docker && cd /tmp/lab-docker

# 2. Crear una página web simple
cat > index.html << 'EOF'
<!DOCTYPE html>
<html><body><h1>Módulo 16 — Contenedores funcionando</h1></body></html>
EOF

# 3. Crear el Dockerfile
cat > Dockerfile << 'EOF'
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 80
EOF

# 4. Construir la imagen
docker build -t mi-web:v1 .
docker images | grep mi-web

# 5. Ejecutar y probar
docker run -d --name mi-web-lab -p 8081:80 mi-web:v1
curl -s http://localhost:8081

# 6. Ver las capas de la imagen
docker history mi-web:v1

# 7. Limpiar
docker stop mi-web-lab && docker rm mi-web-lab
docker rmi mi-web:v1
cd - && rm -rf /tmp/lab-docker

Lab 16.4 — Redes y volúmenes de contenedores

# 1. Crear una red personalizada
docker network create lab-red

# 2. Lanzar dos contenedores en la misma red
docker run -d --name contenedor-a --network lab-red alpine sleep 300
docker run -d --name contenedor-b --network lab-red alpine sleep 300

# 3. Verificar que se pueden ver por nombre (DNS interno)
docker exec contenedor-a ping -c 3 contenedor-b

# 4. Crear y usar un volumen
docker volume create lab-vol
docker run -d --name con-vol --network lab-red \
-v lab-vol:/datos alpine sleep 300

# 5. Escribir datos en el volumen
docker exec con-vol sh -c 'echo "Datos persistentes" > /datos/prueba.txt'

# 6. Leer los datos desde otro contenedor con el mismo volumen
docker run --rm -v lab-vol:/datos alpine cat /datos/prueba.txt

# 7. Limpiar todo
docker stop contenedor-a contenedor-b con-vol
docker rm contenedor-a contenedor-b con-vol
docker volume rm lab-vol
docker network rm lab-red

Lab 16.5 — Namespaces: entender el aislamiento

# Demostración de namespaces PID sin Docker

# 1. Ver el PID de bash actual
echo "Mi PID en el sistema: $$"

# 2. Crear un namespace de PID nuevo
# (requiere util-linux >= 2.28 y privilegios)
sudo unshare --pid --fork --mount-proc /bin/bash << 'INNEREOF'
echo "Mi PID dentro del namespace: $$"
echo "Todos los procesos que veo:"
ps aux
INNEREOF

# 3. Comparar: en el sistema hay muchos procesos; dentro del namespace solo 1

# 4. Ver namespaces de un contenedor en ejecución
docker run -d --name ns-test alpine sleep 100
PID=$(docker inspect ns-test --format '{{.State.Pid}}')
echo "PID del contenedor en el host: $PID"
sudo ls -la /proc/$PID/ns/ # Namespaces del contenedor
docker stop ns-test && docker rm ns-test

Lab 16.6 — Kubernetes básico con minikube o k3s

# Verificar si kubectl está disponible
kubectl version --client 2>/dev/null || echo "kubectl no disponible"

# Si tienes minikube:
if command -v minikube >/dev/null; then
minikube status 2>/dev/null || minikube start

# Desplegar nginx
kubectl create deployment nginx --image=nginx:alpine
kubectl expose deployment nginx --port=80 --type=NodePort
kubectl get pods
kubectl get services

# Ver los logs del Pod
kubectl logs -l app=nginx

# Limpiar
kubectl delete deployment nginx
kubectl delete service nginx
fi

Resumen del módulo

Virtualización: hipervisores tipo 1 vs 2; VMs vs contenedores; VT-x/AMD-V; KVM+QEMU+libvirt
KVM/libvirt: virt-install, virsh (start/stop/snapshot/clone), redes NAT y bridge, cloud-init
Contenedores por dentro: namespaces (PID/net/mnt/uts/user), cgroups (límites de recursos), OverlayFS (capas de imagen)
Docker/Podman: ciclo de vida (run/ps/logs/exec/stop/rm), imágenes (pull/push/tag), docker stats
Dockerfile: FROM/RUN/COPY/ENV/CMD/ENTRYPOINT, multi-stage builds, .dockerignore, escaneo con Trivy
Redes: bridge/host/none, redes custom con DNS, publicación de puertos, veth pairs
Volúmenes: volúmenes Docker vs bind mounts vs tmpfs; persistencia de datos
Compose: compose.yaml (services/volumes/networks/depends_on/healthcheck), docker compose up/down/logs/exec
Producción: Podman Quadlet + systemd, rootless, límites de recursos, política de reinicio, etiquetas de versión
Kubernetes: arquitectura (control plane/workers/Pods/Deployments/Services), kubectl, rolling updates, rollback, minikube/k3s

Próximo paso: Módulo 17 — Linux como servidor. Con contenedores y virtualización dominados, el siguiente paso es poner servicios reales en producción: Apache/Nginx, bases de datos, correo, DNS y todo el stack de un servidor empresarial.


Última actualización: 2024-06
Versión: 1.0
Estado: ✅ Listo para enseñanza