Resumen

Docker es una plataforma para empaquetar una aplicación junto con todo lo que necesita para correr (runtime, librerías, configuración) en una unidad llamada contenedor. Ese contenedor funciona igual en tu notebook, en el servidor de un compañero o en producción.

Analogía: el contenedor de carga

Antes de los contenedores de carga estándar, cada barco se cargaba distinto según la mercadería: bolsas, barriles, cajas. El contenedor metálico estandarizó todo: a la grúa no le importa qué hay adentro, solo sabe moverlo. Docker hace lo mismo con el software: a quien lo ejecuta no le importa si adentro hay Python, Node o Java.

Key points

  • Resuelve el clásico “en mi máquina funciona”: el entorno viaja con la app
  • Una imagen es la plantilla (solo lectura); un contenedor es una instancia corriendo de esa imagen
  • Los contenedores comparten el kernel del sistema operativo anfitrión: por eso son mucho más livianos que una máquina virtual
  • Las imágenes se construyen en capas que se cachean y se reutilizan
  • Un contenedor es efímero: lo que no esté en un volumen se pierde al borrarlo

El problema que resuelve

"En mi máquina funciona"

La app anda en tu computadora con Python 3.12 y Postgres 16, pero en el servidor hay Python 3.9, a otra persona del equipo le falta una librería del sistema y en producción una variable de entorno tiene otro valor. Cada entorno es distinto, y cada diferencia es un bug potencial.

Con Docker, el entorno se describe en un archivo (Dockerfile), se construye una vez como imagen, y esa misma imagen se ejecuta en todos lados.

Contenedores vs. máquinas virtuales

flowchart TB
    subgraph vm["Máquinas virtuales"]
        direction TB
        VA[App A] --- VLA[Librerías A] --- VOA[SO invitado A]
        VB[App B] --- VLB[Librerías B] --- VOB[SO invitado B]
        VOA --- HYP[Hipervisor]
        VOB --- HYP
        HYP --- HW1[Hardware]
    end
    subgraph ct["Contenedores"]
        direction TB
        CA[App A] --- CLA[Librerías A] --- ENG[Docker Engine]
        CB[App B] --- CLB[Librerías B] --- ENG
        ENG --- KER[Kernel del SO anfitrión]
        KER --- HW2[Hardware]
    end
Máquina virtualContenedor
Qué virtualizaEl hardware completoEl sistema operativo (procesos aislados)
Sistema operativoUno completo por VMComparte el kernel del anfitrión
Tamaño típicoGigabytesMegabytes
ArranqueMinutosSegundos o menos
AislamientoMuy fuerteBueno, pero el kernel es compartido
Cuándo convieneCorrer otro SO, aislamiento estrictoEmpaquetar y desplegar aplicaciones

¿Y en Windows o macOS?

Los contenedores de Linux necesitan un kernel Linux. Docker Desktop resuelve esto corriendo una máquina virtual Linux liviana por detrás (en Windows, a través de WSL 2). Por eso Docker en Windows consume más recursos que en un servidor Linux.

Cómo funciona por dentro

Un contenedor no es magia: es un proceso común de Linux con tres mecanismos del kernel encima.

MecanismoQué haceEjemplo
NamespacesAíslan lo que el proceso veTiene sus propios PIDs (se ve a sí mismo como PID 1), su propia red, su propio hostname y su propio sistema de archivos
cgroupsLimitan lo que el proceso usaMáximo 512 MB de RAM o medio núcleo de CPU
Union filesystem (OverlayFS)Apila capas de solo lectura y agrega una capa escribible arribaDiez contenedores de la misma imagen comparten las capas base en disco

Para recordarlo

Namespaces = qué puede ver. cgroups = cuánto puede usar.

Conceptos clave

flowchart LR
    DF[Dockerfile<br/>la receta] -->|docker build| IMG[Imagen<br/>la plantilla]
    IMG -->|docker run| C1[Contenedor 1]
    IMG -->|docker run| C2[Contenedor 2]
    IMG -->|docker push| REG[(Registry<br/>Docker Hub, GHCR)]
    REG -->|docker pull| IMG2[Imagen en<br/>otro servidor]
ConceptoQué esAnalogía en POO
DockerfileArchivo de texto con las instrucciones para construir una imagenEl código fuente de la clase
ImagenPlantilla inmutable, formada por capasLa clase
ContenedorInstancia en ejecución de una imagen, con una capa escribible propiaUn objeto (instancia)
RegistryServidor que almacena y distribuye imágenes (Docker Hub, GitHub Container Registry)Un repositorio de paquetes, como PyPI o npm
VolumenAlmacenamiento que sobrevive al contenedorUna base de datos externa al objeto
RedConecta contenedores entre sí y con el exterior—

Nombres de imagen y tags

ghcr.io/checorazza/mi-api:1.4.0 se lee como registry/usuario/repositorio:tag. Si no se indica registry, se asume Docker Hub; si no se indica tag, se asume latest.

latest no significa "la última versión"

latest es solo el tag por defecto, no se actualiza solo ni garantiza nada. Usar FROM python:latest hace que el build cambie de un día para otro sin aviso. Siempre fijar una versión concreta: python:3.12-slim.

Arquitectura de Docker

flowchart LR
    CLI[docker CLI] -->|API REST| D[dockerd<br/>Docker daemon]
    D --> CD[containerd<br/>gestiona contenedores]
    CD --> R[runc<br/>crea el proceso aislado]
    R --> CONT[Contenedor]
    D <-->|pull / push| REG[(Registry)]
  • docker CLI: el comando que usás. Solo le habla al daemon.
  • dockerd: el daemon, que construye imágenes, maneja redes y volúmenes.
  • containerd y runc: las piezas de bajo nivel que realmente crean y supervisan los contenedores. Kubernetes usa containerd directamente, sin dockerd.

El estándar OCI

Las imágenes y los runtimes siguen el estándar OCI (Open Container Initiative). Por eso una imagen construida con Docker corre en Podman, en containerd o en Kubernetes, y al revés.

Ciclo de vida de un contenedor

stateDiagram-v2
    [*] --> Created: docker create
    Created --> Running: docker start
    Running --> Paused: docker pause
    Paused --> Running: docker unpause
    Running --> Exited: docker stop / el proceso termina
    Exited --> Running: docker start
    Exited --> [*]: docker rm

docker run es un atajo para create + start. Un contenedor vive mientras viva su proceso principal: si ese proceso termina, el contenedor pasa a Exited.

Dockerfile

Instrucciones principales

InstrucciónPara qué
FROMImagen base de la que se parte (siempre la primera instrucción)
WORKDIRDirectorio de trabajo para las instrucciones siguientes
COPYCopia archivos del proyecto a la imagen
RUNEjecuta un comando al construir la imagen (instalar dependencias)
ENVDefine variables de entorno
ARGVariables disponibles solo durante el build
EXPOSEDocumenta en qué puerto escucha la app (no publica nada por sí solo)
USERUsuario con el que corren las instrucciones siguientes y el contenedor
CMDComando por defecto al ejecutar el contenedor
ENTRYPOINTEjecutable fijo del contenedor; CMD le pasa los argumentos por defecto

Ejemplo: API en Python

Estructura del proyecto:

mi-api/
├── Dockerfile
├── .dockerignore
├── requirements.txt
└── main.py
# requirements.txt
fastapi==0.115.0
uvicorn==0.30.6
# main.py
from fastapi import FastAPI
 
app = FastAPI()
 
@app.get("/")
def inicio():
    return {"mensaje": "Hola desde un contenedor"}
FROM python:3.12-slim
 
WORKDIR /app
 
# 1. Primero solo las dependencias: esta capa se reutiliza
#    mientras requirements.txt no cambie
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
 
# 2. Después el código, que cambia seguido
COPY . .
 
# 3. No correr como root
RUN useradd --create-home appuser
USER appuser
 
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
docker build -t mi-api:1.0 .
docker run -d --name api -p 8000:8000 mi-api:1.0
curl http://localhost:8000   # {"mensaje":"Hola desde un contenedor"}

--host 0.0.0.0

Si la app escucha en 127.0.0.1, solo acepta conexiones desde adentro del contenedor y no se puede acceder desde afuera aunque el puerto esté publicado. Dentro de un contenedor, las apps tienen que escuchar en 0.0.0.0.

Capas y caché

Cada instrucción que modifica archivos (RUN, COPY) crea una capa. Al reconstruir, Docker reutiliza las capas que no cambiaron, pero en cuanto una capa cambia, todas las de arriba se reconstruyen.

flowchart BT
    L1["FROM python:3.12-slim<br/>(capa base, compartida)"] --> L2["COPY requirements.txt<br/>RUN pip install<br/>(cacheada si no cambian las dependencias)"]
    L2 --> L3["COPY . .<br/>(se reconstruye en cada cambio de código)"]
    L3 --> L4["Capa escribible del contenedor<br/>(se pierde al borrarlo)"]

Regla de oro del orden

Lo que cambia poco va arriba, lo que cambia seguido va abajo. Si se hace COPY . . antes del pip install, cada cambio en una línea de código reinstala todas las dependencias.

CMD vs. ENTRYPOINT

CMDENTRYPOINT
RolComando o argumentos por defectoEl ejecutable fijo del contenedor
Se reemplaza condocker run imagen otro-comandodocker run --entrypoint ...
Uso típicoApps webContenedores que funcionan como una herramienta de línea de comandos
ENTRYPOINT ["python", "convertir.py"]
CMD ["--formato", "png"]
# docker run conversor            -> python convertir.py --formato png
# docker run conversor --formato jpg -> python convertir.py --formato jpg

Forma exec vs. forma shell

CMD ["uvicorn", "main:app"] (exec, con corchetes) ejecuta el proceso directamente. CMD uvicorn main:app (shell) lo ejecuta a través de /bin/sh, que pasa a ser el proceso principal y no reenvía las señales: docker stop no puede cerrar la app de forma ordenada y termina matándola a los 10 segundos. Preferir siempre la forma exec.

.dockerignore

Funciona como .gitignore, pero para lo que se copia a la imagen. Evita imágenes pesadas y que se filtren secretos.

.git
.venv
__pycache__/
node_modules/
.env
*.log

Multi-stage builds

Una imagen puede tener varias etapas: una para compilar (con todas las herramientas de build) y otra final que solo copia el resultado. La imagen final no lleva compiladores ni dependencias de desarrollo.

# Etapa 1: compilar el frontend
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
 
# Etapa 2: solo los archivos estáticos, servidos por nginx
FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
flowchart LR
    subgraph s1["Etapa build (se descarta)"]
        N[node:22-alpine] --> NM[node_modules<br/>código fuente<br/>herramientas de build] --> DIST["dist/"]
    end
    subgraph s2["Imagen final"]
        NG[nginx:1.27-alpine] --> HTML[archivos estáticos]
    end
    DIST -->|COPY --from=build| HTML

Resultado: en vez de una imagen de cientos de MB con Node y node_modules, queda una de unas pocas decenas de MB.

Comandos esenciales

TareaComando
Construir una imagendocker build -t nombre:tag .
Listar imágenesdocker images
Crear y arrancar un contenedordocker run -d --name app -p 8080:80 imagen
Contenedores corriendo / todosdocker ps / docker ps -a
Ver logs (en vivo)docker logs -f app
Abrir una terminal adentrodocker exec -it app sh
Detener / arrancardocker stop app / docker start app
Borrar contenedor / imagendocker rm app / docker rmi imagen
Ver configuración completadocker inspect app
Uso de CPU y memoriadocker stats
Descargar / subir imágenesdocker pull imagen / docker push imagen
Limpiar lo que no se usadocker system prune

Flags de docker run más usados:

FlagQué hace
-dCorre en segundo plano (detached)
-p 8080:80Publica el puerto 80 del contenedor en el 8080 del anfitrión
-e CLAVE=valorDefine una variable de entorno
-v volumen:/rutaMonta un volumen
--nameLe da un nombre al contenedor
--rmBorra el contenedor cuando termina
-itModo interactivo con terminal
--restart unless-stoppedLo reinicia si se cae o si se reinicia el servidor

Volúmenes: persistir datos

La capa escribible de un contenedor desaparece con el contenedor. Para datos que tienen que sobrevivir (una base de datos, archivos subidos) se usan montajes:

flowchart LR
    subgraph host["Anfitrión"]
        NV[("Volumen con nombre<br/>gestionado por Docker")]
        BM["Carpeta del anfitrión<br/>./src"]
        RAM["Memoria RAM"]
    end
    subgraph cont["Contenedor"]
        P1["/var/lib/postgresql/data"]
        P2["/app/src"]
        P3["/tmp/cache"]
    end
    NV --- P1
    BM --- P2
    RAM --- P3
TipoSintaxisCuándo usarlo
Volumen con nombre-v datos-db:/var/lib/postgresql/dataDatos de producción, bases de datos. Docker gestiona dónde se guardan
Bind mount-v ./src:/app/srcDesarrollo: editás el código en tu máquina y el contenedor ve los cambios al instante
tmpfs--tmpfs /tmp/cacheDatos temporales que no deben tocar el disco

Redes

RedComportamiento
bridge (por defecto)Red privada interna. Los contenedores se comunican por IP
bridge definida por el usuarioIgual, pero con DNS interno: los contenedores se encuentran por nombre (db, api)
hostEl contenedor usa directamente la red del anfitrión, sin aislamiento
noneSin red
flowchart LR
    U[Navegador] -->|localhost:8000| P{{"Puerto publicado<br/>-p 8000:8000"}}
    subgraph red["Red interna de Docker"]
        API[api:8000] -->|"db:5432<br/>(resuelto por DNS interno)"| DB[(db:5432)]
    end
    P --> API

El puerto de la base de datos no está publicado: solo la API puede llegar a ella. Es la forma correcta: publicar únicamente lo que tiene que ser accesible desde afuera.

Docker y el firewall

En Linux, Docker escribe sus propias reglas de iptables para publicar puertos, y esas reglas pasan por encima de UFW. Un -p 5432:5432 deja la base de datos accesible desde internet aunque UFW diga que el puerto está cerrado. Para exponer un puerto solo a la propia máquina: -p 127.0.0.1:5432:5432.

Docker Compose

Levantar una app con API + base de datos usando docker run implica crear la red, los volúmenes y cada contenedor a mano con sus flags. Compose describe todo en un archivo YAML y lo levanta con un solo comando.

# compose.yaml
services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgresql://app:${DB_PASSWORD}@db:5432/tienda
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped
 
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: tienda
    volumes:
      - datos-db:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d tienda"]
      interval: 5s
      timeout: 3s
      retries: 5
 
volumes:
  datos-db:
# .env (en el mismo directorio, y en .gitignore)
DB_PASSWORD=una-contraseña-segura
  • Compose crea automáticamente una red propia para el proyecto: api encuentra a la base de datos como db.
  • ${DB_PASSWORD} se lee del archivo .env, así la contraseña no queda en el repositorio.
  • depends_on con condition: service_healthy espera a que Postgres esté listo para recibir conexiones, no solo a que el contenedor haya arrancado.
ComandoQué hace
docker compose up -dConstruye (si hace falta) y levanta todo en segundo plano
docker compose up -d --buildFuerza a reconstruir las imágenes
docker compose psEstado de los servicios
docker compose logs -f apiLogs de un servicio
docker compose exec db psql -U app tiendaEjecuta un comando dentro de un servicio
docker compose downDetiene y borra contenedores y red (los volúmenes quedan)
docker compose down -vLo mismo, borrando también los volúmenes (se pierden los datos)

Versiones de Compose

El comando actual es docker compose (con espacio, integrado a Docker). docker-compose (con guion) es la versión 1, ya discontinuada. La clave version: al principio del YAML también es obsoleta y se puede omitir.

Buenas prácticas

  • Imágenes base chicas: variantes -slim o -alpine, o imágenes distroless. Menos paquetes = menos tamaño y menos vulnerabilidades
  • Versiones fijas en FROM y en las dependencias, nunca latest
  • Ordenar las instrucciones para aprovechar la caché: dependencias antes que el código
  • Un proceso por contenedor: la API en uno, la base de datos en otro. Se escalan, actualizan y reinician por separado
  • Usuario sin privilegios con USER, en vez de root
  • .dockerignore para no copiar .git, node_modules, .env
  • Multi-stage builds para que la imagen final no lleve herramientas de compilación
  • Configuración por variables de entorno, no hardcodeada en la imagen: la misma imagen sirve para desarrollo, staging y producción
  • Healthchecks para que Docker (o el orquestador) sepa si la app realmente responde

Los secretos quedan en las capas

Si un Dockerfile hace COPY .env . y en una instrucción posterior RUN rm .env, el archivo sigue existiendo en la capa anterior y cualquiera con la imagen puede extraerlo. Los secretos nunca se copian a la imagen: se pasan en tiempo de ejecución (variables de entorno, Docker secrets) o, si se necesitan durante el build, con RUN --mount=type=secret.

El grupo docker equivale a root

Quien puede ejecutar comandos de Docker puede montar / del anfitrión dentro de un contenedor y modificar cualquier archivo del sistema. Agregar un usuario al grupo docker es darle acceso de administrador. Para evitarlo existe el modo rootless de Docker, o Podman.

Docker en el ecosistema

HerramientaQué es
Docker ComposeVarios contenedores en una sola máquina
KubernetesOrquestación de contenedores en muchas máquinas: escalado automático, reinicio de contenedores caídos, despliegues sin downtime
PodmanAlternativa a Docker sin daemon y rootless por defecto, con comandos compatibles (podman run ...)
RegistriesDocker Hub, GitHub Container Registry, registries de AWS, GCP y Azure

En un pipeline de CI/CD típico, cada push construye una imagen, corre los tests dentro de esa imagen, la sube a un registry con el hash del commit como tag, y producción despliega exactamente esa imagen.

Ventajas y desventajas

  • El entorno es reproducible: se elimina el “en mi máquina funciona”
  • Un proyecto nuevo se levanta con un solo comando, sin instalar bases de datos ni runtimes a mano
  • Aislamiento entre apps: cada una con sus propias versiones de dependencias
  • Mucho más liviano y rápido que una máquina virtual
  • La misma imagen va de desarrollo a producción
  • Curva de aprendizaje: redes, volúmenes y capas no son triviales
  • El aislamiento es menor que el de una VM, porque el kernel es compartido
  • En Windows y macOS corre sobre una VM, con más consumo de recursos y acceso a archivos más lento
  • Los datos persistentes requieren pensar en volúmenes y backups desde el principio