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 upy 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 cien vez denpm install: más reproducible.- Separar
COPY package*.jsondel 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 anode: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.

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-alpineen vez denode: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 buildmostrando cache hits (capas reutilizadas). docker imagescon tamaño antes/después de multi-stage.docker psydocker logs -fen ejecución.- Estructura del repo mostrando
.dockerignoreyDockerfile. docker compose uplevantando 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 startdepende de dev, tu workflow está chueco: mueve lo necesario adependencieso 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.

Checklist final (para decir: ‘sí, ya quedó’)
- Tengo
.dockerignorey el build context no trae basura. - Mi Dockerfile aprovecha cache (manifests primero).
- No meto
.envni secretos dentro de la imagen. - La imagen corre como usuario no-root (cuando aplique).
-
docker run -pfunciona 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 sololatest. - 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.
Idea para cerrar bien este post: toma una sola práctica de aquí y conviértela en algo que tu equipo pueda aplicar hoy.
Cuando un artículo aterriza en decisiones reales, deja de ser contenido y se vuelve ventaja.


