Empaqueta una vez, ejecuta en cualquier parte
Pocas frases han perseguido tanto a los equipos de desarrollo como “en mi máquina funciona”. Una aplicación que corre perfecta en la laptop del programador falla en el servidor porque allí hay otra versión del lenguaje, falta una librería del sistema o una variable de entorno está mal configurada. Durante años, la solución fue documentar a mano cada paso de instalación y rezar para que nadie se saltara ninguno.
Docker cambió ese panorama al permitir empaquetar una aplicación junto con todo lo que necesita para funcionar: el sistema base, el runtime, las dependencias y la configuración. El resultado es una imagen que se comporta igual en tu equipo, en el servidor de integración continua y en producción. El flujo de trabajo se reduce a tres verbos: construir, ejecutar y listo.
En esta guía vas a recorrer ese ciclo completo con herramientas actuales. Instalarás Docker Engine 29 en Ubuntu 26.04 LTS, escribirás un Dockerfile para una API en Node.js, aprenderás a crear una imagen de Docker optimizada, ejecutarás contenedores, los administrarás desde la línea de comandos y llevarás todo a un servidor en la nube. Al final encontrarás errores frecuentes con su solución, buenas prácticas de seguridad y una hoja de referencia rápida.
Contenedores frente a máquinas virtuales
Antes de escribir comandos conviene entender qué problema resuelve Docker y en qué se diferencia de la virtualización tradicional. Comparar contenedores frente a máquinas virtuales aclara por qué los contenedores se volvieron el estándar para desplegar software moderno.
Una máquina virtual emula un equipo completo. Un hipervisor reparte el hardware físico entre varias VM, y cada una arranca su propio sistema operativo con su kernel, sus servicios y su consumo fijo de memoria y disco. Si ejecutas cuatro aplicaciones en cuatro VM, tienes cuatro sistemas operativos completos funcionando a la vez.
Un contenedor, en cambio, comparte el kernel del sistema anfitrión. Usa funciones del kernel de Linux, como los namespaces y los cgroups, para aislar procesos, red y sistema de archivos, y para limitar cuánta CPU y memoria puede consumir cada uno. No hay un segundo sistema operativo arrancando: solo los procesos de tu aplicación y sus dependencias.
Las consecuencias prácticas son claras:
- Arranque: un contenedor inicia en segundos o menos; una VM suele tardar minutos.
- Tamaño: una imagen de contenedor ocupa decenas o cientos de megabytes; una VM, varios gigabytes.
- Densidad: en el mismo servidor caben muchos más contenedores que máquinas virtuales.
- Aislamiento: la VM ofrece un aislamiento más fuerte, porque no comparte kernel, lo que la hace preferible para ejecutar cargas de terceros no confiables.
Ambas tecnologías no compiten tanto como se complementan. De hecho, lo habitual en la nube es ejecutar contenedores dentro de máquinas virtuales, que es justo lo que harás en la sección de despliegue.
Conceptos clave: imagen, contenedor, registro y Dockerfile
Docker introduce un vocabulario propio que conviene tener claro desde el principio:
- Dockerfile: archivo de texto con las instrucciones para construir una imagen. Es la receta.
- Imagen: plantilla de solo lectura generada a partir del Dockerfile. Contiene el sistema de archivos y los metadatos necesarios para ejecutar la aplicación.
- Contenedor: instancia en ejecución de una imagen. De una misma imagen puedes lanzar tantos contenedores como necesites.
- Registro: servidor donde se almacenan y distribuyen imágenes. Docker Hub es el público por defecto; también existen GitHub Container Registry, Amazon ECR o registros privados.
- Capa: cada instrucción del Dockerfile genera una capa del sistema de archivos. Docker reutiliza las capas que no cambian, lo que acelera las construcciones.
Una analogía útil viene de la programación orientada a objetos: el Dockerfile es el código fuente de una clase, la imagen es la clase compilada y el contenedor es un objeto creado a partir de ella.
Dockerfile --(docker build)--> Imagen --(docker run)--> Contenedor
|
(docker push / pull)
|
Registro
Instalar Docker Engine en Ubuntu 26.04 LTS
Muchos tutoriales antiguos instalan un paquete llamado docker-engine o el paquete docker.io de los repositorios de la distribución. El primero lleva años obsoleto y el segundo suele ir varias versiones por detrás. La forma recomendada es usar el repositorio oficial de Docker, que hoy distribuye Docker Engine 29.
Primero, prepara las claves de firma:
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
Después, añade el repositorio y instala los paquetes, que incluyen el motor, el cliente, containerd y los complementos de Buildx y Compose:
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
Para usar Docker sin anteponer sudo a cada comando, añade tu usuario al grupo docker y vuelve a iniciar sesión. Ten presente que pertenecer a ese grupo equivale en la práctica a tener privilegios de administrador en la máquina:
sudo usermod -aG docker $USER
newgrp docker
Comprueba la instalación:
docker --version
docker compose version
docker run --rm hello-world
Docker version 29.9.2, build 6d1f2a7
Docker Compose version v5.6.0
Hello from Docker!
This message shows that your installation appears to be working correctly.
En macOS y Windows la vía habitual es Docker Desktop, que incluye una máquina virtual ligera con Linux donde corre el motor. Los comandos de esta guía funcionan igual en ambos casos.
La aplicación de ejemplo: una API de recordatorios
Para practicar usarás una API mínima en Node.js con Express 5 que gestiona recordatorios en memoria. La estructura del proyecto es la siguiente:
recordatorios-api/
├── src/
│ └── servidor.js
├── package.json
├── package-lock.json
├── .dockerignore
├── Dockerfile
├── compose.yaml
├── construir.sh
└── ejecutar.sh
El archivo package.json define la dependencia y el script de arranque:
{
"name": "recordatorios-api",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node src/servidor.js"
},
"dependencies": {
"express": "^5.1.0"
}
}
Y src/servidor.js expone tres rutas, incluida una de salud que más adelante usarás para comprobar que el contenedor responde:
import express from "express";
const app = express();
app.use(express.json());
const recordatorios = [];
const PUERTO = Number(process.env.PUERTO ?? 3000);
const HOST = process.env.HOST ?? "0.0.0.0";
app.get("/salud", (_req, res) => res.json({ estado: "ok" }));
app.get("/recordatorios", (_req, res) => res.json(recordatorios));
app.post("/recordatorios", (req, res) => {
const nuevo = { id: recordatorios.length + 1, texto: req.body.texto };
recordatorios.push(nuevo);
res.status(201).json(nuevo);
});
app.listen(PUERTO, HOST, () => {
console.log(`API escuchando en http://${HOST}:${PUERTO}`);
});
Fíjate en el detalle del host. La aplicación escucha en 0.0.0.0, es decir, en todas las interfaces de red. Si escuchara solo en localhost, dentro del contenedor ese nombre se refiere al propio contenedor, y las peticiones que llegan desde fuera nunca la alcanzarían. Es uno de los errores más frecuentes al contenerizar servicios web.
Escribir el Dockerfile paso a paso
En lugar de partir de una imagen de Ubuntu e instalar Node.js a mano, la práctica actual es partir directamente de la imagen oficial de Node. Ahorra pasos, reduce el tamaño y recibe parches de seguridad de forma regular. Para producción se recomienda una versión LTS; Node.js 24 está en mantenimiento LTS y Node.js 26 se convierte en la nueva LTS a finales de octubre de 2026, así que ambas son opciones válidas.
# syntax=docker/dockerfile:1
FROM node:24-slim
ENV NODE_ENV=production
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY src ./src
USER node
EXPOSE 3000
CMD ["node", "src/servidor.js"]
Cada instrucción cumple una función concreta:
FROM node:24-slimdefine la imagen base: Debian reducido con Node.js 24 ya instalado.ENV NODE_ENV=productionactiva optimizaciones de Express y evita comportamientos de desarrollo.WORKDIR /appcrea el directorio de trabajo y lo usa como referencia para las instrucciones siguientes.- El primer
COPYtrae solo los archivos de dependencias. Así, mientras no cambien, Docker reutiliza la capa denpm ciaunque modifiques el código. RUN npm ci --omit=devinstala exactamente las versiones del lockfile, sin dependencias de desarrollo.COPY src ./srccopia el código de la aplicación.USER nodeejecuta el proceso con un usuario sin privilegios que la imagen oficial ya incluye.EXPOSE 3000documenta el puerto en el que escucha la aplicación. No lo publica por sí solo; eso se hace al ejecutar el contenedor.CMDdefine el comando de arranque en forma exec, con un arreglo JSON, para que Node reciba directamente las señales de parada.
Usar node directamente en CMD en lugar de npm start tiene una ventaja: cuando Docker envía la señal de detención, la recibe el proceso de Node y no un proceso intermedio, lo que permite un apagado ordenado.
Excluir archivos con .dockerignore
Al construir, Docker envía al motor todo el contenido del directorio indicado como contexto. Si ahí está la carpeta node_modules de tu equipo, archivos de entorno o el historial de Git, la construcción será más lenta y podrías filtrar secretos dentro de la imagen. El archivo .dockerignore funciona como .gitignore:
node_modules
npm-debug.log
.git
.gitignore
.env
.env.*
Dockerfile
compose.yaml
*.md
Excluir node_modules es especialmente importante: las dependencias deben instalarse dentro de la imagen, compiladas para su sistema operativo y arquitectura, no copiarse desde tu máquina.
Construir la imagen
Con el Dockerfile listo, la construcción es un único comando ejecutado desde la raíz del proyecto:
docker build -t recordatorios-api:1.0 .
La opción -t asigna un nombre y una etiqueta a la imagen con el formato nombre:etiqueta, siempre en minúsculas y sin espacios. Si omites la etiqueta, Docker usa latest. El punto final indica el contexto de construcción, es decir, el directorio cuyo contenido estará disponible para las instrucciones COPY.
Desde hace varias versiones, Docker utiliza BuildKit como motor de construcción por defecto. Muestra un progreso más claro, ejecuta pasos en paralelo cuando puede y gestiona la caché de forma más inteligente:
[+] Building 14.2s (10/10) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> [internal] load metadata for docker.io/library/node:24-slim 1.1s
=> [internal] load .dockerignore 0.0s
=> [1/5] FROM docker.io/library/node:24-slim 6.3s
=> [2/5] WORKDIR /app 0.1s
=> [3/5] COPY package.json package-lock.json ./ 0.0s
=> [4/5] RUN npm ci --omit=dev 5.8s
=> [5/5] COPY src ./src 0.0s
=> exporting to image 0.6s
=> => naming to docker.io/library/recordatorios-api:1.0 0.0s
Para listar y eliminar imágenes usa los subcomandos de docker image, aunque los atajos clásicos siguen funcionando:
docker image ls
docker image rm recordatorios-api:1.0
# Equivalentes abreviados
docker images
docker rmi recordatorios-api:1.0
Si vuelves a construir sin cambiar package.json, verás que el paso de npm ci aparece como CACHED y la construcción tarda apenas un segundo. Ese es el efecto de ordenar bien las instrucciones.
Ejecutar y administrar contenedores
Ahora toca lanzar un contenedor a partir de la imagen:
docker run -d --name recordatorios -p 8080:3000 recordatorios-api:1.0
Las opciones más importantes son:
-dejecuta el contenedor en modo desacoplado y devuelve el control de la terminal.--name recordatoriosasigna un nombre legible para referirte al contenedor sin usar su identificador.-p 8080:3000publica el puerto 3000 del contenedor en el puerto 8080 del anfitrión.
Saber mapear puertos entre host y contenedor es clave: el orden es siempre anfitrion:contenedor. Con el mapeo anterior, la API responde en el puerto 8080 de tu máquina:
curl http://localhost:8080/salud
curl -X POST http://localhost:8080/recordatorios \
-H "Content-Type: application/json" \
-d '{"texto": "Renovar el certificado TLS"}'
{"estado":"ok"}
{"id":1,"texto":"Renovar el certificado TLS"}
La opción de ejecutar contenedores en segundo plano es la habitual en servidores, pero mientras desarrollas puede ser útil omitir -d para ver los logs directamente en la terminal. También puedes añadir --rm para que el contenedor se elimine automáticamente al detenerse, lo que evita acumular contenedores de prueba.
Para administrar el ciclo de vida, estos son los comandos básicos:
docker ps # contenedores en ejecución
docker ps -a # todos, incluidos los detenidos
docker stop recordatorios # detiene con una señal ordenada
docker start recordatorios # vuelve a arrancar uno detenido
docker restart recordatorios
docker rm recordatorios # elimina un contenedor detenido
docker rm -f recordatorios # lo detiene y elimina en un paso
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES
3fa91c2d8b7e recordatorios-api:1.0 "docker-entrypoint.s…" Up 2 minutes 0.0.0.0:8080->3000/tcp recordatorios
La configuración que depende del entorno se pasa con variables, sin reconstruir la imagen:
docker run -d --name recordatorios -p 8080:4000 \
-e PUERTO=4000 \
--restart unless-stopped \
--memory 256m --cpus 0.5 \
recordatorios-api:1.0
La política --restart unless-stopped reinicia el contenedor si falla o si el servidor se reinicia, y las opciones --memory y --cpus limitan los recursos que puede consumir.
Inspeccionar, ver logs y entrar en un contenedor
Cuando algo no funciona, estas herramientas te dan visibilidad sobre lo que ocurre dentro del contenedor:
docker logs recordatorios # salida estándar acumulada
docker logs -f --tail 50 recordatorios # seguir los últimos 50 en vivo
docker stats recordatorios # consumo de CPU y memoria en tiempo real
docker inspect recordatorios # configuración completa en JSON
Para abrir una terminal dentro de un contenedor en ejecución usa docker exec. Las imágenes slim incluyen bash, pero las basadas en Alpine solo traen sh:
docker exec -it recordatorios bash
node@3fa91c2d8b7e:/app$ ls
node_modules package-lock.json package.json src
node@3fa91c2d8b7e:/app$ whoami
node
Recuerda que los cambios que hagas a mano dentro de un contenedor se pierden al eliminarlo. Úsalo para diagnosticar, no para configurar; cualquier ajuste permanente debe ir al Dockerfile.
Automatizar con scripts y con Docker Compose
Escribir comandos largos una y otra vez invita a errores. Una primera solución sencilla son dos scripts de shell en la raíz del proyecto:
#!/usr/bin/env bash
# construir.sh
set -euo pipefail
docker build -t recordatorios-api:1.0 .
#!/usr/bin/env bash
# ejecutar.sh
set -euo pipefail
docker rm -f recordatorios 2>/dev/null || true
docker run -d --name recordatorios -p 8080:3000 \
--restart unless-stopped recordatorios-api:1.0
Con ellos, el ciclo completo se reduce a bash construir.sh && bash ejecutar.sh. Sin embargo, la herramienta pensada para esto es Docker Compose, que describe uno o varios servicios en un archivo YAML. Su versión actual es la 5.6 y se invoca como docker compose, con espacio; el antiguo binario docker-compose con guion pertenece a la primera generación, ya retirada.
services:
api:
build: .
image: recordatorios-api:1.0
container_name: recordatorios
ports:
- "8080:3000"
environment:
PUERTO: "3000"
restart: unless-stopped
healthcheck:
test:
[
"CMD",
"node",
"-e",
"fetch('http://localhost:3000/salud').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))",
]
interval: 30s
timeout: 5s
retries: 3
Los comandos de Compose cubren todo el ciclo:
docker compose up -d --build # construye y arranca
docker compose ps # estado de los servicios
docker compose logs -f api # logs del servicio
docker compose down # detiene y elimina contenedores y red
La propiedad version que aparecía al inicio de los archivos de Compose antiguos ya no es necesaria; las versiones actuales la ignoran y muestran una advertencia si la incluyes. Compose brilla de verdad cuando añades más servicios, como una base de datos o una caché, porque los conecta en una red privada donde se encuentran por nombre.
Llevar el contenedor a un servidor en la nube
La gran promesa de Docker es que lo que funciona en tu equipo funciona igual en el servidor. Para comprobarlo puedes usar una instancia de Amazon EC2, un droplet de DigitalOcean o cualquier máquina virtual con Linux. El flujo más limpio no es copiar el código y construir allí, sino publicar la imagen en un registro y descargarla en el servidor.
Primero, etiqueta y sube la imagen a un registro. Este ejemplo usa GitHub Container Registry:
echo "$GITHUB_TOKEN" | docker login ghcr.io -u tu-usuario --password-stdin
docker tag recordatorios-api:1.0 ghcr.io/tu-usuario/recordatorios-api:1.0
docker push ghcr.io/tu-usuario/recordatorios-api:1.0
Hay un detalle importante si desarrollas en un Mac con Apple Silicon y despliegas en servidores x86. Las imágenes se construyen por defecto para la arquitectura de tu máquina. Con Buildx puedes generar una imagen multiplataforma en un solo paso:
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t ghcr.io/tu-usuario/recordatorios-api:1.0 \
--push .
En el servidor, después de instalar Docker Engine como viste antes, descarga y ejecuta la imagen. Publicar en el puerto 80 permite acceder sin indicar puerto en la URL:
docker pull ghcr.io/tu-usuario/recordatorios-api:1.0
docker run -d --name recordatorios -p 80:3000 \
--restart unless-stopped \
ghcr.io/tu-usuario/recordatorios-api:1.0
No olvides abrir el puerto en el firewall del proveedor; en EC2 se configura en el grupo de seguridad de la instancia. Para un servicio real, coloca delante un proxy inverso como Caddy, Nginx o Traefik que gestione HTTPS, y deja el puerto de la aplicación cerrado al exterior.
Optimizar la imagen: capas, caché y builds multi-stage
Una imagen pequeña se descarga más rápido, arranca antes y expone menos superficie de ataque. Las técnicas principales son tres.
La primera es ordenar las instrucciones de la menos a la más cambiante, como ya hiciste al copiar package.json antes que el código. Cada vez que una capa cambia, Docker reconstruye esa capa y todas las posteriores.
La segunda es combinar comandos relacionados en un único RUN y limpiar en el mismo paso los archivos temporales, porque lo que se borra en una capa posterior sigue ocupando espacio en la anterior:
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
La tercera, y la más potente, es la construcción multi-stage de imágenes, en la que usas una etapa con todas las herramientas de compilación y copias a la etapa final solo el resultado. Es imprescindible cuando la aplicación requiere un paso de build, como una API escrita en TypeScript:
# syntax=docker/dockerfile:1
FROM node:24-slim AS compilacion
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build && npm prune --omit=dev
FROM node:24-slim AS ejecucion
ENV NODE_ENV=production
WORKDIR /app
COPY --from=compilacion --chown=node:node /app/node_modules ./node_modules
COPY --from=compilacion --chown=node:node /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/servidor.js"]
La instrucción RUN --mount=type=cache reutiliza la caché de npm entre construcciones sin guardarla dentro de la imagen. Puedes comparar tamaños con docker image ls:
REPOSITORY TAG IMAGE ID CREATED SIZE
recordatorios-api multi a41c0e9b72d3 10 seconds ago 231MB
recordatorios-api 1.0 7be2f1d0c9a8 12 minutes ago 236MB
recordatorios-api ubuntu e03f5b1a8c44 3 days ago 812MB
La última fila representa el enfoque antiguo de partir de Ubuntu e instalar Node.js a mano: más de tres veces el tamaño, con muchos paquetes que la aplicación nunca usa.
Seguridad en contenedores
Los contenedores aíslan, pero no son una barrera mágica. Estas prácticas reducen riesgos de forma notable:
- Ejecuta como usuario sin privilegios con
USER. Un proceso root dentro del contenedor tiene más opciones de escapar si existe una vulnerabilidad en el kernel. - Usa imágenes base oficiales y fija versiones concretas, como
node:24-slim, en lugar delatest, para que las construcciones sean reproducibles. - Nunca copies secretos en la imagen. Cualquier persona con acceso a la imagen puede extraer sus capas. Pasa credenciales en tiempo de ejecución con variables de entorno o gestores de secretos, y en el build usa
--secretde BuildKit. - Escanea las imágenes en busca de vulnerabilidades conocidas con herramientas como Docker Scout o Trivy, idealmente en tu pipeline de integración continua.
- Reconstruye periódicamente para incorporar los parches de la imagen base, aunque tu código no haya cambiado.
- Limita capacidades y recursos. Opciones como
--read-only,--cap-drop ALLy los límites de memoria reducen el impacto de un compromiso.
docker run -d --name recordatorios -p 8080:3000 \
--read-only --tmpfs /tmp \
--cap-drop ALL \
--security-opt no-new-privileges \
recordatorios-api:1.0
docker scout cves recordatorios-api:1.0
Errores comunes al empezar con Docker
Estos tropiezos aparecen una y otra vez en los primeros proyectos. Identificarlos ahorra horas de depuración.
Confundir EXPOSE con publicar un puerto. EXPOSE es documentación; si no pasas -p al ejecutar, el servicio no será accesible desde fuera del contenedor.
Escuchar en localhost dentro del contenedor. La aplicación debe enlazarse a 0.0.0.0; de lo contrario, el puerto publicado no recibirá tráfico.
Copiar node_modules desde el equipo local. Las dependencias con módulos nativos compilados para macOS o Windows fallan en Linux. Exclúyelas con .dockerignore e instálalas en la imagen.
Usar la etiqueta latest en producción. No sabes qué versión exacta se está ejecutando y un pull puede cambiarla sin aviso. Etiqueta cada versión de forma explícita, por ejemplo con el número de versión o el hash del commit.
Guardar datos importantes dentro del contenedor. Su sistema de archivos es efímero. Para datos persistentes usa volúmenes:
docker volume create datos-recordatorios
docker run -d --name recordatorios -p 8080:3000 \
-v datos-recordatorios:/app/datos \
recordatorios-api:1.0
Usar la forma shell en CMD, como CMD npm start. El proceso real queda detrás de un intérprete y no recibe las señales de parada, así que Docker termina matándolo tras el tiempo de espera, sin apagado ordenado.
Solución de problemas: mensajes de error frecuentes
Cuando algo falla, el mensaje suele señalar la causa. Estos son los más habituales.
Sin permisos para hablar con el demonio:
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock
Tu usuario no pertenece al grupo docker. Ejecuta sudo usermod -aG docker $USER y abre una nueva sesión, o antepone sudo al comando.
El demonio no está en marcha:
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
Inícialo y actívalo para que arranque con el sistema:
sudo systemctl enable --now docker
El puerto ya está ocupado:
docker: Error response from daemon: driver failed programming external connectivity on endpoint recordatorios: Bind for 0.0.0.0:8080 failed: port is already allocated.
Otro contenedor o proceso usa ese puerto. Localízalo con docker ps o sudo ss -ltnp | grep 8080 y detenlo, o publica en otro puerto del anfitrión.
El nombre del contenedor ya existe:
docker: Error response from daemon: Conflict. The container name "/recordatorios" is already in use by container "3fa91c2d8b7e".
Elimina el contenedor anterior con docker rm -f recordatorios antes de crear uno nuevo, o usa otro nombre.
Arquitectura incompatible:
exec /usr/local/bin/docker-entrypoint.sh: exec format error
La imagen se construyó para una arquitectura distinta a la del servidor, típicamente arm64 frente a amd64. Reconstruye con --platform linux/amd64 o genera una imagen multiplataforma con Buildx.
Archivo ausente en el contexto de construcción:
ERROR: failed to solve: failed to compute cache key: "/package-lock.json": not found
El archivo no existe o está excluido en .dockerignore. Genera el lockfile con npm install y revisa las reglas de exclusión.
Disco lleno tras muchas construcciones:
no space left on device
Docker acumula imágenes, capas y caché sin usar. Revisa el consumo y limpia lo que sobra:
docker system df
docker system prune --volumes
docker builder prune
Docker frente a otras alternativas
Docker no es la única forma de ejecutar contenedores ni siempre la mejor opción. Conocer las alternativas ayuda a decidir con criterio.
Podman es compatible con la mayoría de comandos de Docker y con los Dockerfile, pero funciona sin demonio y está pensado para ejecutar contenedores sin privilegios de root desde el inicio. Es la opción predeterminada en distribuciones como Fedora y RHEL, y suele bastar con usar podman donde escribirías docker.
containerd y nerdctl ofrecen el runtime que Docker usa por debajo, sin la capa adicional. Kubernetes trabaja directamente con containerd, así que en clústeres no necesitas Docker Engine en los nodos, aunque las imágenes que construyes con Docker funcionan sin cambios.
Las máquinas virtuales siguen siendo la elección correcta cuando necesitas un aislamiento fuerte, otro sistema operativo o ejecutar código de terceros no confiable.
Las plataformas como servicio, por ejemplo Render, Fly.io, Railway o Google Cloud Run, aceptan una imagen de contenedor y se encargan de servidores, escalado y certificados. Son ideales cuando no quieres administrar infraestructura, y tu Dockerfile sigue siendo el mismo.
Para orquestar muchos contenedores en varios servidores, con autoescalado y despliegues sin interrupción, el estándar es Kubernetes. Docker Compose cubre muy bien entornos de desarrollo y despliegues en un solo servidor; cuando el sistema crece, el salto natural es un orquestador.
Casos de uso reales
Docker aparece en casi todas las etapas del ciclo de vida del software. En desarrollo, permite que un equipo levante con un solo docker compose up la API, la base de datos, la caché y una cola de mensajes, con las mismas versiones para todos y sin instalar nada en el sistema.
En integración continua, cada ejecución del pipeline construye la imagen, ejecuta las pruebas dentro de un contenedor limpio y publica el artefacto en un registro. Lo que se prueba es exactamente lo que se despliega.
En arquitecturas de microservicios, cada servicio vive en su propia imagen con su propio lenguaje y dependencias. Un servicio en Node.js, otro en Python y otro en Go conviven sin conflictos en el mismo servidor.
En entornos de datos y aprendizaje automático, los contenedores fijan versiones de librerías científicas y controladores, lo que hace reproducibles experimentos que de otro modo dependerían de la configuración de cada máquina.
También es habitual empaquetar herramientas de línea de comandos en imágenes para ejecutarlas sin instalarlas, por ejemplo un formateador de código o un cliente de base de datos de una versión concreta.
Hoja de referencia rápida
Estos son los comandos que usarás a diario, agrupados por tarea:
# Imágenes
docker build -t nombre:etiqueta .
docker image ls
docker image rm nombre:etiqueta
docker pull imagen:etiqueta
docker push registro/usuario/imagen:etiqueta
# Contenedores
docker run -d --name app -p 8080:3000 nombre:etiqueta
docker ps -a
docker stop app && docker rm app
docker logs -f app
docker exec -it app sh
# Compose
docker compose up -d --build
docker compose down
# Limpieza y diagnóstico
docker system df
docker system prune
Conclusiones
Docker resuelve un problema que acompañó durante décadas al desarrollo de software: la diferencia entre el entorno donde se escribe el código y el entorno donde se ejecuta. Al empaquetar la aplicación con sus dependencias en una imagen, el ciclo se reduce a construir, ejecutar y listo, con el mismo resultado en cualquier máquina.
En esta guía instalaste Docker Engine 29 desde el repositorio oficial, escribiste un Dockerfile moderno basado en la imagen oficial de Node.js, construiste y ejecutaste contenedores, publicaste puertos, pasaste configuración con variables de entorno y automatizaste el proceso con scripts y con Docker Compose. También llevaste la imagen a un registro y a un servidor en la nube, aprendiste a reducir su tamaño con builds multi-stage y aplicaste prácticas de seguridad como ejecutar sin privilegios y no incluir secretos.
El siguiente paso natural es incorporar Docker a tu pipeline de integración continua, añadir a Compose los servicios que tu aplicación necesita y, cuando el sistema crezca a varios servidores, explorar un orquestador como Kubernetes. Las imágenes que construyas hoy te servirán sin cambios en cada una de esas etapas.
