Cómo dockerizar una aplicación

Para dockerizar una aplicación, escribe un Dockerfile con la imagen base, las dependencias, el código y el comando de inicio. Luego ejecuta docker build para crear la imagen y docker run para iniciar un contenedor.

Actualizado el 5 min de lectura

Objetivos de aprendizaje

Después de leer este artículo podrás:

  • Escribir un Dockerfile funcional para una aplicación Node.js
  • Construir la imagen y ejecutar el contenedor con variables de entorno
  • Reducir la imagen con .dockerignore, caché de capas y multi-stage
En esta página

"En mi máquina funciona" suele deberse a diferencias pequeñas: la versión de Node, una librería del sistema, una variable olvidada. Dockerizar una aplicación lo resuelve empaquetando todo lo que necesita en una imagen que se comporta igual en tu laptop, en el CI y en un servidor.

El punto de partida es un archivo de texto llamado Dockerfile. El ejemplo usa Node.js, pero la lógica sirve para cualquier lenguaje: elegir una base, instalar dependencias, copiar el código y definir el comando de inicio.

DockerfileCódigodockerbuildImagencódigodependenciasimagen basecapas desolo lecturadockerrunContenedoren un portátilContenedoren CIContenedoren un servidor
El build empaqueta el Dockerfile y el código en una imagen por capas; run inicia esa misma imagen en cualquier máquina.

¿Qué necesitas antes de empezar?#

  • Docker instalado (Docker Desktop en Windows y macOS, o Docker Engine en Linux). Compruébalo con docker --version.
  • Una aplicación con un comando de inicio conocido. En el ejemplo, un package.json y un package-lock.json, con el punto de entrada en src/index.js.
  • Una terminal abierta en la raíz del proyecto.

¿Qué es una imagen y qué es un contenedor?#

La imagen es el paquete: un sistema base, las dependencias y tu código, guardados como capas de solo lectura. El contenedor es una instancia en ejecución de esa imagen. De una misma imagen puedes iniciar muchos contenedores. El Dockerfile es la receta que describe cómo construir la imagen. Para ver cómo se comparan los contenedores con los servidores completos, lee VPS vs plataforma en la nube.

¿Cómo se escribe el primer Dockerfile?#

Crea un archivo llamado exactamente Dockerfile (sin extensión) en la raíz del proyecto:

dockerfile
FROM node:lts-alpine
WORKDIR /app

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

COPY . .

ENV NODE_ENV=production
EXPOSE 3000
USER node
CMD ["node", "src/index.js"]

Qué hace cada instrucción:

  • FROM node:lts-alpine: la imagen base. Docker Hub publica la imagen oficial node con etiquetas por versión, la etiqueta lts (que sigue a la LTS más reciente) y variantes alpine (más pequeña) y slim. En producción, cambia lts por la versión mayor que probaste para que el build no cambie solo.
  • WORKDIR /app: el directorio de trabajo dentro de la imagen. Los comandos siguientes se ejecutan ahí.
  • COPY package.json package-lock.json ./: copia primero solo los archivos de dependencias.
  • RUN npm ci --omit=dev: instala las dependencias de producción exactamente como están en el lockfile.
  • COPY . .: copia el resto del código.
  • ENV y EXPOSE: definen una variable y documentan el puerto en el que escucha la aplicación. EXPOSE no publica el puerto; eso se hace en docker run.
  • USER node: la imagen oficial de Node ya incluye un usuario node. Ejecutar sin root limita el daño si la aplicación es comprometida.
  • CMD: el comando que se ejecuta cuando arranca el contenedor. Usa la forma de lista (exec), sin pasar por una shell.

¿Por qué copiar package.json antes que lo demás? Cada instrucción crea una capa, y Docker reutiliza de la caché las capas que no cambiaron. Si solo editaste un archivo .js, npm ci no se vuelve a ejecutar.

¿Cómo se ignoran archivos con .dockerignore?#

COPY . . copia todo lo que hay en la carpeta, incluso lo que no debería ir en la imagen. Crea un .dockerignore junto al Dockerfile:

node_modules
.git
.env
**/*.log
Dockerfile
.dockerignore

Así no se cuela tu node_modules local (que puede tener binarios incompatibles), el historial de Git y, sobre todo, los secretos del .env. El build es más rápido y la imagen, más pequeña.

¿Cómo se construye y se ejecuta el contenedor?#

Construye la imagen y ponle un nombre (etiqueta):

bash
docker build -t mi-app .

El punto final es el contexto del build: la carpeta que se envía a Docker. Después ejecútala:

bash
docker run --rm -p 3000:3000 --env-file .env mi-app
  • -p 3000:3000 conecta el puerto 3000 de tu equipo con el puerto 3000 del contenedor (host:contenedor).
  • --env-file .env entrega las variables al ejecutar, sin grabarlas en la imagen. Más en variables de entorno.
  • --rm elimina el contenedor al detenerse.

Añade -d para ejecutar en segundo plano, docker ps para listar contenedores y docker logs <id> para ver la salida.

¿Cómo saber si funcionó?#

  1. docker ps muestra el contenedor con estado Up.
  2. curl http://localhost:3000 (o el navegador) responde igual que la aplicación fuera de Docker.
  3. docker logs <id> muestra la salida de tu programa sin errores.
  4. docker images lista la imagen y su tamaño.

¿Cómo reducir la imagen con un build multi-stage?#

Si el proyecto necesita un paso de compilación (TypeScript, bundlers), las herramientas de build no tienen por qué ir en la imagen final. Un build multi-stage usa varios FROM y copia solo el resultado:

dockerfile
FROM node:lts-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:lts-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/index.js"]

La primera etapa instala todo y compila. La segunda parte de una base limpia, instala solo las dependencias de producción y recibe la carpeta dist con COPY --from=build. El compilador y las devDependencies se quedan fuera. El ejemplo supone que npm run build genera la carpeta dist con el punto de entrada en dist/index.js; ajusta las rutas a tu proyecto.

¿Cuáles son los errores comunes?#

El contenedor arranca y se detiene al instante. El proceso del CMD terminó. Ejecuta docker logs <id> y busca la causa, normalmente un error de código o una variable que falta.

La aplicación corre en el contenedor, pero el navegador no abre. Olvidaste el -p, o la aplicación escucha en 127.0.0.1. Dentro de un contenedor debe escuchar en 0.0.0.0; en Node, app.listen(3000, "0.0.0.0").

Cannot find module en el contenedor. Las dependencias no se instalaron en la imagen, o el .dockerignore excluyó un archivo necesario. Revisa las líneas COPY y RUN npm ci.

Build lento en cada cambio. Copiaste el código antes de instalar las dependencias. Mantén el orden: archivos de dependencias, instalación y después el código.

Los binarios nativos fallan en Alpine. Alpine usa musl en lugar de glibc, y algunos paquetes con código nativo no funcionan en él. Cambia la base a node:lts-slim.

Secretos en la imagen. Nunca uses COPY .env ni ENV TOKEN=... con valores reales. Quien tenga la imagen puede leerlos inspeccionando las capas.

Con la imagen lista, falta decidir dónde se ejecuta y cómo llegan las versiones nuevas. El flujo completo está en qué es el deploy. Cuando la aplicación necesite más de un servicio (base de datos, caché), el siguiente paso es describir todo en un archivo de Docker Compose.

Preguntas frecuentes

¿Necesito saber Linux para escribir un Dockerfile?

Lo básico ayuda, porque los comandos RUN se ejecutan dentro de un sistema Linux. Para la mayoría de las aplicaciones bastan cinco instrucciones (FROM, WORKDIR, COPY, RUN y CMD) y los mismos comandos que ya usas para instalar e iniciar el proyecto.

¿Cuál es la diferencia entre CMD y RUN?

RUN se ejecuta durante el build y su resultado queda guardado en la imagen (por ejemplo, instalar dependencias). CMD define el comando que corre cuando el contenedor arranca.

¿Puedo meter el archivo .env dentro de la imagen?

No es recomendable. Cualquiera que tenga la imagen puede leer los secretos. Excluye el .env con un .dockerignore y pasa las variables al ejecutar el contenedor, con --env-file o -e.

¿Por qué mi contenedor arranca y se detiene enseguida?

Un contenedor vive mientras su proceso principal (el del CMD) esté corriendo. Si ese proceso termina o falla, el contenedor se detiene. Revisa el motivo con docker logs.

Siguiente artículoVPS o hosting compartido: cuál elegirEl hosting compartido reparte un servidor entre muchos sitios y es simple. Un VPS te da root y recursos propios, pero lo administras tú. Así puedes elegir.

Más en Deploy

Ver los 33 artículos