Docker fundamentos
Docker resuelve un problema que todo desarrollador conoce: "en mi máquina funciona". Empaqueta tu aplicación con todas sus dependencias (sistema operativo, librerías, configuraciones) en un contenedor que funciona igual en cualquier sitio.
Tres conceptos que necesitas dominar antes de tocar nada:
- Dockerfile: la receta para construir tu contenedor. Cada instrucción es un paso: instalar dependencias, copiar código, configurar el entorno.
- Imagen: el resultado de ejecutar esa receta. Es inmutable. Una vez construida, no cambia.
- Contenedor: una instancia en ejecución de una imagen. Puedes tener 10 contenedores corriendo la misma imagen.
Tu primer Dockerfile
Vamos a crear un Dockerfile para una API FastAPI con multi-stage build. Esto significa que usamos una etapa para instalar dependencias y otra para la imagen final. El resultado es una imagen más ligera y segura.
# Etapa 1: Builder (instala dependencias)
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Etapa 2: Runtime (solo lo necesario)
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
La etapa de construcción instala las dependencias y la etapa de ejecución contiene solo lo necesario para arrancar la aplicación. Esta separación reduce superficie de ataque y evita llevar herramientas de compilación al entorno final; el ahorro concreto depende de las imágenes y dependencias elegidas.
Volumes: persistir datos
Cuando un contenedor se destruye, sus datos desaparecen. Los volumes solucionan esto: almacenan datos fuera del contenedor, en el sistema de archivos del host.
# Crear un volume
docker volume create postgres_data
# Usar el volume al ejecutar el contenedor
docker run -v postgres_data:/var/lib/postgresql/data postgres:16
Para bases de datos, logs y archivos subidos por usuarios, los volumes son obligatorios. Sin ellos, pierdes todo al reiniciar el contenedor.
Networks: comunicación entre contenedores
Por defecto, los contenedores están aislados. No se ven entre si. Creas una network para que se comuniquen internamente sin exponer puertos al exterior.
# Crear network
docker network create app_network
# Conectar contenedores a la red
docker run --network app_network --name fastapi mi-api
docker run --network app_network --name postgres postgres:16
Dentro de la network, los contenedores se encuentran por nombre. FastAPI conecta a PostgreSQL usando postgres:5432 como host, no localhost.
Docker Compose para tu stack
Docker Compose te permite definir múltiples contenedores en un solo archivo YAML. En lugar de ejecutar 5 comandos docker run, defines todo en docker-compose.yml y levantas con un comando.
Este es el stack real que vamos a desplegar:
versión: "3.9"
services:
postgres:
image: postgres:16-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- internal
restart: unless-stopped
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- internal
restart: unless-stopped
fastapi:
build:
context: ./backend
dockerfile: Dockerfile
environment:
DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME}
REDIS_URL: redis://redis:6379
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- internal
restart: unless-stopped
n8n:
image: n8nio/n8n:latest
volumes:
- n8n_data:/home/node/.n8n
environment:
N8N_BASIC_AUTH_ACTIVE: "true"
N8N_BASIC_AUTH_USER: ${N8N_USER}
N8N_BASIC_AUTH_PASSWORD: ${N8N_PASSWORD}
networks:
- internal
restart: unless-stopped
caddy:
image: caddy:2-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
networks:
- internal
restart: unless-stopped
volumes:
postgres_data:
redis_data:
n8n_data:
caddy_data:
caddy_config:
networks:
internal:
driver: bridge
Detalles críticos de este archivo:
- Variables de entorno: todas las credenciales vienen de un archivo
.envque NO se commitea al repositorio. Creas un.env.examplesin valores reales como referencia. - Health checks: PostgreSQL y Redis tienen health checks. FastAPI no arranca hasta que ambos esten listos (
condition: service_healthy). - Restart policy:
unless-stoppedsignifica que si el servidor se reinicia, los contenedores vuelven automáticamente. solo se detienen si tu los paras manualmente. - Network interna: todos los servicios comparten una red privada. solo Caddy expone puertos (80 y 443) al exterior.
Secretos en producción
Nunca pongas contraseñas reales en el docker-compose.yml que commiteas. Usa archivos .env (excluidos del repo con .gitignore) o Docker secrets para producción. En GitHub Actions, usa Secrets del repositorio.
Caddy como reverse proxy
Caddy es el reverse proxy más simple que existe. Una configuración de 4 líneas te da SSL automático con Let's Encrypt, HTTP/2, compresión, y headers de seguridad. Todo lo que nginx necesita 50 líneas y plugins adicionales, Caddy lo hace por defecto.
api.tudominio.com {
reverse_proxy fastapi:8000
}
n8n.tudominio.com {
reverse_proxy n8n:5678
}
tudominio.com {
root * /srv/frontend
file_server
try_files {path} /index.html
}
Eso es todo el Caddyfile. Caddy automáticamente:
- Solicita certificados SSL de Let's Encrypt para cada dominio
- Renueva los certificados antes de que caduquen
- Redirige HTTP a HTTPS
- Activa HSTS (HTTP Strict Transport Security)
- Configura headers de seguridad básicos
Comparado con la alternativa nginx, que requiere: instalar certbot como servicio separado, configurar cron para renovación, escribir bloques server con ssl_certificate y ssl_certificate_key, configurar redirects manuales. Caddy elimina toda esa complejidad.
Zero-downtime deploys: cuando actualizas tu API, Caddy mantiene las conexiones existentes mientras el nuevo contenedor arranca. No hay interrupción de servicio.
Configurar un servidor en Hetzner
Hetzner ofrece infraestructura cloud con centros de datos europeos. Antes de crear el servidor, decide la región, el sistema operativo, el método de acceso y los recursos según la carga real. Empieza con una configuración sencilla y amplía cuando la observación del servicio lo justifique.
Paso 1: Crear el servidor
- Crea una cuenta en Hetzner Cloud
- Nuevo proyecto, nuevo servidor
- Ubicación: Falkenstein (fsn1) o Helsinki (hel1)
- Imagen: Ubuntu 22.04
- Tipo: CPX31 (o CPX21 si tu presupuesto es ajustado)
- SSH Key: sube tu clave pública (
~/.ssh/id_ed25519.pub)
Paso 2: Seguridad básica
# Conectar por SSH
ssh root@TU_IP_SERVIDOR
# Actualizar sistema
apt update && apt upgrade -y
# Crear usuario no-root
adduser deploy
usermod -aG sudo deploy
# Copiar SSH key al nuevo usuario
mkdir -p /home/deploy/.ssh
cp ~/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
# Desactivar login por password (solo SSH key)
sed -i 's/PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl restart sshd
# Firewall basico
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp # SSH
ufw allow 80/tcp # HTTP
ufw allow 443/tcp # HTTPS
ufw enable
Paso 3: Instalar Docker
# Instalar Docker
curl -fsSL https://get.docker.com | sh
usermod -aG docker deploy
# Instalar Docker Compose plugin
apt install docker-compose-plugin -y
# Verificar
docker --versión
docker compose versión
Paso 4: Primer despliegue
# Como usuario deploy
su - deploy
# Clonar tu repositorio
git clone https://github.com/tu-usuario/tu-proyecto.git
cd tu-proyecto
# Crear archivo .env con credenciales reales
cp .env.example .env
nano .env # Editar con valores reales
# Levantar todo
docker compose up -d
# Verificar que todo está corriendo
docker compose ps
docker compose logs --tail 50
En este punto tienes tu stack corriendo. Si configuraste DNS apuntando a la IP del servidor, Caddy ya está generando certificados SSL. Tu API es accesible por HTTPS.
Cloudflare Pages y DNS
Para frontends estaticos (Next.js en modo export, landing pages, documentación), Cloudflare Pages es gratuito y tiene CDN global. Deploy automático desde GitHub, previews por branch, custom domains incluidos.
Deploy de un frontend
- Ve a Cloudflare Pages
- Conecta tu repositorio de GitHub
- Configura el build:
- Framework: Next.js (o "None" para HTML estático)
- Build command:
npm run build - Output directory:
out/(static export) o.next/
- Deploy. Cada push a main redespliega automáticamente.
configuración DNS
Si tu dominio está en Cloudflare (recomendado), configuras los registros DNS así:
tudominio.comCNAME atu-proyecto.pages.dev(frontend en CF Pages)api.tudominio.comA record a la IP de Hetzner (proxy off, DNS only)n8n.tudominio.comA record a la IP de Hetzner (proxy off, DNS only)
Proxy on vs proxy off
Para subdominios que apuntan a Hetzner con Caddy, usa proxy off (DNS only, nube gris). Si usas proxy on (nube naranja), Cloudflare gestiona el SSL y puede interferir con los certificados de Caddy. Elige uno u otro, no ambos.
Workers y Functions
Cloudflare Workers te permite ejecutar lógica en el edge (servidor más cercano al usuario). Para funciones simples (redireccion, A/B testing, headers), es gratuito y muy rápido. Para lógica de negocio compleja, mantente con FastAPI en Hetzner.
CI/CD con GitHub Actions
CI/CD (Continuous Integration / Continuous Deployment) significa que cuando haces push a main, el código se testea, se construye y se despliega automáticamente. Sin entrar al servidor manualmente.
El pipeline completo
name: Deploy
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-versión: "3.11"
- run: pip install -r requirements.txt
- run: pip install ruff pytest
- run: ruff check . # Lint
- run: pytest tests/ -v # Tests
build-and-push:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}:latest
deploy:
needs: build-and-push
runs-on: ubuntu-latest
steps:
- name: Deploy to Hetzner
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: deploy
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /home/deploy/tu-proyecto
docker compose pull
docker compose up -d --remove-orphans
docker image prune -f
El pipeline tiene tres fases:
- Test: ejecuta linter (ruff) y tests (pytest). Si falla, no continua.
- Build and push: construye la imagen Docker y la sube a GitHub Container Registry.
- Deploy: conecta por SSH al servidor Hetzner, descarga la nueva imagen y reinicia los contenedores.
Secrets: las credenciales (SERVER_HOST, SSH_PRIVATE_KEY) se guardan en Settings > Secrets > Actions del repositorio de GitHub. Nunca en el código.
Preguntas frecuentes
¿Qué diferencia hay entre una imagen y un contenedor Docker?
La imagen es una plantilla inmutable con el sistema de archivos y la configuración. El contenedor es una instancia en ejecución de esa imagen, con su proceso, red y almacenamiento temporal.
¿Cuándo necesito Docker Compose?
Cuando una aplicación depende de varios servicios, Compose permite describirlos juntos, declarar redes y volúmenes y levantar el conjunto con una configuración versionable.
¿Qué hace Caddy como reverse proxy?
Recibe las peticiones públicas, gestiona HTTPS cuando la configuración y el DNS lo permiten y las reenvía al servicio interno correspondiente sin exponer cada contenedor directamente.
¿Cloudflare Pages sustituye a un servidor?
Puede alojar un frontend estático, pero no reemplaza un backend que necesita procesos persistentes, acceso privado a datos o tareas de larga duración. Ambos servicios pueden convivir bajo distintos subdominios.
¿Dónde deben guardarse los secretos del despliegue?
Fuera del repositorio. En local pueden cargarse desde ficheros excluidos del control de versiones; en CI y producción deben usarse los almacenes de secretos de la plataforma con permisos mínimos.
¿Qué debe ocurrir si falla un despliegue automático?
El flujo debe detenerse, conservar los registros necesarios para diagnosticar el fallo y permitir volver a una versión conocida. Nunca debe ocultar un error ni marcar como correcto un servicio que no responde.
📚 Aprende más en el curso
Este artículo complementa el Módulo M17: DevOps para IA. Incluye vídeo, quiz, flashcards con repaso espaciado y proyecto práctico.