Qué es Docker y cómo contenerizar tu primera aplicación

D
DanisCh
(Actualizado: ) 13 min de lectura
Qué es Docker y cómo contenerizar tu primera aplicación
DevOps Docker

¿Cuántas veces has escuchado "en mi máquina funciona"? Es la frase más famosa del desarrollo de software, y describe un problema real: una aplicación puede funcionar perfectamente en el ordenador del desarrollador y fallar en producción porque la versión de Python es diferente, falta una librería del sistema o la variable de entorno no está configurada igual. Docker resuelve exactamente ese problema.

En esta guía aprenderás qué es Docker, cómo funciona, y cómo empaquetar una aplicación real para que funcione igual en cualquier máquina.

Qué es Docker

Docker es una plataforma que permite empaquetar una aplicación junto con todo lo que necesita para ejecutarse: el código, las dependencias, las librerías del sistema, las variables de configuración y el runtime. Ese paquete se llama contenedor.

Un contenedor es como una caja sellada que contiene todo lo necesario. Da igual en qué máquina abras la caja: el contenido siempre es exactamente el mismo y la aplicación funciona igual.

Contenedores vs máquinas virtuales

Antes de Docker, la solución habitual era usar máquinas virtuales (VMs): emular un ordenador completo dentro de otro. Las VMs incluyen su propio sistema operativo completo, lo que las hace pesadas (varios gigabytes) y lentas de arrancar (minutos).

Los contenedores de Docker son mucho más ligeros porque comparten el kernel del sistema operativo del host. Un contenedor incluye solo lo necesario por encima del kernel: librerías, runtime y código. El resultado son contenedores que pesan megabytes en lugar de gigabytes y arrancan en segundos.

# Máquina virtual:
# ┌─────────────────────────────────────┐
# │          Tu aplicación              │
# │         Librerías / Runtime         │
# │      Sistema operativo completo     │  ← varios GB
# │           Hypervisor                │
# └─────────────────────────────────────┘
#              Hardware

# Contenedor Docker:
# ┌──────────────┬──────────────┐
# │  Contenedor  │  Contenedor  │
# │  Tu app      │  Otra app    │
# │  Librerías   │  Librerías   │
# ├──────────────┴──────────────┤
# │       Docker Engine         │
# │   Sistema operativo host    │  ← compartido
# └─────────────────────────────┘
#              Hardware

Conceptos fundamentales

Imagen: la plantilla de solo lectura a partir de la cual se crean los contenedores. Contiene el sistema de archivos con todo lo necesario. Las imágenes se construyen siguiendo las instrucciones de un Dockerfile.

Contenedor: una instancia en ejecución de una imagen. Puedes crear múltiples contenedores a partir de la misma imagen. Los cambios dentro de un contenedor no afectan a la imagen ni a otros contenedores.

Dockerfile: un archivo de texto con las instrucciones para construir una imagen, paso a paso.

Docker Hub: el registro público de imágenes de Docker. Contiene imágenes oficiales de Python, Node.js, PostgreSQL, Nginx y miles más.

# Relación entre los conceptos:

# Dockerfile → (docker build) → Imagen → (docker run) → Contenedor
#
# Es como:
# Receta → (cocinar) → Pastel → (servir) → Porción en el plato
#
# De una receta (Dockerfile) construyes el pastel (Imagen)
# Del pastel sacas porciones (Contenedores)
# Si sirves varias porciones, cada una es independiente

Instalación

# macOS y Windows: descargar Docker Desktop
# https://www.docker.com/products/docker-desktop
# Incluye Docker Engine, Docker CLI y Docker Compose

# Linux (Ubuntu/Debian):
sudo apt-get update
sudo apt-get install 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
sudo apt-get install docker-ce docker-ce-cli docker-compose-plugin

# Verificar la instalación
docker --version           # Docker version 26.x.x
docker compose version     # Docker Compose version v2.x.x

# Ejecutar el contenedor de prueba oficial
docker run hello-world
# Descarga la imagen hello-world, crea un contenedor y muestra un mensaje

Comandos esenciales de Docker

# ─── Imágenes ─────────────────────────────────────────────────────────────────
docker images                    # listar imágenes descargadas
docker pull python:3.12-slim     # descargar una imagen de Docker Hub
docker rmi nombre-imagen         # eliminar una imagen
docker build -t mi-app:1.0 .     # construir imagen desde el Dockerfile actual
docker image prune               # eliminar imágenes sin usar

# ─── Contenedores ─────────────────────────────────────────────────────────────
docker ps                        # contenedores en ejecución
docker ps -a                     # todos (incluyendo detenidos)
docker run nginx                 # crear y arrancar un contenedor
docker run -d nginx              # en segundo plano (detached)
docker run -p 8080:80 nginx      # mapear puerto host:contenedor
docker run --name mi-nginx nginx # asignar nombre al contenedor
docker stop mi-nginx             # detener un contenedor
docker start mi-nginx            # arrancar uno detenido
docker restart mi-nginx          # reiniciar
docker rm mi-nginx               # eliminar (debe estar detenido)
docker rm -f mi-nginx            # eliminar aunque esté en ejecución

# ─── Inspeccionar y depurar ───────────────────────────────────────────────────
docker logs mi-nginx             # ver los logs del contenedor
docker logs -f mi-nginx          # seguir los logs en tiempo real
docker exec -it mi-nginx bash    # abrir terminal dentro del contenedor
docker inspect mi-nginx          # información detallada del contenedor
docker stats                     # uso de CPU, memoria y red en tiempo real

# ─── Limpieza ─────────────────────────────────────────────────────────────────
docker system prune              # eliminar todo lo que no se usa
docker system prune -a           # también las imágenes sin contenedores

Tu primera aplicación contenerizada: Python/FastAPI

Vamos a contenerizar una API REST sencilla con FastAPI. Primero, la aplicación:

# Estructura del proyecto
# mi-api/
# ├── main.py
# ├── requirements.txt
# └── Dockerfile

# main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def raiz():
    return {"mensaje": "Hola desde Docker", "version": "1.0"}

@app.get("/salud")
def salud():
    return {"estado": "ok"}

@app.get("/usuarios/{id}")
def obtener_usuario(id: int):
    # Simulación: en una app real vendría de la BD
    usuarios = {
        1: {"nombre": "Ana García",   "email": "ana@mail.com"},
        2: {"nombre": "Carlos López", "email": "carlos@mail.com"},
    }
    if id not in usuarios:
        from fastapi import HTTPException
        raise HTTPException(status_code=404, detail="Usuario no encontrado")
    return usuarios[id]
# requirements.txt
fastapi==0.111.0
uvicorn[standard]==0.29.0

Ahora, el Dockerfile:

# Dockerfile
# Cada línea es una instrucción que añade una capa a la imagen

# FROM: imagen base sobre la que construir
# python:3.12-slim es la versión mínima de Python 3.12 (sin herramientas extra)
FROM python:3.12-slim

# WORKDIR: directorio de trabajo dentro del contenedor
# Todos los comandos siguientes se ejecutan desde aquí
WORKDIR /app

# COPY: copiar archivos del host al contenedor
# Primero copiamos solo requirements.txt para aprovechar la caché de Docker
# (si no cambia requirements.txt, Docker no reinstala las dependencias)
COPY requirements.txt .

# RUN: ejecutar un comando durante la construcción de la imagen
RUN pip install --no-cache-dir -r requirements.txt

# Ahora copiar el resto del código
# (se hace después de instalar dependencias para aprovechar la caché)
COPY . .

# EXPOSE: documentar qué puerto usa la aplicación
# (no abre el puerto realmente, es informativo)
EXPOSE 8000

# CMD: comando que se ejecuta al arrancar el contenedor
# Forma exec (lista): la recomendada, no usa shell intermedio
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# Construir la imagen
docker build -t mi-api:1.0 .

# Salida del proceso de construcción:
# [1/4] FROM python:3.12-slim
# [2/4] WORKDIR /app
# [3/4] COPY requirements.txt .
# [4/4] RUN pip install -r requirements.txt
# [5/4] COPY . .
# Successfully built a1b2c3d4e5f6
# Successfully tagged mi-api:1.0

# Arrancar el contenedor
docker run -d \
  --name mi-api \
  -p 8000:8000 \
  mi-api:1.0

# Verificar que funciona
curl http://localhost:8000
# {"mensaje": "Hola desde Docker", "version": "1.0"}

curl http://localhost:8000/usuarios/1
# {"nombre": "Ana García", "email": "ana@mail.com"}

# Ver los logs
docker logs mi-api
# INFO:     Started server process [1]
# INFO:     Waiting for application startup.
# INFO:     Application startup complete.
# INFO:     Uvicorn running on http://0.0.0.0:8000

El mismo proceso para Node.js/Express

# Estructura
# mi-api-node/
# ├── src/index.js
# ├── package.json
# └── Dockerfile

# src/index.js
const express = require('express');
const app     = express();
app.use(express.json());

app.get('/', (req, res) => {
  res.json({ mensaje: 'Hola desde Docker', version: '1.0' });
});

app.get('/salud', (req, res) => {
  res.json({ estado: 'ok' });
});

const PUERTO = process.env.PORT || 3000;
app.listen(PUERTO, () => {
  console.log(`Servidor en puerto ${PUERTO}`);
});
# Dockerfile para Node.js
FROM node:20-slim

WORKDIR /app

# Copiar primero package*.json para aprovechar la caché
COPY package*.json ./

# Instalar solo dependencias de producción
RUN npm ci --only=production

# Copiar el resto del código
COPY src/ ./src/

EXPOSE 3000

# Usar el usuario 'node' en lugar de root (más seguro)
USER node

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

Instrucciones del Dockerfile explicadas

# FROM: imagen base
FROM python:3.12-slim         # imagen oficial de Python, versión mínima
FROM node:20-alpine           # Node.js sobre Alpine Linux (muy pequeño, ~5MB)
FROM ubuntu:22.04             # Ubuntu completo (útil para apps complejas)
FROM scratch                  # imagen vacía (para binarios autocontenidos en Go)

# ARG: argumento de construcción (solo disponible durante el build)
ARG ENTORNO=produccion
ARG VERSION=1.0

# ENV: variable de entorno (disponible también en el contenedor en ejecución)
ENV PYTHONUNBUFFERED=1       # Python muestra los logs inmediatamente
ENV NODE_ENV=production
ENV PORT=8000

# COPY vs ADD
COPY archivo.txt /app/        # copiar archivo o carpeta
ADD archivo.tar.gz /app/      # como COPY pero también descomprime archives
# En general, usa COPY: ADD tiene comportamientos implícitos que pueden sorprender

# RUN: ejecutar durante la construcción
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
# Combinar comandos en un solo RUN con && reduce el número de capas

# VOLUME: punto de montaje para datos persistentes
VOLUME /data                  # los datos en /data sobreviven al contenedor

# USER: usuario con el que ejecutar los comandos siguientes
USER nobody                   # no ejecutar como root en producción

# HEALTHCHECK: verificar periódicamente que el contenedor está sano
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
  CMD curl -f http://localhost:8000/salud || exit 1

# ENTRYPOINT vs CMD
ENTRYPOINT ["python"]         # el ejecutable principal, no se puede sobreescribir
CMD ["main.py"]               # argumentos por defecto para ENTRYPOINT, sí se pueden sobreescribir

# docker run mi-imagen otra-cosa.py  → ejecuta: python otra-cosa.py

El archivo .dockerignore

# .dockerignore funciona igual que .gitignore
# Excluye archivos del contexto de construcción de Docker
# Hace el build más rápido y las imágenes más ligeras

# Python
__pycache__/
*.py[cod]
*.egg-info/
.pytest_cache/
.mypy_cache/
venv/
.venv/
dist/
.env
.env.*

# Node.js
node_modules/
npm-debug.log
.npm

# Git
.git/
.gitignore

# Documentación y tests (no necesarios en producción)
README.md
docs/
tests/
*.test.js
*.spec.py

# Docker
Dockerfile
.dockerignore
docker-compose.yml

Dockerfile multi-stage: imágenes más pequeñas y seguras

Un Dockerfile multi-stage usa varias imágenes base en secuencia. La primera (stage de build) instala herramientas de compilación y construye la aplicación. La segunda (stage de producción) solo copia los artefactos necesarios, descartando todo lo demás. El resultado es una imagen mucho más pequeña.

# Dockerfile multi-stage para Python
# ─── Stage 1: instalar dependencias ──────────────────────────────────────────
FROM python:3.12-slim AS builder

WORKDIR /app
COPY requirements.txt .

# Instalar dependencias en un directorio separado
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# ─── Stage 2: imagen de producción ───────────────────────────────────────────
FROM python:3.12-slim AS produccion

WORKDIR /app

# Copiar solo las dependencias instaladas del stage anterior
COPY --from=builder /install /usr/local

# Copiar el código de la aplicación
COPY . .

# Crear usuario sin privilegios
RUN adduser --disabled-password --gecos '' appuser
USER appuser

EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/salud')"

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

# Resultado:
# Imagen con builder:    ~450MB
# Imagen de producción:  ~120MB (sin herramientas de build, sin pip cache)

Docker Compose: orquestar múltiples contenedores

Una aplicación real raramente es solo un contenedor. Necesitas la aplicación, la base de datos, quizás una caché Redis. Docker Compose permite definir y arrancar todos esos servicios con un solo comando.

# docker-compose.yml
version: '3.9'

services:
  # La aplicación
  api:
    build: .                          # construir desde el Dockerfile del directorio actual
    ports:
      - "8000:8000"                   # host:contenedor
    environment:
      - DATABASE_URL=postgresql://postgres:secreto@db:5432/mi_app
      - JWT_SECRET=desarrollo-secreto-local
      - ENTORNO=desarrollo
    depends_on:
      db:
        condition: service_healthy    # esperar a que la BD esté lista
    volumes:
      - .:/app                        # montar el código local (útil en desarrollo)
    restart: unless-stopped

  # Base de datos PostgreSQL
  db:
    image: postgres:16-alpine         # imagen oficial de PostgreSQL
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=secreto
      - POSTGRES_DB=mi_app
    ports:
      - "5432:5432"                   # exponer para conectar con DBeaver/pgAdmin
    volumes:
      - postgres_data:/var/lib/postgresql/data  # persistir los datos
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  # Caché Redis
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

# Volúmenes nombrados: persisten aunque el contenedor se elimine
volumes:
  postgres_data:
  redis_data:
# Comandos de Docker Compose
docker compose up              # arrancar todos los servicios (en primer plano)
docker compose up -d           # en segundo plano
docker compose up --build      # reconstruir las imágenes antes de arrancar
docker compose down            # parar y eliminar contenedores
docker compose down -v         # también eliminar los volúmenes (borra los datos)
docker compose logs            # ver logs de todos los servicios
docker compose logs -f api     # seguir logs del servicio 'api'
docker compose ps              # estado de los servicios
docker compose exec api bash   # abrir terminal en el contenedor 'api'
docker compose restart api     # reiniciar un servicio concreto
docker compose build           # reconstruir las imágenes sin arrancar

Variables de entorno y secretos en Docker

# Opción 1: env_file en docker-compose.yml
services:
  api:
    env_file:
      - .env          # leer variables del archivo .env
    # Las variables del .env se pasan al contenedor

# Opción 2: referenciar variables del entorno del host
services:
  api:
    environment:
      - JWT_SECRET=${JWT_SECRET}      # toma el valor de la variable del host
      - DATABASE_URL=${DATABASE_URL}

# Opción 3: secrets de Docker (para producción, más seguro)
services:
  api:
    secrets:
      - jwt_secret
    environment:
      - JWT_SECRET_FILE=/run/secrets/jwt_secret   # la app lee el secreto del archivo

secrets:
  jwt_secret:
    file: ./secrets/jwt_secret.txt

Flujo de trabajo en desarrollo

# .env (no subir a Git)
DATABASE_URL=postgresql://postgres:secreto@db:5432/mi_app_dev
JWT_SECRET=secreto-de-desarrollo-local
DEBUG=true
# docker-compose.dev.yml: sobreescribir la configuración para desarrollo
version: '3.9'

services:
  api:
    volumes:
      - .:/app                    # hot reload: los cambios en el código se aplican sin reconstruir
    command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
    environment:
      - DEBUG=true
# Arrancar en modo desarrollo
docker compose -f docker-compose.yml -f docker-compose.dev.yml up

# Flujo típico:
# 1. docker compose up -d              → arrancar todo
# 2. Editar código                     → hot reload, sin reiniciar
# 3. docker compose logs -f api        → ver los logs mientras desarrollas
# 4. docker compose exec api bash      → entrar si necesitas depurar
# 5. docker compose down               → parar al terminar el día

Resumen

  • Docker empaqueta una aplicación junto con todas sus dependencias en un contenedor, garantizando que funciona igual en cualquier entorno. Resuelve el problema de "en mi máquina funciona".
  • Los contenedores son más ligeros que las máquinas virtuales porque comparten el kernel del sistema operativo host en lugar de emular uno completo.
  • El Dockerfile define las instrucciones para construir una imagen. Las instrucciones más importantes: FROM (imagen base), WORKDIR (directorio de trabajo), COPY (copiar archivos), RUN (ejecutar comandos), ENV (variables de entorno), CMD (comando al arrancar).
  • Copia primero requirements.txt o package.json y luego el código fuente. Docker cachea las capas y no reinstala dependencias si no han cambiado.
  • Usa .dockerignore para excluir archivos innecesarios (node_modules, venv, .git) del contexto de construcción.
  • Las imágenes multi-stage separán el entorno de build del de producción, produciendo imágenes mucho más pequeñas y seguras.
  • Docker Compose permite definir y orquestar múltiples servicios (app, base de datos, caché) con un único archivo docker-compose.yml y el comando docker compose up.

¿Te ha gustado esta entrada?

Compártela con tus compañeros para que también sigan aprendiendo.

Comunidad y Comentarios

0 COMENTARIOS

No hay comentarios todavía. Sé el primero en compartir tu opinión.

Escribe tu opinión
Respondiendo a