Docker para desarrolladores: imágenes, contenedores y Compose

DevOps2026-10-12TryQuickToolBox

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.

ConceptoQué esComando típico
ImagenPlantilla inmutabledocker build, docker pull
ContenedorInstancia en ejecucióndocker run, docker ps
VolumenAlmacenamiento persistentedocker volume create
ComposeDefinición multicontenedordocker 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.0

La 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:

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:

  1. 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:.
  2. 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/src

Con 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

  1. Escribe un Dockerfile para tu aplicación y mantenlo mínimo.
  2. Añade un docker-compose.yml con la aplicación más sus dependencias.
  3. Usa bind mounts para el código fuente y volúmenes con nombre para los datos.
  4. Mantén los secretos fuera del archivo; usa un archivo .env y referencia variables con ${VAR}.
  5. Añade un .dockerignore que contenga node_modules, .git y la salida de compilación para acelerar las construcciones.
  6. Ejecuta docker compose up --build e 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

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.