From n00b to ZeroCool / Profesionalización

Docker: empaqueta tu app como si supieras lo que haces (aunque sea lunes)

Guía práctica para dockerizar tu app: Dockerfile, build, run, multi-stage, .dockerignore, errores típicos y checklist para producción.

Lo que vale la pena leer aquí

Ahí es donde sale el clásico: dependencias instaladas a mano, una versión de Node que jurabas que era la misma, variables de entorno guardadas en tu cerebro (mala idea) y un README que escribió tu yo con fe de viernes.

Intro con gancho

Tu app corre perfecto en tu laptop… hasta que cae el mensaje: “¿Lo puedes subir a staging antes de la junta de las 6?”. Y tú con tu compu ya cansada, ventilador al 200%, y un setup que según estaba “bien documentado”.

Ahí es donde sale el clásico: dependencias instaladas a mano, una versión de Node que jurabas que era la misma, variables de entorno guardadas en tu cerebro (mala idea) y un README que escribió tu yo con fe de viernes.

Docker no te vuelve mago. Pero sí te baja la fricción: empacas tu app con lo que necesita para correr y la mueves entre tu máquina, el server del cliente o el runner de CI sin andar rezándole al “en mi máquina sí funciona”.

Qué vas a aprender

  • Cómo escribir un Dockerfile decente (no el copypaste que aguanta… hasta que no).
  • Cómo build/run/tag imágenes sin hacerte bolas.
  • Cómo usar multi-stage builds para imágenes más ligeras.
  • Cómo manejar variables de entorno, puertos, volúmenes y usuarios.
  • Errores comunes que sí pegan en el jale (y cómo salir sin quemar production).

Contexto práctico (por qué esto sí importa en el jale)

Cuando dockerizas bien:

  • El onboarding deja de ser talacha: la banda nueva corre el proyecto con un docker compose up y listo.
  • Tu pipeline deja de depender del “humor” del runner.
  • Tus deploys se vuelven repetibles: misma imagen que probaste = misma imagen que publicas.

Cuando dockerizas mal:

  • Imágenes de 2GB “porque se pudo”.
  • Builds lentísimos (y el CI te cobra ese tiempo).
  • Secrets horneados dentro de la imagen (y luego a ver quién hace rollback sin llorar).
  • Permisos rotos en producción (hola, contenedor corriendo como root).

Lo más real: Docker es para reducir variabilidad. No elimina bugs, pero sí ayuda a que el bug sea tuyo, no del entorno.

Paso a paso: dockeriza una app sin sufrir

Voy a usar ejemplos de Node.js y Python porque son pan de cada día. El patrón aplica igual para Go/Java/.NET.

1) Aclara qué estás empaquetando

Antes de tocar el Dockerfile, contesta en corto:

  • ¿Cómo se corre? (npm start, python app.py, gunicorn, etc.)
  • ¿Qué puerto expone? (3000, 8080…)
  • ¿Qué necesita en runtime? (binarios, libs, CA certs, ffmpeg, etc.)
  • ¿Qué NO debe ir a la imagen? (node_modules, .env, logs)

Este mini inventario te evita el Dockerfile “a ciegas” que luego te cuesta una noche.

2) Crea un .dockerignore (sí, primero)

Si no lo haces, Docker manda TODO tu folder como build context. Si traes node_modules/ o un venv/, tu build se vuelve mudanza con refrigerador incluido.

Crea .dockerignore:

node_modules
npm-debug.log
.env
.git
.gitignore
Dockerfile
docker-compose.yml
dist
build
__pycache__
*.pyc
.venv
venv

Tip de guerra: cuando el build está raro y lento, muchas veces ni es Docker… es tu contexto gigante.

3) Dockerfile base (Node.js) — versión “ya me pegó en producción”

Para una API típica (Express/Nest/etc.).

# syntax=docker/dockerfile:1

FROM node:20-alpine AS deps
WORKDIR /app

# Copia solo manifests para cachear dependencias
COPY package*.json ./
RUN npm ci

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

# Copia node_modules desde la etapa deps
COPY --from=deps /app/node_modules ./node_modules

# Copia el resto del código
COPY . .

# Seguridad básica: no corras como root
RUN addgroup -S app && adduser -S app -G app
USER app

EXPOSE 3000
CMD ["npm","start"]

Decisiones que sí importan (tradeoffs reales):

  • npm ci en vez de npm install: más reproducible.
  • Separar COPY package*.json del resto: cachea capas y acelera builds.
  • alpine: imagen más chica, pero a veces duele si compilas dependencias nativas (bcrypt, sharp). Si te pega con deadline encima, cámbiate a node:20-slim.
  • USER app: te evita sustos cuando montas volúmenes o te toca un cluster más estricto.

4) Dockerfile para Python (FastAPI/Flask) — limpio y práctico

# syntax=docker/dockerfile:1

FROM python:3.12-slim AS base
WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

# Dependencias del sistema (ajusta según tu caso)
RUN apt-get update \
    && apt-get install -y --no-install-recommends build-essential \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# Usuario no-root
RUN useradd -m appuser
USER appuser

EXPOSE 8000
CMD ["python","-m","uvicorn","main:app","--host","0.0.0.0","--port","8000"]

Si el build se pone pesado por compilar cosas, evalúa wheels precompilados o arma una etapa de builder (multi-stage) y copia el resultado. Eso te baja tiempo y drama en CI.

5) Build, tag y run como persona funcional

Build:

docker build -t miapp:dev .

Correr:

docker run --rm -p 3000:3000 --name miapp miapp:dev

Con variables de entorno (sin hornear secretos en la imagen):

docker run --rm -p 3000:3000 \
  -e NODE_ENV=production \
  -e DATABASE_URL="postgres://..." \
  miapp:dev

Ver logs:

docker logs -f miapp

Entrar al contenedor (debug express cuando ya estás contra la pared):

docker exec -it miapp sh

6) Multi-stage build para frontend (React/Vite/Next export) o Node con build

Si tu app tiene build step (npm run build) y luego solo sirves dist/, no te lleves las herramientas de build al runtime. Eso es peso muerto.

Ejemplo (Vite + Nginx):

# syntax=docker/dockerfile:1

FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine AS runtime
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80

Bajas tamaño y superficie de ataque. También arranca más rápido, que en production se agradece.

Docker: empaqueta tu app como si supieras lo que haces (aunque sea lunes) - visual explicativa 1
Visual de apoyo: Intro con gancho

7) Docker Compose para el combo real: app + base de datos

Porque lo normal es:

  • un backend
  • una DB
  • y el “luego vemos observabilidad”

docker-compose.yml básico:

services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgres://postgres:postgres@db:5432/postgres
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_PASSWORD=postgres
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

Correr:

docker compose up --build

Tradeoff sin humo: Compose es buenísimo para dev, demos y reproducir bugs. Para prod ya depende (ECS, Kubernetes, Nomad, etc.). Pero si ni Compose tienes, hablar de Kubernetes es querer hacer deploy sin saber ni dónde está el Dockerfile.

8) Cosas que te ahorran horas en CI/CD

  • Pin de versiones: node:20-alpine en vez de node:latest.
  • Capas cacheables: copia manifests antes del código.
  • BuildKit: si tu entorno lo soporta, úsalo.

Ejemplo con BuildKit:

DOCKER_BUILDKIT=1 docker build -t miapp:ci .

Screenshots sugeridos

  • Terminal con docker build mostrando cache hits (capas reutilizadas).
  • docker images con tamaño antes/después de multi-stage.
  • docker ps y docker logs -f en ejecución.
  • Estructura del repo mostrando .dockerignore y Dockerfile.
  • docker compose up levantando app y Postgres.

Errores comunes + solución (los que sí pasan)

1) “Mi imagen pesa 1.8GB”

Causa típica: no tienes .dockerignore, o metiste node_modules, venv, dist, etc.

Solución: agrega .dockerignore y copia solo lo necesario. Si se tarda un buen en “Sending build context…”, ya encontraste el bug.

2) “No encuentra dependencias en runtime”

Causa típica: instalaste dependencias en una etapa pero no las copiaste, o NODE_ENV=production te brincó devDependencies que sí necesitabas.

Solución:

  • Separa build vs runtime como debe ser.
  • Si tu npm start depende de dev, tu workflow está chueco: mueve lo necesario a dependencies o ajusta el entrypoint.

3) “Permission denied” al escribir archivos

Causa típica: corres como usuario no-root (bien), pero tu app intenta escribir en /app o en una carpeta sin permisos.

Solución: crea directorios con permisos correctos o escribe en /tmp.

RUN mkdir -p /app/uploads && chown -R app:app /app/uploads

4) “Funciona local, truena en contenedor”

Causa típica: usas localhost dentro del contenedor para hablar con DB/Redis. En Docker, localhost eres tú mismo (el contenedor), no el servicio de al lado.

Solución: usa el nombre del servicio de Compose (db, redis).

5) “Mi build falla con Alpine”

Causa típica: dependencias nativas que necesitan toolchain o librerías (musl vs glibc).

Solución:

  • Instala deps del sistema (talacha controlada), o
  • Cambia a -slim.

Neta: a veces “ligero” sale caro si te avientas dos horas peleando compilación con el deploy encima.

Docker: empaqueta tu app como si supieras lo que haces (aunque sea lunes) - visual explicativa 2
Visual de apoyo: Qué vas a aprender

Checklist final (para decir: ‘sí, ya quedó’)

  • Tengo .dockerignore y el build context no trae basura.
  • Mi Dockerfile aprovecha cache (manifests primero).
  • No meto .env ni secretos dentro de la imagen.
  • La imagen corre como usuario no-root (cuando aplique).
  • docker run -p funciona y responde un healthcheck básico (aunque sea /health).
  • En Compose, uso nombres de servicio (no localhost) para dependencias.
  • La imagen tiene tag claro (miapp:1.0.0), no solo latest.
  • Tamaño razonable (API: intenta < 300–500MB; estática: decenas de MB).

FAQ

1) ¿Docker es lo mismo que una VM?

No. Una VM trae un sistema operativo completo. Docker usa contenedores que comparten el kernel del host: menos overhead y más rápido para empaquetar y mover apps.

2) ¿Debo usar latest?

Para pruebas rápidas, va. Para CI/production, mejor pin (ej. node:20-alpine) o incluso digest. latest te cambia el piso sin avisar.

3) ¿Dónde pongo mis variables de entorno?

En runtime: docker run -e, --env-file, secrets del orquestador, o variables del runner CI. Evita meterlas al build porque terminan en capas de imagen.

4) ¿Cómo sé qué puerto exponer?

El que tu app escucha dentro del contenedor. EXPOSE sirve como documentación, pero manda -p host:container.

5) ¿Qué tan “seguro” es correr todo en Docker?

Mejora mucho la higiene (aislamiento, reproducibilidad), pero no es escudo mágico. Cuida usuarios no-root, imágenes mínimas, updates y cero secretos dentro de la imagen.

Siguiente episodio

Ya que tu app vive en un contenedor sin drama, el siguiente paso es dejar de correr comandos a mano.

Toca armar un pipeline de build/test/push para que tu Docker image salga lista en cada merge, sin rezos, sin “deploy de compas” y con rollback cuando haga falta.