Construyamos un entorno de desarrollo con Docker

Principiante | 90 min de lectura | 2025.12.02

Lo que aprenderas en este tutorial

  • Escritura de Dockerfile y significado de cada instruccion
  • Build de imagenes y ejecucion de contenedores
  • Gestion de multiples contenedores con docker-compose
  • Construccion de entorno de desarrollo Node.js + PostgreSQL

Requisitos previos: Docker Desktop instalado. Si docker --version muestra la version, estas listo.

Que es Docker? Por que surgio?

Historia de la tecnologia de contenedores

Las raices de la tecnologia de contenedores se remontan a chroot de UNIX V7 en 1979. Posteriormente, evoluciono de la siguiente manera:

AnoTecnologiaDescripcion
1979chrootAislamiento del sistema de archivos
2000FreeBSD JailAislamiento de procesos
2006cgroups (Google)Limitacion de recursos
2008LXC (Linux Containers)Contenedores ligeros para Linux
2013DockerEstandarizacion y popularizacion de contenedores

El nacimiento de Docker

En 2013, Solomon Hykes (dotCloud) presento Docker.

“Docker hizo posible empaquetar aplicaciones facilmente y ejecutarlas de la misma manera en cualquier lugar” — Docker oficial

Por que Docker fue revolucionario

  1. Compartir “entornos funcionales”: Resolvio el problema de “funciona en mi maquina”
  2. Ligero: A diferencia de VMs, comparte el SO, inicio en segundos
  3. Reproducibilidad de imagenes: Reproduccion completa del entorno mediante Dockerfile
  4. Ecosistema: Comparticion de imagenes a traves de Docker Hub

Diferencia entre VMs y contenedores

flowchart TB
    subgraph VM["Maquina Virtual (VM)"]
        direction TB
        AppA1["App A"] & AppB1["App B"]
        GuestOS1["Guest OS"] & GuestOS2["Guest OS"]
        Hypervisor["Hypervisor"]
        HostOS1["Host OS"]
        HW1["Hardware"]
    end

    subgraph Container["Contenedor"]
        direction TB
        AppA2["App A"] & AppB2["App B"]
        Runtime["Container Runtime<br/>(Docker)"]
        HostOS2["Host OS"]
        HW2["Hardware"]
    end
CaracteristicaVMContenedor
Tiempo de inicioMinutosSegundos
Uso de memoriaGBMB
Nivel de aislamientoCompletoA nivel de proceso
Independencia de SOCompletaKernel compartido

Documentacion oficial: Docker overview

Conceptos basicos de Docker

Imagenes y contenedores

  • Imagen (Image): El “plano” de la aplicacion. Solo lectura
  • Contenedor (Container): Una “instancia en ejecucion” creada a partir de una imagen
flowchart LR
    subgraph Static["Estatico"]
        Dockerfile["Dockerfile"]
        Image["node:20<br/>(imagen)"]
    end
    subgraph Dynamic["Dinamico"]
        Container["En ejecucion<br/>(contenedor)"]
        Stdout["stdout<br/>(logs)"]
    end
    Dockerfile -->|build| Image
    Image -->|run| Container
    Container -->|logs| Stdout

Estructura de capas de imagenes

Las imagenes Docker estan compuestas por capas, y solo se reconstruyen las partes modificadas:

FROM node:20-alpine    # Capa base (~100MB)
WORKDIR /app           # Capa de configuracion (~0KB)
COPY package*.json ./  # Capa de dependencias (~1KB)
RUN npm install        # Capa node_modules (~50MB)
COPY . .               # Capa de codigo de app (~10KB)

Mejor practica: Coloca los elementos que cambian menos arriba y los que cambian mas abajo para aprovechar el cache eficientemente

Step 1: Crear estructura del proyecto

Primero, creamos la estructura de directorios del proyecto.

mkdir docker-tutorial
cd docker-tutorial
mkdir src
touch Dockerfile docker-compose.yml src/index.js package.json

Step 2: Crear aplicacion Node.js

Creamos un servidor Express simple.

package.json

{
  "name": "docker-tutorial",
  "version": "1.0.0",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "nodemon": "^3.0.1"
  }
}

src/index.js

const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.get('/', (req, res) => {
    res.json({
        message: 'Hola desde Docker!',
        timestamp: new Date().toISOString()
    });
});

app.get('/health', (req, res) => {
    res.json({ status: 'healthy' });
});

app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});

Step 3: Crear Dockerfile

El Dockerfile es el plano para construir la imagen.

Dockerfile

# Especificar imagen base
FROM node:20-alpine

# Establecer directorio de trabajo
WORKDIR /app

# Copiar archivos de dependencias (optimizacion de cache)
COPY package*.json ./

# Instalar dependencias
RUN npm install

# Copiar codigo de aplicacion
COPY . .

# Exponer puerto (documentacion)
EXPOSE 3000

# Comando de inicio
CMD ["npm", "start"]

Explicacion detallada de instrucciones Dockerfile

InstruccionDescripcionEjemplo
FROMEspecifica imagen baseFROM node:20-alpine
WORKDIREstablece directorio de trabajoWORKDIR /app
COPYCopia archivosCOPY . .
RUNEjecuta comando durante buildRUN npm install
CMDComando por defecto al iniciar contenedorCMD ["npm", "start"]
ENTRYPOINTPunto de entrada del contenedorENTRYPOINT ["node"]
ENVEstablece variable de entornoENV NODE_ENV=production
EXPOSEExpone puerto (documentacion)EXPOSE 3000
ARGArgumento durante buildARG VERSION=1.0

Diferencia entre CMD y ENTRYPOINT

# CMD: Comando por defecto sobrescribible
CMD ["npm", "start"]
# docker run myapp npm run dev  ← se sobrescribe

# ENTRYPOINT: Comando que siempre se ejecuta
ENTRYPOINT ["node"]
CMD ["index.js"]
# docker run myapp app.js  ← se ejecuta como node app.js

Documentacion oficial: Dockerfile reference

Step 4: Build y ejecucion de imagen

# Build de imagen
docker build -t my-node-app .

# Verificar imagen construida
docker images

# Iniciar contenedor
docker run -p 3000:3000 my-node-app

# Iniciar en segundo plano
docker run -d -p 3000:3000 --name my-app my-node-app

# Acceder a http://localhost:3000 en el navegador

Opciones comunes de docker run

docker run \
  -d                      # Modo detach (segundo plano)
  -p 3000:3000            # Mapeo de puerto (host:contenedor)
  --name my-app           # Nombre del contenedor
  -e NODE_ENV=production  # Variable de entorno
  -v $(pwd):/app          # Montaje de volumen
  --rm                    # Eliminar automaticamente al terminar
  my-node-app             # Nombre de imagen

Step 5: Construccion de entorno con docker-compose

Gestionamos multiples servicios (app + base de datos) a la vez.

docker-compose.yml

version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgres://user:password@db:5432/mydb
    volumes:
      - ./src:/app/src  # Para hot reload
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

volumes:
  postgres_data:

Estructura de docker-compose.yml

version: '3.8'          # Version de especificacion Compose

services:               # Definicion de servicios (contenedores)
  service_name:
    image: xxx          # o build: ./path
    ports:              # Mapeo de puertos
    environment:        # Variables de entorno
    volumes:            # Volumenes
    depends_on:         # Dependencias
    restart:            # Politica de reinicio

volumes:                # Volumenes nombrados
networks:               # Redes personalizadas

Comandos docker-compose

# Iniciar todos los servicios
docker-compose up

# Iniciar en segundo plano
docker-compose up -d

# Reconstruir imagen e iniciar
docker-compose up --build

# Ver logs
docker-compose logs -f app

# Iniciar solo un servicio especifico
docker-compose up app

# Detener servicios
docker-compose down

# Eliminar incluyendo volumenes
docker-compose down -v

# Verificar estado de servicios
docker-compose ps

Hot reload: Al montar el codigo fuente con volumes, los cambios en archivos se reflejan inmediatamente en el contenedor.

Documentacion oficial: Docker Compose overview

Mejores practicas de Dockerfile

1. Build multi-etapa

Creamos imagenes ligeras para produccion:

# Etapa de build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Etapa de produccion
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]

2. Uso de .dockerignore

Excluir archivos innecesarios del contexto de build:

.dockerignore

node_modules
npm-debug.log
.git
.gitignore
.env
*.md
.DS_Store
coverage
.nyc_output

3. Ejecutar con usuario no-root

Por seguridad, evitar usuario root:

FROM node:20-alpine

# Crear usuario no-root
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001

WORKDIR /app
COPY --chown=nodejs:nodejs . .

# Cambiar a usuario no-root
USER nodejs

CMD ["node", "index.js"]

4. Agregar health check

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

5. Principios de 12 Factor App

The Twelve-Factor App son principios de diseno para aplicaciones cloud-native:

  • Configuracion: Gestionar configuracion con variables de entorno
  • Dependencias: Declarar explicitamente (package.json)
  • Puerto: Exponer servicios mediante binding de puertos
  • Procesos: Ejecutar como procesos sin estado
  • Logs: Emitir como stream a salida estandar

Comandos Docker de uso frecuente

Operaciones de contenedor

# Ver contenedores en ejecucion
docker ps

# Ver todos los contenedores (incluyendo detenidos)
docker ps -a

# Ejecutar comando dentro del contenedor
docker exec -it container_name sh

# Ver logs del contenedor
docker logs container_name
docker logs -f container_name  # Seguir

# Detener contenedor
docker stop container_name

# Eliminar contenedor
docker rm container_name

# Eliminar todos los contenedores detenidos
docker container prune

Operaciones de imagen

# Lista de imagenes
docker images

# Eliminar imagen
docker rmi image_name

# Eliminar imagenes no utilizadas
docker image prune

# Ver historial de imagen
docker history image_name

Limpieza

# Eliminar recursos innecesarios de una vez
docker system prune

# Eliminar incluyendo volumenes (cuidado!)
docker system prune -a --volumes

# Verificar uso de disco
docker system df

Solucion de problemas

El contenedor no inicia

# Verificar logs
docker logs container_name

# Iniciar en modo interactivo para debug
docker run -it my-node-app sh

Puerto en uso

# Verificar puertos en uso
lsof -i :3000

# Mapear a otro puerto
docker run -p 3001:3000 my-node-app

Build lento

  1. Verificar .dockerignore
  2. Optimizar orden de capas
  3. Usar build multi-etapa

Imagen muy grande

# Verificar tamano de imagen
docker images

# Usar imagen base ligera
FROM node:20-alpine  # ~100MB (version normal ~900MB)

# Eliminar archivos innecesarios
RUN npm ci --only=production && npm cache clean --force

Proximos pasos

Una vez dominados los fundamentos de Docker, aprende orquestacion:

Enlaces de referencia

Documentacion oficial

Mejores practicas

Herramientas y recursos

  • Docker Desktop - Herramienta de gestion GUI
  • Dive - Herramienta de analisis de capas de imagenes Docker
  • Hadolint - Herramienta de analisis estatico de Dockerfile

Cheat sheets

← Volver a la lista