Docker para desarrolladores: imágenes, contenedores y Compose
Clonaste un proyecto, ejecutaste las instrucciones de instalación y te topaste con un muro: una librería del sistema que falta, una base de datos que no arranca o una versión de Node incompatible. Docker existe para eliminar ese tipo de problemas. En lugar de instalar servicios directamente en tu máquina, los describes una vez y los ejecutas en contenedores aislados que se comportan igual en todas partes.
Esta guía explica las tres ideas que realmente necesitas: imágenes, contenedores y Compose. Se mantiene práctica, con comandos que puedes ejecutar mientras lees.
Imágenes vs contenedores: el modelo mental
Una imagen es una plantilla de solo lectura. Contiene una instantánea del sistema de archivos más metadatos como el comando por defecto y los puertos expuestos. Un contenedor es una instancia en ejecución (o detenida) de esa imagen con una fina capa de escritura encima.
Si vienes de la programación orientada a objetos, la analogía es cercana: una imagen es una clase, un contenedor es un objeto. Puedes iniciar muchos contenedores desde una sola imagen, y los cambios dentro de un contenedor no modifican la imagen.
| Concepto | Qué es | Comando típico |
|---|---|---|
| Imagen | Plantilla inmutable | docker build, docker pull |
| Contenedor | Instancia en ejecución | docker run, docker ps |
| Volumen | Almacenamiento persistente | docker volume create |
| Compose | Definición multicontenedor | docker compose up |
Tu primera imagen con un Dockerfile
Un Dockerfile es una receta. Cada instrucción crea una capa, y Docker almacena en caché las capas para que las reconstrucciones sean rápidas cuando nada ha cambiado. Aquí tienes un pequeño ejemplo de Node.js:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]Dos hábitos importan aquí. Primero, copia los manifiestos de dependencias antes que el resto del código fuente para que la capa de npm ci se almacene en caché cuando solo cambia el código de la aplicación. Segundo, prefiere una imagen base ligera como alpine para reducir el tamaño y la superficie de ataque.
Constrúyela y ejecútala:
docker build -t myapp:1.0 .
docker run --rm -p 3000:3000 myapp:1.0La opción -p 3000:3000 mapea el puerto 3000 del host al puerto 3000 del contenedor. --rm elimina el contenedor cuando termina, lo que mantiene tu máquina ordenada durante el desarrollo.
Comandos habituales de contenedores
Estos cubren la mayor parte del trabajo diario:
docker ps— lista los contenedores en ejecución; añade-apara incluir los detenidos.docker logs -f <container>— transmite la salida de la aplicación.docker exec -it <container> sh— abre una shell dentro de un contenedor en ejecución.docker stop <container>— apagado ordenado.docker images— lista las imágenes locales y sus tamaños.docker system prune— elimina contenedores, redes e imágenes colgantes no usados.
Cuando un contenedor termina inmediatamente, lee primero los logs. Las causas más comunes son un CMD incorrecto, una variable de entorno que falta o el proceso enlazando a 127.0.0.1 en lugar de 0.0.0.0 dentro del contenedor.
Por qué existe Compose
Una aplicación real normalmente necesita más de un proceso: una API, una base de datos, quizá una caché. Ejecutar tres comandos docker run con largas listas de opciones es propenso a errores. Docker Compose te permite declarar todos los servicios en un archivo YAML y arrancarlos juntos.
services:
api:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:secret@db:5432/app
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:Fíjate en que la API se conecta a db, no a localhost. Compose crea una red privada donde cada servicio es accesible por su nombre de servicio. Esta es la fuente de confusión más común para los recién llegados.
Arranca todo con docker compose up, o docker compose up -d para ejecutarlo en segundo plano. Reconstruye tras cambios en el código con docker compose up --build. Desmonta con docker compose down, y añade -v si también quieres eliminar los volúmenes con nombre.
Volúmenes, bind mounts y recarga en vivo
Los contenedores son efímeros; los datos dentro de ellos desaparecen al eliminarlos. Dos mecanismos resuelven esto:
- Volúmenes con nombre — gestionados por Docker, ideales para archivos de bases de datos. Persisten entre reinicios de contenedores y se declaran en la sección
volumes:. - Bind mounts — mapean un directorio del host dentro del contenedor, ideales para el código fuente durante el desarrollo.
services:
api:
build: .
volumes:
- ./src:/app/srcCon un bind mount, editar archivos en tu host se refleja dentro del contenedor. Combínalo con un observador de archivos (nodemon, uvicorn --reload, air) y obtendrás recarga en vivo sin reconstruir la imagen.
Un flujo de trabajo práctico de desarrollo
- Escribe un Dockerfile para tu aplicación y mantenlo mínimo.
- Añade un
docker-compose.ymlcon la aplicación más sus dependencias. - Usa bind mounts para el código fuente y volúmenes con nombre para los datos.
- Mantén los secretos fuera del archivo; usa un archivo
.envy referencia variables con${VAR}. - Añade un
.dockerignoreque contenganode_modules,.gity la salida de compilación para acelerar las construcciones. - Ejecuta
docker compose up --builde itera.
Si tu servicio produce logs que necesitas inspeccionar más tarde, recuerda que los logs de los contenedores son solo texto. Las herramientas que analizan logs de servidor, como un analizador de logs de nginx, se combinan bien con los contenedores porque puedes canalizar o copiar los logs fuera sin instalar nada en el host.
Errores comunes
- Imágenes pesadas: usa compilaciones multi-etapa para compilar en una etapa y copiar solo los artefactos a una imagen de ejecución ligera.
- Reconstruir todo: ordena las instrucciones del Dockerfile de la menos a la más cambiante.
- Hosts codificados: dentro de Compose, usa nombres de servicio; fuera, usa variables de entorno.
- Ignorar la limpieza: las imágenes colgantes y los contenedores detenidos se acumulan. Ejecuta
docker system pruneperiódicamente. - Ejecutar como root: añade un
USERno root en las imágenes de producción.
Preguntas frecuentes
¿Cuál es la diferencia entre una imagen y un contenedor?
Una imagen es una plantilla inmutable que contiene un sistema de archivos y metadatos. Un contenedor es una instancia en ejecución de esa imagen con una capa de escritura añadida. Puedes iniciar muchos contenedores desde una sola imagen.
¿Necesito Docker Compose para un solo servicio?
No. Un solo comando docker run es suficiente para un contenedor. Compose resulta valioso cuando tienes múltiples servicios, redes compartidas o variables de entorno que quieres declarar en un solo lugar.
¿Cómo se comunican los contenedores entre sí?
Compose coloca todos los servicios en una red compartida donde cada servicio es accesible por su nombre de servicio. Usa ese nombre como hostname, por ejemplo db:5432, en lugar de localhost.
Docker recompensa una pequeña inversión inicial. Una vez que las imágenes, los contenedores y Compose encajan, incorporar a un compañero de equipo o cambiar de máquina deja de ser un ritual de configuración y se convierte en un solo comando.