Guía de Docker: De 0 a 100

Guía de Docker: De 0 a 100

Desde el primer contenedor hasta producción: instalación, Dockerfile, compose, Swarm, K8s, patrones, seguridad y recursos en español.

FECHA:
ACTUALIZADO:
AUTOR:Jorge Beneyto Castelló
LECTURA:28 MINUTOS DE LECTURA

Guía de Docker: De 0 a 100

¿Qué es Docker?

Docker es una plataforma de contenerización que permite empaquetar aplicaciones con todas sus dependencias en contenedores ligeros y portables. Fue lanzado por Docker Inc. (entonces dotCloud) en 2013, obra de Solomon Hykes, y revolucionó la forma en que desplegamos software.

¿Qué lo hace diferente de una máquina virtual?

AspectoContenedor DockerMáquina Virtual
KernelComparte el kernel del hostKernel propio (huésped)
ArranqueMilisegundosMinutos
AislamientoNamespaces + cgroups (nivel proceso)Hypervisor (nivel hardware)
TamañoMB (solo app + librerías)GB (SO completo)
Overhead~0% CPU/RAM5–15% CPU/RAM
MigraciónTrivial (imagen portable)Compleja (snapshots grandes)

Los contenedores usan namespaces (pid, net, mnt, uts, ipc, user) para aislar procesos y cgroups para limitar recursos (CPU, RAM, disco). Esto es posible porque todos los contenedores sobre una misma máquina comparten el kernel del host, a diferencia de las VMs que virtualizan hardware completo.

¿Dónde se usa Docker?

  • Desarrollo: entornos idénticos en local para todo el equipo
  • CI/CD: builds y tests reproducibles en pipelines (GitHub Actions, GitLab CI, Jenkins)
  • Producción: despliegues escalables con Swarm o Kubernetes, microservicios, serverless
  • Edge/IoT: contenedores ligeros en dispositivos con recursos limitados
  • Data Science: notebooks, modelos ML, pipelines reproducibles

¿Quién lo usa? Google (todo corre en contenedores), Netflix (orquestación masiva), Spotify (microservicios en K8s), Uber (cientos de servicios), Airbnb (infraestructura con Docker + K8s).

🔗 Docker — What is a Container?, Docker Overview docs.docker.com, Containers vs VMs (IBM)


Prerrequisitos

Antes de empezar, necesitas:

  • Conocimientos básicos de terminal: navegar directorios, ejecutar comandos, usar un editor de texto
  • El lenguaje/herramienta instalado en tu sistema (sigue la sección de Instalación si aún no lo tienes)

Si no cumples algún requisito, no te preocupes: cada sección te guiará paso a paso.

¿Cómo empezar? Instalación en todos los entornos

Windows

  1. Activa WSL2: wsl --install en PowerShell como administrador
  2. Descarga Docker Desktop
  3. Durante la instalación, marca “Use WSL 2 instead of Hyper-V”
  4. Verifica: docker --version

macOS

  • Docker Desktop: descarga de docker.com
  • Colima (open-source): brew install colima && colima start
  • OrbStack (rápido, Apple Silicon): brew install orbstack
  • Verifica: docker --version && docker compose version

Linux (Debian/Ubuntu)

sudo apt remove docker docker-engine docker.io containerd runc
sudo apt update && sudo apt install ca-certificates curl gnupg
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
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
sudo apt update && sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
docker run hello-world

Fedora/RHEL:

sudo dnf -y install dnf-plugin
sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
sudo dnf install docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo systemctl enable --now docker && sudo usermod -aG docker $USER

Docker Engine vs Docker Desktop

AspectoDocker EngineDocker Desktop
ComponentesEngine + CLI + Containerd + BuildKitEngine + CLI + UI + Compose + BuildKit + Extensions
PlataformaLinuxWindows, macOS (Linux vía VM)
LicenciaApache 2.0Free para pequeños, pago para grandes
Consumo RAM~50–100 MB~1–2 GB
InstalaciónRepositorio oficialInstaller con GUI
Ideal paraServidores, CI/CD, producciónDesarrollo local en Win/Mac

Rootless mode (Linux)

sudo apt install -y uidmap dbus-user-system
dockerd-rootless-setuptool.sh install
export PATH=/usr/bin:$PATH
export DOCKER_HOST=unix:///run/user/$UID/docker.sock
systemctl --user enable docker --now

Alias útiles

alias dkps='docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"'
alias dkls='docker image ls --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"'
alias dkrm='docker rm -f $(docker ps -aq) 2>/dev/null; echo "Eliminados"'
alias dklog='docker logs -f --tail 100'
alias dkexec='docker exec -it'
alias dkprune='docker system prune -af --volumes'
alias dcup='docker compose up -d --build'
alias dcdown='docker compose down'
alias dclogs='docker compose logs -f'
alias dkbuild='docker build -t'
alias dkrun='docker run -it --rm'

🔗 Docker Engine installation docs, Docker Desktop docs, Rootless mode docs


Escala de aprendizaje 0–100

Nivel 0–15: Fundamentos absolutos

Qué aprender:

  • Qué es un contenedor: proceso aislado con su propio sistema de archivos
  • docker run, docker ps, docker stop, docker rm
  • docker pull, docker images, docker rmi
  • Diferencia entre imagen (plantilla) y contenedor (instancia)
  • docker run hello-world y entender qué pasa
  • docker run -it ubuntu bash — explorar un contenedor interactivo
  • Conceptos clave: los contenedores comparten el kernel del host. NO son máquinas virtuales.

Proyecto: Servidor web estático — contenedor nginx que sirva un index.html. docker run -d -p 8080:80 -v $(pwd):/usr/share/nginx/html nginx:alpine.

Nivel 15–30: Dockerfile y builds

Qué aprender:

  • Dockerfile básico: FROM, WORKDIR, COPY, RUN, CMD
  • Orden de instrucciones y layer caching
  • .dockerignore
  • EXPOSE, ENV, ARG
  • Diferencia entre RUN, CMD y ENTRYPOINT

Proyecto: App Node.js dockerizada — Dockerfile con layer caching. Copia package.json primero, instala, luego copia el resto.

Nivel 30–45: Docker Compose y multi-servicio

Qué aprender:

  • compose.yml: services, build, image, ports, volumes, environment
  • docker compose up, down, logs, exec
  • Dependencias con depends_on y condiciones
  • Variables de entorno y archivos .env
  • Profiles: entornos dev/staging/prod

Proyecto: App web + BD + caché — compose con web (Node.js/Python), PostgreSQL y Redis.

Nivel 45–60: Multi-stage, volúmenes y redes

Qué aprender:

  • Multi-stage builds
  • Volúmenes: bind mount, volume, tmpfs
  • Redes avanzadas: bridge, host, overlay
  • HEALTHCHECK, logging drivers
  • Sidecar y Ambassador patterns

Proyecto: App Go con multi-stage — Dockerfile multi-stage (~10 MB final). Bind mount para desarrollo, volume para logs. Red separada para BD.

Nivel 60–75: Seguridad, registros y CI/CD

Qué aprender:

  • No ejecutar como root: USER appuser
  • --cap-drop=ALL --cap-add=NET_BIND_SERVICE
  • Secrets: --secret en build, monturas en runtime
  • docker scout / Trivy para vulnerabilidades
  • GitHub Actions: build + push con caché
  • Tags semánticos: v1.2.3, SHA

Proyecto: CI/CD pipeline — GitHub Action que construye, escanea, etiqueta y publica en GHCR.

Nivel 75–90: Swarm y orquestación

Qué aprender:

  • docker swarm init, docker service create, docker stack deploy
  • Redes overlay multi-host
  • Rolling updates
  • Secrets y configs en Swarm

Proyecto: Cluster Swarm local — 3 nodos, stack con app (3 réplicas), BD, Redis. Rolling update y escalado.

Nivel 90–100: Kubernetes, service mesh y producción

Qué aprender:

  • Minikube, Deployments, Services, ConfigMaps, Secrets, PVCs
  • Ingress controller, HPA
  • Service Mesh: Istio/Linkerd para mTLS
  • Observabilidad: Prometheus + Grafana
  • GitOps: ArgoCD

Proyecto: Microservicios en K8s — tres servicios en Minikube. Deployments con HPA, Ingress, Prometheus, Istio.

🔗 Docker Swarm docs, Kubernetes docs


Primeros pasos y configuración del entorno

El primer contenedor

docker run hello-world

Esto descarga la imagen hello-world de Docker Hub, crea un contenedor, ejecuta el binario hello que imprime el mensaje de bienvenida, y el contenedor se detiene.

🧪 Verificación

docker run hello-world && echo "Docker funciona correctamente" || echo "Error en Docker"

Dockerfile básico

Un Dockerfile es un script de instrucciones para construir una imagen. Cada instrucción crea una capa (layer) que se cachea.

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
docker build -t mi-app .
docker run -p 3000:3000 mi-app

.dockerignore

node_modules
.git
.env
*.md
.gitignore
Dockerfile
.dockerignore
dist

docker compose

services:
  web:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - .:/app
    environment:
      - NODE_ENV=development
    depends_on:
      - db
  db:
    image: "img/guia_0_100_docker/guia_0_100_docker_cover-1200.webp"
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: devpassword
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5
volumes:
  pgdata:
docker compose up -d
docker compose logs -f
docker compose exec web sh
docker compose down -v

🧪 Mini-script: verificar instalación completa

#!/bin/bash
echo "=== Verificación Docker ==="
docker --version && echo "✅ CLI OK" || echo "❌ CLI falló"
docker compose version && echo "✅ Compose OK" || echo "❌ Compose falló"
docker run --rm hello-world > /dev/null 2>&1 && echo "✅ Contenedor OK" || echo "❌ Contenedor falló"
echo "=== Fin ==="

🔗 Dockerfile reference, Compose specification


Sistema de archivos: volúmenes, monturas y almacenamiento

¿Qué es?

Docker tiene su propio sistema de archivos en capas (overlay2) que permite compartir datos entre el host y los contenedores, entre contenedores, y gestionar el almacenamiento eficientemente.

Tipos de monturas

# Named volume (Docker gestiona la ruta)
docker volume create mis-datos
docker run -v mis-datos:/data ubuntu

# Bind mount (ruta explícita del host)
docker run -v /ruta/host:/ruta/contenedor ubuntu
docker run --mount type=bind,source=/ruta/host,target=/ruta/contenedor ubuntu

# tmpfs (solo en RAM)
docker run --tmpfs /app/temp:noexec,nosuid,size=64M ubuntu
TipoPersistenciaRendimientoCaso de uso
Named volumeSí (gestionado por Docker)ExcelenteBD, datos de producción
Bind mountSí (en el host)ExcelenteDesarrollo, hot reload
tmpfsNo (se pierde al parar)Muy rápido (RAM)Caché, sesiones, tmp

docker cp

docker cp ./archivo.conf contenedor:/etc/app/config.conf
docker cp contenedor:/var/log/app.log ./app.log
docker cp ./data/ contenedor:/app/data/

COPY vs ADD

COPY archivo.txt /destino/
COPY --chown=appuser:appgroup . /app
ADD archivo.tar.gz /destino/   # Extrae automáticamente

Regla: usa siempre COPY. ADD con URLs es mala práctica. ADD con tarballs es aceptable pero prefiere COPY + RUN tar -xf.

Contexto de build y .dockerignore

El contexto es todo lo que envías al daemon. El .dockerignore excluye archivos:

node_modules
.git
.env
*.md
dist
.cache
*.log
docker build -t mi-app .
docker build -f ./app/Dockerfile ./app

docker export/import (contenedores a tarballs)

docker export mi-contenedor > mi-contenedor.tar
cat mi-contenedor.tar | docker import - mi-imagen:importada

docker save/load (imágenes a tarballs)

docker save mi-app:latest | gzip > mi-app.tar.gz
docker load < mi-app.tar.gz
docker save mi-app:latest | ssh servidor docker load

Overlay2: el sistema de archivos en capas

Docker usa el driver overlay2 (Linux nativo) para unir capas en un sistema de archivos único:

Capa superior (contenedor, r/w)
---
Capa N (última instrucción Dockerfile, r/o)
---
...
---
Capa 0 (FROM python:3.12-slim, r/o)
  • Copy-on-Write (CoW): si un contenedor modifica un archivo de una capa inferior, overlay2 lo copia a la capa superior antes de modificarlo
  • Compartición: si dos contenedores usan la misma imagen base, comparten las mismas capas en disco
docker history mi-app:latest
docker inspect mi-app:latest --format '{{.RootFS.Layers}}'
docker system df

Build cache

Docker cachea cada capa por el hash de su instrucción y archivos. La caché se invalida cuando:

  1. Cambia el contenido de un archivo COPY o ADD
  2. Cambia el comando de un RUN
  3. Cambia la imagen base (FROM)
  4. Una capa anterior se invalida (cascada)
docker build --no-cache -t mi-app .
docker build --cache-from ghcr.io/mi-org/app:latest -t mi-app .
docker build --progress=plain -t mi-app . 2>&1 | grep CACHED

🧪 Mini-script: explorar capas de una imagen

#!/bin/bash
IMAGE=${1:-nginx:alpine}
echo "=== Capas de $IMAGE ==="
docker pull -q "$IMAGE" > /dev/null
docker history "$IMAGE" --format "table {{.CreatedBy}}\t{{.Size}}" | head -20
echo "=== Tamaño total ==="
docker images "$IMAGE" --format "{{.Size}}"

✅ Buenas prácticas

  • Ordena instrucciones de menos a más cambiantes para maximizar caché
  • Combina RUN comandos con && para reducir capas
  • Usa .dockerignore para reducir el contexto de build
  • Prefiere volúmenes named sobre bind mounts en producción
  • No almacenes datos de BD en bind mounts — usa volúmenes

🔗 Para saber más


Algoritmos y optimización de builds

¿Qué es?

Docker no es un lenguaje de programación y no tiene algoritmos en el sentido tradicional. Sin embargo, sus mecanismos internos tienen comportamientos analizables en términos de complejidad.

Resolución de capas (overlay2) — O(n)

Cada capa es una diferencia respecto a la anterior. Cuando el contenedor lee un archivo, overlay2 debe recorrer las capas de arriba abajo hasta encontrarlo:

Lectura de /etc/passwd -> busca en capa r/w -> no está -> capa N -> ... -> capa 0 -> lo encuentra

En el peor caso, O(n) donde n = número de capas. Con caché de metadatos (dcache), O(1). Se recomienda máximo ~20-50 capas.

Cache invalidation — O(n) en capas modificadas

FROM python:3.12-slim    # Capa 0 — cacheada
WORKDIR /app             # Capa 1 — cacheada
COPY requirements.txt .  # Capa 2 — ¡CAMBIA!
RUN pip install -r ...   # Capa 3 — se reconstruye
COPY . .                 # Capa 4 — se reconstruye

Si la capa 2 se invalida, las capas 3 y 4 también. Ordena de menos a más cambiante.

Dependencias entre servicios — O(V + E)

depends_on en compose crea un grafo de dependencias resuelto con orden topológico:

services:
  web:
    depends_on: [api, redis]
  api:
    depends_on:
      db:
        condition: service_healthy
  worker:
    depends_on: [redis, db]
  redis:
  db:

No se permiten dependencias cíclicas — error de validación.

Scheduling en Swarm/Kubernetes

  • Swarm: spreading — distribuye réplicas entre nodos minimizando desbalance
  • K8s: predicados (filtros) + prioridades (puntuación) — O(P x N)

Round-robin DNS

docker compose up -d --scale api=5

Docker DNS resuelve el nombre del servicio con round-robin: O(1) por resolución.

Tabla resumen

ConceptoComplejidadFactor determinante
Resolución de capas (overlay2)O(n) peor caso, O(1) con cachén = número de capas
Cache invalidationO(n) capas desde el cambioOrden del Dockerfile
Dependencias (compose)O(V + E) topológicoV = servicios, E = depends
Scheduling SwarmSpreading (balance de recursos)Nodos y réplicas
Scheduling K8sO(P x N) predicados + prioridadesPods y nodos
Round-robin DNSO(1) por resoluciónNúmero de réplicas
Build context transferO(tamaño contexto)Archivos enviados al daemon

🧪 Mini-script: medir tiempo de build con y sin caché

#!/bin/bash
echo "=== Build SIN caché ==="
time docker build --no-cache -t test-cache . 2>&1 | tail -3
echo "=== Build CON caché ==="
time docker build -t test-cache . 2>&1 | tail -3

✅ Buenas prácticas

  • Máximo 20-50 capas por imagen
  • Ordena Dockerfile de menos a más cambiante
  • Usa --cache-from en CI para reusar imágenes previas
  • El contexto de build debe ser lo más pequeño posible

🔗 Para saber más


Conceptos clave explicados a fondo

Imágenes vs Contenedores

¿Qué es? Una imagen es una plantilla inmutable de solo lectura con el sistema de archivos y configuración de la app. Un contenedor es una instancia ejecutable de una imagen: una capa r/w sobre las capas de solo lectura.

docker build -t mi-app:v1 .
docker run -d --name c1 mi-app:v1
docker run -d --name c2 mi-app:v1

🔗 Docker overview

Capas (Layer Caching)

¿Qué es? Cada instrucción en el Dockerfile crea una capa. Docker cachea cada capa por su hash. Si una capa no ha cambiado, Docker reusa la cacheada.

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "main.py"]

💡 Para maximizar el caché: ordena instrucciones de menos a más cambiantes, separa dependencias del código, combina RUN comandos.

ENTRYPOINT vs CMD

CMD ["python", "app.py"]
ENTRYPOINT ["python"]
Dockerfiledocker run imagendocker run imagen --help
CMD ["python", "app.py"]python app.pypython app.py --help
ENTRYPOINT ["python"] + CMD ["app.py"]python app.pypython --help

Patrón recomendado: ENTRYPOINT para el ejecutable fijo, CMD para argumentos por defecto.

Dockerfile multi-stage

Permite usar múltiples imágenes base en un mismo Dockerfile:

FROM golang:1.22 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /bin/app .

FROM scratch
COPY --from=builder /bin/app /bin/app
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/bin/app"]
docker build -t go-app .
# ~10 MB en lugar de ~800 MB

🔗 Multi-stage builds

ARG vs ENV

ARG VERSION=latest
ENV NODE_ENV=production
  • ARG: solo durante el build, se pasa con --build-arg
  • ENV: persiste en runtime, config de la app
  • Seguridad: nunca pongas secrets en ENV. Usa --secret de BuildKit.

HEALTHCHECK

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

Estados: starting → healthy → unhealthy. Swarm y K8s usan esto para reiniciar contenedores.

Volúmenes: bind mount, volume, tmpfs

docker volume create mis-datos
docker run -v mis-datos:/data ubuntu
docker run --mount type=bind,src=/host/ruta,target=/container/ruta ubuntu
docker run --tmpfs /app/temp ubuntu
TipoPersistenciaRendimientoCaso de uso
VolumeSíExcelenteBD, datos de producción
Bind mountSíExcelenteDesarrollo, hot reload
tmpfsNoMuy rápido (RAM)Caché, sesiones

Redes: bridge, host, overlay, macvlan

docker network create --driver bridge mi-red
docker run --network mi-red --name web nginx
docker network create --driver overlay --attachable mi-red-overlay
docker network create --driver macvlan --subnet=192.168.1.0/24 --gateway=192.168.1.1 -o parent=eth0 macvlan-red

Registro: Docker Hub, GHCR, Harbor

docker login
docker tag mi-app usuario/mi-app:1.0.0
docker push usuario/mi-app:1.0.0

docker login ghcr.io -u USERNAME --password-stdin < token.txt
docker tag mi-app ghcr.io/mi-org/mi-app:1.0.0
docker push ghcr.io/mi-org/mi-app:1.0.0

Mejor práctica: etiqueta siempre con SHA + semántico en CI:

docker tag mi-app ghcr.io/mi-org/mi-app:${GITHUB_SHA::8}
docker tag mi-app ghcr.io/mi-org/mi-app:${GITHUB_REF_NAME#v}
docker push --all-tags ghcr.io/mi-org/mi-app

Seguridad

No ejecutar como root:

RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

Secrets: nunca en imágenes:

docker build --secret id=mysecret,src=./secret.txt -t mi-app .

Capabilities:

docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE mi-app

Análisis de vulnerabilidades:

docker scout quickview mi-app
trivy image mi-app

Patrones de Docker (reinterpretación de patrones de diseño)

Sidecar Container

Un contenedor auxiliar que acompaña al principal. Ejemplo: proxy sidecar.

services:
  app:
    build: .
    networks:
      - backend
  sidecar-proxy:
    image: "img/guia_0_100_docker/guia_0_100_docker_cover-1200.webp"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    ports:
      - "80:80"
    networks:
      - backend
      - frontend
networks:
  frontend:
  backend:
    internal: true

Ambassador Container

Proxy que abstrae la conexión a un servicio externo. La app se conecta a localhost del ambassador.

services:
  app:
    build: .
    environment:
      - DATABASE_URL=postgres://user:pass@ambassador:5432/db
    depends_on:
      - ambassador
  ambassador:
    image: "img/guia_0_100_docker/guia_0_100_docker_cover-1200.webp"
    volumes:
      - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro

Init Containers

Contenedores que se ejecutan antes que el principal para preparar el entorno.

services:
  init-db:
    image: "img/guia_0_100_docker/guia_0_100_docker_cover-1200.webp"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/miapp
    depends_on:
      db:
        condition: service_healthy
    profiles:
      - setup

Blue/Green Deploy

Dos versiones coexisten y se cambia el tráfico atómicamente.

docker compose --profile blue up -d
docker compose --profile green up -d
docker compose exec nginx nginx -s reload
docker compose --profile blue down

🔗 Docker security best practices, Docker patterns, Kubernetes Patterns (Red Hat)

🧪 Mini-script: comprobar seguridad de una imagen

#!/bin/bash
IMAGE=${1:-nginx:alpine}
echo "=== Análisis de $IMAGE ==="
docker scout quickview "$IMAGE" 2>/dev/null || echo "Docker Scout no disponible"
echo "=== Verificar usuario no root ==="
docker run --rm "$IMAGE" whoami 2>/dev/null || echo "(no hay whoami)"
echo "=== Capas ==="
docker history "$IMAGE" --format "{{.Size}}\t{{.CreatedBy}}" | head -10

Testing y calidad

Frameworks y herramientas

FrameworkPropósitoAsyncCLI
Docker Compose testsTests de integración multi-servicioSídocker compose up --wait
Container health checksVerificar estado del contenedorNodocker inspect --format='{{.State.Health.Status}}'
TestcontainersTests con contenedores desde códigoSípip install testcontainers
batsTests de scripts bash/DockerfileNobats test/*.bats
container-structure-testTests de estructura de imágenesNocontainer-structure-test test --image

Cómo testear cada concepto

Dockerfile y capas → verificar que el build es correcto:

#!/bin/bash
# test_build.sh
docker build -t test-app . || exit 1
echo "✅ Build exitoso"
SIZE=$(docker images test-app --format "{{.Size}}")
echo "Tamaño: $SIZE"

Health checks → verificar que el contenedor responde:

docker compose up -d --wait
docker inspect --format='{{.State.Health.Status}}' "$(docker compose ps -q app)" | grep -q healthy
echo "✅ Health check OK"

Integración con Testcontainers (Python):

# test_docker.py
from testcontainers.postgres import PostgresContainer
import psycopg2

with PostgresContainer("postgres:16-alpine") as postgres:
    conn = psycopg2.connect(
        host=postgres.get_container_host_ip(),
        port=postgres.get_exposed_port(5432),
        user=postgres.POSTGRES_USER,
        password=postgres.POSTGRES_PASSWORD,
        dbname=postgres.POSTGRES_DB,
    )
    cur = conn.cursor()
    cur.execute("SELECT 1")
    assert cur.fetchone() == (1,)
    print("✅ Testcontainer Postgres funciona")

Test de estructura de imagen (container-structure-test):

# structure-test.yaml
schemaVersion: "2.0.0"
fileContentTests:
  - name: "Non-root user"
    path: "/etc/passwd"
    expectedContents: ["appuser"]
commandTests:
  - name: "Node version"
    command: "node"
    args: ["--version"]
    expectedOutput: ["v20"]
container-structure-test test --image mi-app --config structure-test.yaml

Tests parametrizados con bash

#!/bin/bash
# test_compose.sh
SERVICES=("api" "db" "redis")
for svc in "${SERVICES[@]}"; do
    docker compose up -d "$svc" --wait
    STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$(docker compose ps -q "$svc")" 2>/dev/null || echo "no healthcheck")
    echo "$svc: $STATUS"
done
docker compose down

Cobertura y CI

# .github/workflows/docker-test.yml
name: Docker Tests
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build
        run: docker build -t mi-app .
      - name: Test
        run: |
          docker compose up -d --wait
          curl -f http://localhost:3000/health || exit 1
          docker compose down
      - name: Security scan
        run: docker scout quickview mi-app

🧪 Mini-scripts de verificación

Mini-script 1: Verificar que un contenedor arranca y responde

#!/bin/bash
docker run -d --name test-web -p 8080:80 nginx:alpine
sleep 2
if curl -f http://localhost:8080 > /dev/null 2>&1; then
    echo "✅ Contenedor nginx responde"
else
    echo "❌ Contenedor no responde"
fi
docker rm -f test-web

Mini-script 2: Verificar multi-stage (imagen pequeña)

#!/bin/bash
docker build -t test-multi -f Dockerfile.multi .
SIZE=$(docker images test-multi --format "{{.Size}}")
echo "Tamaño imagen: $SIZE"
if [[ $SIZE == *"MB"* ]]; then
    NUM=${SIZE%MB}
    if (( $(echo "$NUM < 50" | bc -l) )); then
        echo "✅ Imagen optimizada (< 50MB)"
    else
        echo "⚠️ Imagen grande ($SIZE)"
    fi
fi

🔗 Testcontainers docs, container-structure-test, Docker Scout docs


Conceptos avanzados: Swarm, Kubernetes y producción

Docker Swarm

Swarm convierte un grupo de servidores en un clúster lógico con balanceo de carga integrado.

# Inicializar clúster
docker swarm init
docker swarm join --token SWMTKN-1-... 192.168.1.10:2377

# Crear servicio
docker service create \
  --name api \
  --replicas 5 \
  --publish published=80,target=3000 \
  --network overlay-net \
  --limit-cpu 0.5 \
  --limit-memory 256M \
  mi-app:latest

# Rolling update
docker service update \
  --image mi-app:v2 \
  --update-parallelism 2 \
  --update-delay 10s \
  api

Stack deploy desde compose:

docker stack deploy -c compose.yml miapp

Kubernetes (K8s)

K8s es el estándar de la industria para orquestación. Minikube te da un clúster local de un nodo.

minikube start
kubectl get nodes

Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: "img/guia_0_100_docker/guia_0_100_docker_cover-1200.webp"
          ports:
            - containerPort: 3000
          resources:
            limits:
              cpu: "0.5"
              memory: "256Mi"

Service:

apiVersion: v1
kind: Service
metadata:
  name: api
spec:
  selector:
    app: api
  ports:
    - port: 80
      targetPort: 3000
  type: ClusterIP

Horizontal Pod Autoscaler:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api
  minReplicas: 2
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl apply -f hpa.yaml
kubectl scale deployment api --replicas=10

Docker Compose en producción

services:
  app:
    build: .
    restart: unless-stopped
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: "0.5"
          memory: "256M"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Service Mesh (Istio)

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: api
spec:
  hosts:
    - api
  http:
    - route:
        - destination:
            host: api
            subset: v1
          weight: 90
        - destination:
            host: api
            subset: v2
          weight: 10

Observabilidad: Prometheus + Grafana

services:
  prometheus:
    image: "img/guia_0_100_docker/guia_0_100_docker_cover-1200.webp"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"
  grafana:
    image: "img/guia_0_100_docker/guia_0_100_docker_cover-1200.webp"
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin

GitOps con ArgoCD

kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
argocd app create miapp --repo https://github.com/mi-org/miapp --path k8s --dest-server https://kubernetes.default.svc

Comparativa de escalado

HerramientaÁmbitoEscaladoBalanceoPersistencia
Compose —scaleUn hostManualDNS RRVolúmenes locales
SwarmMulti-hostManual/automáticoIngress LBVolúmenes + NFS
KubernetesMulti-hostHPA automáticoService + IngressPVC + CSI

🧪 Mini-script: probar Swarm local

#!/bin/bash
echo "=== Iniciar Swarm ==="
docker swarm init 2>/dev/null || echo "Swarm ya iniciado"
echo "=== Desplegar servicio ==="
docker service create --name demo --replicas 3 --publish 80:80 nginx:alpine
sleep 3
echo "=== Servicios ==="
docker service ls
echo "=== Escalar ==="
docker service scale demo=5
sleep 2
docker service ps demo
echo "=== Limpiar ==="
docker service rm demo
docker swarm leave --force 2>/dev/null

🔗 Docker Swarm docs, Kubernetes docs, Minikube docs, Istio docs, Prometheus docs


Proyecto final integrador

Gestor de tareas API dockerizado

Proyecto que combina: file system I/O, algoritmos (caché), patrones (sidecar/ambassador), testing, y red.

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY . .
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 3000
CMD ["node", "server.js"]
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DB_HOST=db
      - DB_USER=tasks
      - DB_PASSWORD=${DB_PASSWORD}
    volumes:
      - ./src:/app/src:ro
      - logs:/app/logs
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
      interval: 10s
      timeout: 5s
      retries: 3
    restart: unless-stopped
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    environment:
      POSTGRES_DB: tasks
      POSTGRES_USER: tasks
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U tasks"]
      interval: 5s
      timeout: 3s
      retries: 5

  redis:
    image: redis:7-alpine
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

volumes:
  pgdata:
  redis-data:
  logs:
// server.js
const express = require('express');
const { Pool } = require('pg');
const Redis = require('ioredis');

const pool = new Pool({
  host: process.env.DB_HOST || 'localhost',
  user: process.env.DB_USER || 'tasks',
  password: process.env.DB_PASSWORD || 'password',
  database: 'tasks'
});

const redis = new Redis({ host: 'redis' });

const app = express();
app.use(express.json());

app.get('/health', (req, res) => res.json({ status: 'ok' }));

app.get('/tasks', async (req, res) => {
  const cached = await redis.get('tasks:list');
  if (cached) return res.json(JSON.parse(cached));
  const result = await pool.query('SELECT * FROM tasks ORDER BY id');
  await redis.setex('tasks:list', 30, JSON.stringify(result.rows));
  res.json(result.rows);
});

app.post('/tasks', async (req, res) => {
  const { title } = req.body;
  const result = await pool.query(
    'INSERT INTO tasks (title) VALUES ($1) RETURNING *',
    [title]
  );
  await redis.del('tasks:list');
  res.status(201).json(result.rows[0]);
});

app.listen(3000, () => console.log('Tasks API on :3000'));

Pruebas:

#!/bin/bash
# test_proyecto.sh
echo "=== Test: Build ==="
docker build -t tasks-api . || exit 1

echo "=== Test: Up ==="
docker compose up -d --wait

echo "=== Test: Health ==="
curl -f http://localhost:3000/health && echo " ✅ Health OK"

echo "=== Test: Crear tarea ==="
curl -s -X POST http://localhost:3000/tasks -H "Content-Type: application/json" -d '{"title":"Test"}' | grep -q "Test" && echo " ✅ Create OK"

echo "=== Test: Listar tareas ==="
curl -s http://localhost:3000/tasks | grep -q "Test" && echo " ✅ List OK"

echo "=== Test: Caché Redis ==="
docker compose exec redis redis-cli get "tasks:list" | grep -q "Test" && echo " ✅ Redis cache OK"

echo "=== Limpiar ==="
docker compose down -v
echo "=== Todos los tests pasaron ==="

Documentación:

  • README.md con instrucciones de build, test y despliegue
  • Scripts: build.sh, test.sh, deploy.sh
  • CI/CD: GitHub Action que ejecuta los tests

Canales y recursos en español

YouTube

  • Midulive — Docker, Kubernetes, CI/CD y DevOps con ejemplos prácticos
  • MoureDev — Docker desde cero, proyectos dockerizados
  • OpenWebinars — Curso completo de Docker y Kubernetes
  • EDteam — Rutas de Docker desde fundamentos hasta Swarm
  • HolaMundo — Introducciones rápidas a Docker Compose y Swarm
  • Programación Fácil — Tutoriales de Docker para principiantes
  • Platzi — Curso de Docker y Curso de Kubernetes

Comunidades

  • r/docker (Reddit) — Noticias, dudas, proyectos
  • r/devops (Reddit) — Comunidad DevOps
  • Docker Community Slack — Canales oficiales
  • Stack Overflow en español — Etiquetas docker y docker-compose

Repositorios

Recursos del blog

Blogs y newsletters


Hacks y tips de productividad

1. docker system prune --volumes — limpia todo lo no usado

docker system prune -af --volumes

2. dive — inspecciona el contenido de cada capa

docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive mi-app

3. lazydocker — TUI interactiva

docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock lazyteam/lazydocker

4. docker scout — análisis de vulnerabilidades

docker scout quickview mi-app
docker scout cves mi-app
docker scout recommendations mi-app

5. --progress=plain — logs detallados del build

docker build --progress=plain --no-cache -t mi-app .

6. BuildKit — builds paralelos y secretos

RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt
RUN --mount=type=secret,id=api_key echo "API key: $(cat /run/secrets/api_key)"

7. Contenedores como herramientas CLI

alias yq='docker run --rm -i -v ${PWD}:/workdir mikefarah/yq'
alias jq='docker run --rm -i -v ${PWD}:/data imega/jq'
alias py3='docker run --rm -i -v ${PWD}:/app python:3.12-slim python'

8. .env con docker compose

DB_PASSWORD=supersecreto
TAG=latest
PORT=3000

9. Docker socket proxy para seguridad

services:
  docker-socket-proxy:
    image: tecnativa/docker-socket-proxy
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      CONTAINERS: 1
      SERVICES: 1

10. Probar healthchecks localmente

docker run --rm alpine sh -c "apk add curl && curl -f http://host.docker.internal:3000/health"

11. Depurar contenedor que no arranca

docker logs contenedor --tail 100
docker inspect contenedor | jq '.[0].State'
docker commit contenedor debug-image && docker run -it --entrypoint sh debug-image

12. Exportar logs a JSON

docker logs contenedor --timestamps 2>&1 | jq -R '{timestamp: ., line: input_line_number}'

🔗 Dive GitHub, LazyDocker GitHub, Docker Scout docs


⚠️ Errores comunes

ErrorCausaSolución
Got permission denied while trying to connect to the Docker daemon socketTu usuario no está en el grupo docker o el daemon no está arrancadoEjecuta sudo usermod -aG docker $USER y vuelve a entrar en la sesión; comprueba con systemctl status docker
port is already allocatedOtro proceso ya usa el puerto que intentas mapear con -pUsa sudo lsof -i :<puerto> para identificar el proceso; matalo o cambia el puerto de host (-p 8080:80)
The container name "<nombre>" is already in useYa existe un contenedor con ese nombre (activo o detenido)Renómbralo con --name otro-nombre o elimina el anterior con docker rm <nombre>
Cannot connect to the Docker daemon at unix:///var/run/docker.sockEl servicio Docker no está arrancadosudo systemctl start docker y verifica con docker ps
docker: command not foundDocker no está instalado o no está en el PATHInstala Docker Engine siguiendo la sección de instalación de esta guía; en WSL instala el paquete docker.io
failed to solve: process ... did not complete successfullyError durante el build con BuildKit (fallo en un RUN, COPY o resolución de imagen base)Revisa el log completo con docker build --no-cache .; comprueba que las rutas en COPY existen y que la imagen base es válida
no matching manifest for linux/arm64 in the manifest list entriesIntentas tirar una imagen solo disponible para amd64 en un Mac M1/M2 o ARMAñade --platform linux/amd64 o busca una imagen multi-arch; alternativamente usa docker buildx para cross-build

🔗 Guías relacionadas

Estas guías te ayudan a complementar lo que aprendiste aquí:


Referencias y documentación oficial


🏆 Retos Relacionados

Pon a prueba lo aprendido con estos desafíos:

COMPARTIR:
COMENTARIOS:

📋 Contenido