Construir una plataforma de pagos es una cosa. Llevarla a produccion es otra. En la sesion 086, containerizamos 0fee.dev para despliegue en EasyPanel -- un PaaS autoalojado que funciona sobre Docker. Tres Dockerfiles (backend, frontend, worker), un archivo de requisitos solo para produccion, una configuracion de nginx con cabeceras de seguridad y compresion gzip, y enrutamiento por subdominio que mapea api.0fee.dev al backend y 0fee.dev al frontend.
Esta sesion tambien produjo las versiones v1.53.0 a v1.55.0, siendo una de las sesiones finales antes de que la plataforma entrara en produccion.
La arquitectura de tres servicios
0fee.dev en produccion funciona como tres servicios Docker:
+-------------------+ +-------------------+ +-------------------+
| Frontend | | Backend | | Worker |
| (SolidJS + | | (FastAPI + | | (Celery + |
| nginx) | | Uvicorn) | | Redis) |
| | | | | |
| 0fee.dev | | api.0fee.dev | | (interno) |
| Puerto 80/443 | | Puerto 8000 | | Sin puerto pub. |
+-------------------+ +-------------------+ +-------------------+
| | |
+------------ Host Docker EasyPanel ----------------+El frontend sirve la aplicacion SolidJS a traves de nginx. El backend ejecuta FastAPI con Uvicorn. El worker ejecuta Celery para tareas en segundo plano (entrega de webhooks, generacion de facturas, envio de correos). Solo el frontend y el backend son accesibles publicamente.
Dockerfile del backend
dockerfile# backend/Dockerfile
FROM python:3.12-slim AS base
WORKDIR /app
# Instalar dependencias del sistema
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Instalar dependencias Python (solo produccion)
COPY requirements-prod.txt .
RUN pip install --no-cache-dir -r requirements-prod.txt
# Copiar codigo de la aplicacion
COPY . .
# Crear usuario no root
RUN useradd --create-home appuser
USER appuser
# Verificacion de salud
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD python -c "import httpx; httpx.get('http://localhost:8000/health')" || exit 1
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]Decisiones clave:
python:3.12-slim en lugar de python:3.12-alpine. Alpine usa musl libc en lugar de glibc, lo que causa problemas de compatibilidad con algunos paquetes Python -- particularmente psycopg2 y cryptography. La imagen slim anade ~50MB pero elimina horas de depuracion.
requirements-prod.txt sin herramientas de desarrollo. El archivo de requisitos de produccion excluye pytest, black, ruff, mypy y otras dependencias de desarrollo. Esto reduce el tamano de la imagen en ~200MB y la superficie de ataque.
Usuario no root. El appuser ejecuta la aplicacion. Incluso si el contenedor se ve comprometido, el atacante no tiene privilegios de root.
4 workers de Uvicorn. Cada worker maneja solicitudes independientemente. En una instancia de EasyPanel de 2 nucleos, 4 workers (2x nucleos de CPU) proporcionan buen rendimiento sin sobresubscripcion.
El archivo de requisitos de produccion
text# requirements-prod.txt
fastapi==0.109.2
uvicorn[standard]==0.27.1
sqlalchemy[asyncio]==2.0.25
asyncpg==0.29.0
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
httpx==0.26.0
celery[redis]==5.3.6
python-multipart==0.0.6
pydantic==2.5.3
pydantic-settings==2.1.0
sqladmin==0.16.1
weasyprint==60.2
Pillow==10.2.0
jinja2==3.1.3
python-dotenv==1.0.1
cryptography==42.0.2Sin pytest, sin black, sin ruff, sin ipdb. Las imagenes de produccion deben contener solo lo que produccion necesita.
Dockerfile del frontend
El Dockerfile del frontend es una compilacion multi-etapa: SolidJS compila a archivos estaticos, luego nginx los sirve:
dockerfile# frontend/Dockerfile
FROM node:20-slim AS builder
WORKDIR /app
# Instalar dependencias
COPY package.json package-lock.json ./
RUN npm ci --production=false
# Argumento de compilacion para URL de API
ARG VITE_API_URL=https://api.0fee.dev
ENV VITE_API_URL=$VITE_API_URL
# Copiar fuente y compilar
COPY . .
RUN npm run build
# Etapa de produccion: nginx sirve archivos estaticos
FROM nginx:1.25-alpine AS production
# Copiar configuracion personalizada de nginx
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copiar activos compilados de la etapa builder
COPY --from=builder /app/dist /usr/share/nginx/html
# Verificacion de salud
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget -qO- http://localhost/ || exit 1
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]El detalle critico es VITE_API_URL. Vite incrusta las variables de entorno en tiempo de compilacion, no en tiempo de ejecucion. Esto significa que la URL de la API debe conocerse cuando se construye la imagen Docker:
bash# Compilar con URL de API de produccion
docker build \
--build-arg VITE_API_URL=https://api.0fee.dev \
-t 0fee-frontend:v1.55.0 \
./frontendSi necesitas diferentes URLs de API para staging y produccion, compilas imagenes separadas. Esta es una restriccion de Vite, no de Docker.
Dockerfile del worker
dockerfile# worker/Dockerfile
FROM python:3.12-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements-prod.txt .
RUN pip install --no-cache-dir -r requirements-prod.txt
COPY . .
RUN useradd --create-home celeryuser
USER celeryuser
CMD ["celery", "-A", "tasks.celery_app", "worker", \
"--loglevel=info", "--concurrency=2", \
"--max-tasks-per-child=100"]El flag --max-tasks-per-child=100 reinicia los procesos worker despues de 100 tareas. Esto previene que las fugas de memoria se acumulen en procesos worker de larga ejecucion -- un problema comun con WeasyPrint (generacion de PDF) que puede tener fugas de memoria en documentos complejos.
La configuracion de nginx
nginx# frontend/nginx.conf
server {
listen 80;
server_name 0fee.dev www.0fee.dev;
root /usr/share/nginx/html;
index index.html;
# Compresion Gzip
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/xml
image/svg+xml
font/woff2;
# Cabeceras de seguridad
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://appleid.cdn-apple.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://api.0fee.dev; frame-src 'self' https://appleid.apple.com;" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# Cachear activos estaticos agresivamente
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Respaldo SPA: servir index.html para todas las rutas
location / {
try_files $uri $uri/ /index.html;
}
# Denegar acceso a archivos ocultos
location ~ /\. {
deny all;
return 404;
}
}Cabeceras de seguridad explicadas
| Cabecera | Valor | Proposito |
|---|---|---|
X-Frame-Options | SAMEORIGIN | Previene clickjacking bloqueando incrustacion en iframe de otros dominios |
X-Content-Type-Options | nosniff | Previene que los navegadores hagan sniffing de tipo MIME |
X-XSS-Protection | 1; mode=block | Habilita filtro XSS del navegador (legado, pero inofensivo) |
Referrer-Policy | strict-origin-when-cross-origin | Limita informacion de referrer enviada a otros origenes |
Content-Security-Policy | (ver arriba) | Restringe carga de recursos a origenes confiables |
Permissions-Policy | camera=(), microphone=(), geolocation=() | Desactiva APIs del navegador que el sitio no necesita |
La Content-Security-Policy es la cabecera mas cuidadosamente elaborada. Permite:
- Scripts de si mismo y en linea (necesario para hidratacion de SolidJS) mas el SDK JS de Apple
- Estilos de si mismo, en linea y Google Fonts
- Fuentes de si mismo y Google Fonts statico
- Imagenes de si mismo, URIs data y cualquier fuente HTTPS
- Conexiones API solo a api.0fee.dev
- Frames de si mismo y Apple (para popup de Apple Sign In)
Compresion Gzip
La configuracion gzip_comp_level 6 es un compromiso deliberado entre tasa de compresion y uso de CPU. El nivel 6 logra ~90% de la compresion maxima (nivel 9) al ~50% del costo de CPU. Para un bundle de SolidJS, esto tipicamente reduce el tamano de transferencia de ~300KB a ~90KB.
Configuracion del servicio EasyPanel
EasyPanel gestiona contenedores Docker a traves de una interfaz web. Los tres servicios de 0fee.dev se configuran como:
yaml# Configuracion conceptual de EasyPanel
services:
- name: 0fee-frontend
image: 0fee-frontend:v1.55.0
domains:
- host: 0fee.dev
port: 80
- host: www.0fee.dev
port: 80
replicas: 1
resources:
cpu: 0.5
memory: 256M
- name: 0fee-backend
image: 0fee-backend:v1.55.0
domains:
- host: api.0fee.dev
port: 8000
replicas: 1
resources:
cpu: 1.0
memory: 512M
env:
- DATABASE_URL=postgresql+asyncpg://...
- REDIS_URL=redis://redis:6379/0
- SECRET_KEY=${SECRET_KEY}
- ENVIRONMENT=production
- name: 0fee-worker
image: 0fee-worker:v1.55.0
domains: [] # Sin acceso publico
replicas: 1
resources:
cpu: 0.5
memory: 512M
env:
- DATABASE_URL=postgresql+asyncpg://...
- REDIS_URL=redis://redis:6379/0Enrutamiento por subdominio
EasyPanel maneja la terminacion SSL y enruta el trafico basado en subdominio:
0fee.devywww.0fee.devenrutan al contenedor frontend en el puerto 80api.0fee.devenruta al contenedor backend en el puerto 8000
HTTPS es manejado por la integracion incorporada de Let's Encrypt de EasyPanel. Los contenedores Docker solo necesitan manejar HTTP.
Etiquetado de versiones: v1.53.0 a v1.55.0
La sesion de containerizacion produjo tres versiones:
| Version | Cambios |
|---|---|
| v1.53.0 | Dockerfiles iniciales, containerizacion basica |
| v1.54.0 | Cabeceras de seguridad nginx anadidas, gzip, CSP |
| v1.55.0 | Requisitos de produccion, verificaciones de salud, ajuste de worker |
Cada version fue etiquetada en git y compilada como una imagen Docker separada. EasyPanel puede revertir a cualquier version anterior cambiando la etiqueta de la imagen -- una capacidad critica para una plataforma de pagos donde los malos despliegues necesitan reversion instantanea.
Lo que aprendimos
Las compilaciones multi-etapa son esenciales para frontends. La etapa builder de Node.js pesa ~1.2GB. La etapa final de nginx pesa ~40MB. Sin compilaciones multi-etapa, envias un gigabyte de herramientas de compilacion a produccion.
VITE_API_URL debe ser un argumento de compilacion. Esto nos sorprendio. Vite reemplaza import.meta.env.VITE_API_URL en tiempo de compilacion con una cadena literal. No hay forma de inyectarlo al arrancar el contenedor sin un entrypoint personalizado que reescriba el JavaScript compilado -- un enfoque fragil. La inyeccion en tiempo de compilacion es el patron previsto.
requirements-prod.txt no es opcional. Las dependencias de desarrollo anaden cientos de megabytes e introducen paquetes innecesarios en el entorno de produccion. Mantiene un archivo separado.
Content-Security-Policy es dificil de configurar correctamente. Nuestra primera CSP bloqueo Apple Sign In, Google Fonts y la conexion API. Tomo tres iteraciones obtener una politica lo suficientemente estricta para ser significativa pero lo suficientemente permisiva para que la aplicacion funcionara.
La gestion de memoria del worker requiere max-tasks-per-child. Sin el, los workers de Celery crecen ilimitadamente en memoria. Con WeasyPrint generando PDFs, un worker puede consumir mas de 500MB si no se recicla periodicamente.
Este articulo es parte de la serie "Como construimos 0fee.dev". 0fee.dev es un orquestador de pagos que cubre mas de 53 proveedores en mas de 200 paises, construido por Juste A. GNIMAVO y Claude desde Abiyan sin ningun ingeniero humano. Sigue la serie para conocer la historia completa de construccion.