1/4/2026 • DevOps • 16 min de lectura

Docker para profesionales: tutorial práctico de principio a fin

Una guía práctica para dominar Docker: imágenes, contenedores, Dockerfile, Compose, volúmenes, redes, registros, multi-stage builds, seguridad, debugging y CI/CD.

#docker#containers#devops#ci-cd#compose

Docker permite empaquetar una aplicación con sus dependencias y ejecutarla de forma consistente en desarrollo, CI/CD y producción. La promesa no es “funciona en mi máquina”, sino “funciona igual en cualquier entorno que pueda ejecutar contenedores”.

Este tutorial tiene un enfoque eminentemente práctico. Vas a crear imágenes, ejecutar contenedores, escribir Dockerfile, levantar servicios con Docker Compose, trabajar con volúmenes y redes, publicar imágenes, depurar problemas y aplicar criterios profesionales de seguridad y operación.

Modelo mental de Docker

Antes de memorizar comandos, conviene separar conceptos:

El flujo básico es:

Dockerfile
docker build
image
docker run
container
logs / exec / inspect
docker push

Verificar instalación

Comprueba que Docker responde:

docker version
docker info

Ejecuta el contenedor de prueba:

docker run --rm hello-world

Lista imágenes locales:

docker images

Lista contenedores en ejecución:

docker ps

Lista todos los contenedores:

docker ps -a

Ejecutar tu primer contenedor útil

Levanta Nginx en segundo plano:

docker run --name web \
  --detach \
  --publish 8080:80 \
  nginx:1.27-alpine

Abre:

http://localhost:8080

Qué significa cada opción:

Ver logs:

docker logs web
docker logs -f web

Entrar al contenedor:

docker exec -it web sh

Detenerlo:

docker stop web

Eliminarlo:

docker rm web

Si quieres borrar automáticamente el contenedor al terminar:

docker run --rm nginx:1.27-alpine

Imágenes y contenedores

Una imagen no “corre”; quien corre es el contenedor. Puedes tener una imagen nginx:1.27-alpine y crear muchos contenedores a partir de ella.

Descargar una imagen:

docker pull redis:7-alpine

Inspeccionar una imagen:

docker image inspect redis:7-alpine

Ver capas de una imagen:

docker history redis:7-alpine

Crear un contenedor sin arrancarlo:

docker create --name cache redis:7-alpine

Arrancarlo:

docker start cache

Detenerlo:

docker stop cache

Eliminarlo:

docker rm cache

Eliminar una imagen local:

docker rmi redis:7-alpine

Regla profesional: no trates los contenedores como servidores permanentes. Si necesitas cambiar algo, cambia el Dockerfile, reconstruye la imagen y redepliega.

Crear un Dockerfile

Supongamos una aplicación Node.js con estos scripts:

{
  "scripts": {
    "build": "vite build",
    "start": "node server.js",
    "test": "vitest run"
  }
}

Un Dockerfile inicial:

# syntax=docker/dockerfile:1
FROM node:22-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

ENV NODE_ENV=production

EXPOSE 3000

CMD ["npm", "start"]

Construye la imagen:

docker build -t myapp:dev .

Ejecuta:

docker run --rm \
  --name myapp \
  --publish 3000:3000 \
  myapp:dev

Instrucciones importantes:

.dockerignore

El contexto de build es lo que Docker envía al builder. Si copias todo sin filtrar, puedes enviar archivos pesados o sensibles.

Crea .dockerignore:

node_modules
dist
.git
.github
.env
*.log
coverage
.DS_Store

Esto mejora rendimiento, reduce riesgo de filtrar secretos y evita invalidar cache por archivos que no importan al build.

Cache de build

Docker reutiliza capas cuando las instrucciones y sus entradas no cambian. Por eso conviene copiar primero archivos de dependencias y después el resto del código.

Buen patrón:

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

Mal patrón:

COPY . .
RUN npm ci
RUN npm run build

En el mal patrón, cualquier cambio en cualquier archivo invalida la capa de instalación de dependencias. En proyectos grandes, eso vuelve lento cada build.

Construir sin cache cuando necesitas descartar capas anteriores:

docker build --no-cache -t myapp:dev .

Ver detalle del build:

docker build --progress=plain -t myapp:dev .

Multi-stage builds

Un multi-stage build usa varios FROM en el mismo Dockerfile. La idea es compilar en una etapa con herramientas pesadas y copiar solo el resultado a una imagen final más pequeña.

Ejemplo para Node.js:

# syntax=docker/dockerfile:1
FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:22-alpine AS runtime
WORKDIR /app

ENV NODE_ENV=production

COPY package*.json ./
RUN npm ci --omit=dev

COPY --from=build /app/dist ./dist
COPY --from=build /app/server.js ./server.js

USER node

EXPOSE 3000

CMD ["node", "server.js"]

Ventajas:

Construir una etapa concreta para depurar:

docker build --target build -t myapp:build .

ENTRYPOINT y CMD

CMD define el comando por defecto. ENTRYPOINT define el ejecutable principal.

Ejemplo con CMD:

CMD ["node", "server.js"]

Ejemplo con ENTRYPOINT y argumentos:

ENTRYPOINT ["node", "cli.js"]
CMD ["--help"]

Al ejecutar:

docker run --rm mycli:dev version

Docker ejecutará:

node cli.js version

Regla práctica:

Evita esta forma salvo que necesites shell:

CMD node server.js

El formato shell maneja señales peor y puede complicar apagados limpios.

Variables de entorno

Define valores por defecto en la imagen:

ENV NODE_ENV=production
ENV PORT=3000

Sobrescribe al ejecutar:

docker run --rm \
  --env PORT=4000 \
  --publish 4000:4000 \
  myapp:dev

Usa un archivo .env local:

docker run --rm --env-file .env myapp:dev

No metas secretos en la imagen:

# No hagas esto
ENV AWS_SECRET_ACCESS_KEY=...

Un secreto en una capa de imagen puede quedar expuesto en historial, registry o caches.

Volúmenes y bind mounts

Los contenedores son efímeros. Si eliminas un contenedor, su capa escribible desaparece. Para persistencia, usa volúmenes.

Crear un volumen:

docker volume create postgres-data

Usarlo con PostgreSQL:

docker run --name db \
  --detach \
  --env POSTGRES_PASSWORD=postgres \
  --volume postgres-data:/var/lib/postgresql/data \
  --publish 5432:5432 \
  postgres:16-alpine

Listar volúmenes:

docker volume ls

Inspeccionar:

docker volume inspect postgres-data

Eliminar:

docker volume rm postgres-data

Bind mount para desarrollo:

docker run --rm \
  --volume "$PWD":/app \
  --workdir /app \
  node:22-alpine \
  npm test

Cuándo usar cada uno:

Redes y puertos

Publicar un puerto:

docker run --rm -p 8080:80 nginx:1.27-alpine

Formato:

host:container

Crear una red user-defined:

docker network create app-net

Ejecutar Redis en esa red:

docker run --name redis \
  --detach \
  --network app-net \
  redis:7-alpine

Ejecutar una app en la misma red:

docker run --name app \
  --rm \
  --network app-net \
  --env REDIS_URL=redis://redis:6379 \
  myapp:dev

En una red definida por el usuario, los contenedores pueden resolverse por nombre. En el ejemplo, redis funciona como hostname.

Inspeccionar red:

docker network inspect app-net

Eliminar red:

docker network rm app-net

Regla profesional: no publiques puertos que no necesita consumir nadie desde fuera del host. La comunicación interna entre contenedores debe ir por redes privadas.

Docker Compose

Docker Compose define una aplicación multi-contenedor con YAML.

Crea compose.yml:

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: development
      DATABASE_URL: postgres://app:app@db:5432/app
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - .:/app
      - node_modules:/app/node_modules

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
      POSTGRES_DB: app
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  node_modules:
  postgres_data:

Levantar:

docker compose up

Levantar en segundo plano:

docker compose up -d

Reconstruir:

docker compose up --build

Ver servicios:

docker compose ps

Ver logs:

docker compose logs -f
docker compose logs -f app

Ejecutar comando dentro de un servicio:

docker compose exec app sh
docker compose exec app npm test

Apagar:

docker compose down

Apagar y borrar volúmenes:

docker compose down -v

No añadas version: salvo que tengas una razón de compatibilidad heredada. Compose moderno se rige por la Compose Specification.

depends_on y healthchecks

depends_on ordena arranque, pero una base de datos puede estar “arrancada” y todavía no aceptar conexiones. Por eso se usa healthcheck.

Ejemplo:

services:
  api:
    build: .
    depends_on:
      postgres:
        condition: service_healthy

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: postgres
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

En Dockerfile, puedes definir un healthcheck para la propia aplicación:

HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD wget -qO- http://localhost:3000/health || exit 1

Un healthcheck no reemplaza métricas ni alertas, pero ayuda a detectar procesos vivos que ya no responden correctamente.

Tags, registry y publicación

Construye con tag:

docker build -t ghcr.io/usuario/myapp:1.0.0 .

Añade otro tag a la misma imagen:

docker tag ghcr.io/usuario/myapp:1.0.0 ghcr.io/usuario/myapp:latest

Inicia sesión:

docker login ghcr.io

Publica:

docker push ghcr.io/usuario/myapp:1.0.0
docker push ghcr.io/usuario/myapp:latest

Buenas prácticas de tagging:

Ejecutar por digest:

docker pull ghcr.io/usuario/myapp@sha256:...

El digest identifica exactamente el contenido de la imagen.

Buildx y multi-arquitectura

Buildx permite construir con BuildKit y crear imágenes para varias arquitecturas.

Ver builders:

docker buildx ls

Crear builder:

docker buildx create --name multiarch --use

Construir y publicar para amd64 y arm64:

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag ghcr.io/usuario/myapp:1.0.0 \
  --push \
  .

Esto es útil si tu equipo desarrolla en Apple Silicon pero producción corre en Linux amd64, o si distribuyes imágenes para varios entornos.

Docker en CI/CD

Un pipeline típico:

checkout
tests
docker build
scan
docker push
deploy

Ejemplo con GitHub Actions:

name: Docker

on:
  push:
    branches:
      - main
    tags:
      - "v*.*.*"
  pull_request:
    branches:
      - main

permissions:
  contents: read
  packages: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - uses: docker/login-action@v3
        if: github.event_name == 'push'
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: docker/build-push-action@v6
        with:
          context: .
          push: ${{ github.event_name == 'push' }}
          tags: ghcr.io/usuario/myapp:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Puntos importantes:

Seguridad práctica

Docker no es una frontera mágica de seguridad. Un contenedor comparte kernel con el host, por lo que debes reducir privilegios.

Buenas prácticas:

Ejecutar como usuario no root en Dockerfile:

FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
CMD ["node", "server.js"]

Ejecutar con filesystem de solo lectura:

docker run --rm \
  --read-only \
  --tmpfs /tmp \
  myapp:prod

Limitar memoria y CPU:

docker run --rm \
  --memory 512m \
  --cpus 1 \
  myapp:prod

Escanear con Docker Scout si está disponible:

docker scout quickview myapp:prod
docker scout cves myapp:prod

Debugging de contenedores

Ver procesos:

docker top myapp

Ver consumo:

docker stats

Inspeccionar metadatos:

docker inspect myapp

Ver logs:

docker logs --tail 100 myapp
docker logs -f myapp

Entrar a un contenedor:

docker exec -it myapp sh

Copiar archivos desde un contenedor:

docker cp myapp:/app/logs ./logs

Ver puertos:

docker port myapp

Ver cambios en el filesystem del contenedor:

docker diff myapp

Si el contenedor se cierra inmediatamente, revisa el exit code:

docker ps -a
docker logs myapp
docker inspect myapp --format '{{.State.ExitCode}}'

Patrón de diagnóstico:

docker ps -a
docker logs
docker inspect
docker exec
docker stats
docker events

Limpieza segura

Docker acumula imágenes, contenedores parados, redes y cache.

Borrar contenedores parados:

docker container prune

Borrar imágenes no usadas:

docker image prune

Borrar redes no usadas:

docker network prune

Borrar volúmenes no usados:

docker volume prune

Borrar todo lo no usado:

docker system prune

Incluir volúmenes:

docker system prune --volumes

Ten cuidado con --volumes: puede eliminar datos persistentes de bases de datos locales.

Ver uso de disco:

docker system df

Flujo profesional recomendado

Para una aplicación de backend:

docker build -t myapp:dev .
docker run --rm -p 3000:3000 myapp:dev
docker compose up --build
docker compose exec app npm test
docker build -t ghcr.io/usuario/myapp:1.0.0 .
docker push ghcr.io/usuario/myapp:1.0.0

Para desarrollo local:

docker compose up --build
docker compose logs -f app
docker compose exec app sh
docker compose down

Para release:

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag ghcr.io/usuario/myapp:1.0.0 \
  --push \
  .

Antes de publicar:

docker run --rm myapp:prod npm test
docker run --rm -p 3000:3000 myapp:prod
docker scout quickview myapp:prod

Errores comunes

Checklist antes de llevar Docker a producción

Chuleta de comandos

ObjetivoComando
Ver versióndocker version
Ver estado del enginedocker info
Listar contenedores activosdocker ps
Listar todos los contenedoresdocker ps -a
Listar imágenesdocker images
Descargar imagendocker pull nginx:1.27-alpine
Construir imagendocker build -t myapp:dev .
Ejecutar contenedordocker run --rm myapp:dev
Publicar puertodocker run -p 8080:80 nginx
Ver logsdocker logs -f nombre
Entrar al contenedordocker exec -it nombre sh
Detener contenedordocker stop nombre
Eliminar contenedordocker rm nombre
Eliminar imagendocker rmi imagen:tag
Crear reddocker network create app-net
Crear volumendocker volume create data
Levantar Composedocker compose up --build
Apagar Composedocker compose down
Publicar imagendocker push registry/app:tag
Ver uso de discodocker system df
Limpiar recursosdocker system prune

Glosario

Bind mount: montaje de una ruta del host dentro de un contenedor.

Build context: conjunto de archivos enviados al builder durante docker build.

BuildKit: backend moderno de construcción de imágenes Docker, con mejor cache, paralelización y funcionalidades avanzadas.

Container: instancia ejecutable de una imagen.

Dockerfile: archivo declarativo que define cómo construir una imagen.

Entrypoint: ejecutable principal de una imagen.

Healthcheck: prueba periódica para determinar si un contenedor está saludable.

Image: plantilla inmutable compuesta por capas y metadatos.

Layer: capa del filesystem generada por una instrucción del build.

Multi-stage build: técnica que usa varias etapas de build para producir una imagen final más pequeña y limpia.

Port publishing: exposición de un puerto del contenedor en el host mediante -p o --publish.

Registry: servidor donde se almacenan y distribuyen imágenes.

Tag: etiqueta legible asociada a una imagen.

Volume: almacenamiento persistente gestionado por Docker.

Compose: herramienta para definir aplicaciones multi-contenedor mediante YAML.

Runner: máquina donde CI/CD ejecuta comandos Docker.

Digest: identificador inmutable del contenido de una imagen.

Namespace: aislamiento del kernel usado por contenedores para procesos, red, mounts y otros recursos.

cgroups: mecanismo del kernel para limitar y contabilizar recursos como CPU y memoria.

Referencias oficiales

Cierre

Docker bien usado no consiste solo en envolver una aplicación en una imagen. Consiste en construir artifacts reproducibles, pequeños, seguros y observables; separar configuración de código; persistir datos donde corresponde; modelar redes con intención; y automatizar publicación y despliegue. Si tu Dockerfile, tu compose.yml y tu pipeline son simples de leer, reproducibles y seguros por defecto, vas por el camino correcto.