Problema
Mantener un servicio web de e‑commerce (API de búsqueda, sincronizador de catálogos y analítica) en un entorno doméstico implica varios retos simultáneos: exposición segura a Internet sin una IP estática, gestión de actualizaciones sin tiempo de inactividad, respaldo fiable de datos críticos y rendimiento aceptable con recursos limitados (CPU de 2 hilos, 8 GB RAM, 12 W de consumo). Cuando la carga supera los miles de requests por día, la combinación de concurrencia en SQLite, contenedores Docker y túneles externos puede volverse inestable si no se planifica una arquitectura coherente.
Causa
-
Exposición directa del puerto
Sin una IP pública fija, abrir puertos 80/443 en el router obliga a usar NAT dinámico y a gestionar certificados manualmente. Cada cambio de IP rompe la disponibilidad. -
SQLite bajo alta concurrencia
SQLite en modo WAL permite lecturas concurrentes, pero escrituras simultáneas pueden generar bloqueos si el daemon de sincronización escribe mientras llegan peticiones API. -
Actualizaciones sin orquestador
Reiniciar procesos sin coordinación genera breves periodos sin respuesta, sobre todo cuando se actualiza código y dependencias a la vez. -
Backups locales sin redundancia
Copiar el archivo.dbmientras está abierto puede producir snapshots corruptos; la ausencia de verificación automática deja errores sin detección. -
Monitorización limitada
Sin alertas proactivas, fallos de sincronización o errores de disco pasan desapercibidos hasta que el usuario final experimenta problemas.
Solución
Una arquitectura basada en tres capas resuelve los puntos críticos sin requerir hardware adicional:
1. Túnel de salida con Cloudflare Tunnel
Ejecutar cloudflared como servicio crea un túnel saliente que lleva tráfico HTTPS a un puerto local (ej. 8000). Cloudflare gestiona TLS, DDoS y caching, mientras el mini PC nunca expone puertos inbound. La configuración mínima consiste en:
- Registro del dominio en Cloudflare.
- Creación de un túnel (
cloudflared tunnel create pcpicker) y asignación de un hostname (api.pcpicker.ma). - Sistema de servicio que mantiene el daemon activo.
2. Red privada con Tailscale
Instalar Tailscale permite SSH y acceso a los contenedores desde cualquier dispositivo autorizado sin abrir el puerto 22. Cada nodo recibe una IP de rango 100.x.x.x, y las reglas de ACL pueden limitar quién accede a qué servicio.
3. Stack Docker + SQLite WAL
- FastAPI + Uvicorn en un contenedor expone la API en
0.0.0.0:8000. - SQLite en modo WAL (
PRAGMA journal_mode=WAL;) garantiza lecturas sin bloqueo mientras el proceso de indexado escribe en transacciones breves. - Umami (Docker + PostgreSQL) se ejecuta aislado, evitando interferencias de I/O con la base principal.
- Docker‑compose define dependencias y asegura que los contenedores se inicien en el orden correcto.
4. GitOps con systemd‑timer
Un timer de 15 minutos revisa el repositorio remoto. Si hay cambios, realiza:
- Copia de seguridad del
.dben/tmp. git pull --ff-only.pip install -r requirements.txt(opoetry install).systemctl restart fastapi.service(graceful shutdown).
El proceso dura menos de 5 s, por lo que la ventana de indisponibilidad es prácticamente nula.
5. Backups automáticos y verificación
Dos timers separados:
- Hot backup (03:30) ejecuta
sqlite3 db.sqlite .backup /mnt/vault/backups/db_$(date +%F).baky luegoPRAGMA quick_check;. Los archivos se rotan 14 diarios y 8 semanales. - Off‑site sync (04:30) usa
rclone syncpara replicar/mnt/vault/backupsa Google Drive. Cualquier error dispara un mensaje a Telegram mediante webhook.
6. Alertas proactivas
Un pequeño script de Python o Bash envía notificaciones a Telegram cuando:
- El backup falla o el
quick_checkdevuelve errores. - El proceso de sincronización del catálogo no completa en menos de X minutos.
- El uso de CPU supera el 80 % durante más de 5 min.
Cuándo aplicar esta solución
- Carga ligera‑media (≤ 100 req/s) en hardware de consumo (mini PC, NUC, Raspberry Pi 4 con SSD).
- Necesidad de exposición pública sin IP estática ni infraestructura cloud.
- Preferencia por SQLite por su simplicidad y bajo overhead, siempre que el modelo de datos sea mayormente de lectura.
- Presupuesto limitado: evita VPS y servicios de CI/CD externos.
No es adecuada cuando:
- Se esperan cientos de requests por segundo o consultas complejas que requieran joins extensos; entonces una base de datos cliente‑servidor (PostgreSQL, MySQL) será más estable.
- Se requiere alta disponibilidad geográfica; el único nodo no brinda redundancia.
- El entorno requiere cumplimiento normativo que imponga cifrado en reposo y auditoría de acceso.
Código
# cloudflared systemd service ( /etc/systemd/system/cloudflared.service )
[Unit]
Description=Cloudflare Tunnel
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/cloudflared tunnel --config /etc/cloudflared/config.yml run pcpicker
Restart=on-failure
User=nobody
Group=nogroup
[Install]
WantedBy=multi-user.target
# docker-compose.yml (fragmento esencial)
services:
api:
image: ghcr.io/miusuario/pcpicker-api:latest
ports:
- "8000:8000"
volumes:
- ./data:/app/data
environment:
- SQLITE_DB=/app/data/db.sqlite
depends_on:
- umami
umami:
image: ghcr.io/marcusolsson/umami:postgresql-latest
environment:
- DATABASE_URL=postgresql://umami:pass@postgres/umami
ports:
- "3000:3000"
postgres:
image: postgres:16-alpine
environment:
- POSTGRES_USER=umami
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=umami
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
# systemd timer para GitOps ( /etc/systemd/system/pcpicker-updater.timer )
[Unit]
Description=Actualizador GitOps de PCPicker
[Timer]
OnBootSec=5min
OnUnitActiveSec=15min
Persistent=true
[Install]
WantedBy=timers.target
# script ejecutado por el timer ( /usr/local/bin/pcpicker-updater.sh )
#!/usr/bin/env bash
set -euo pipefail
cd /opt/pcpicker
git fetch origin
if ! git diff --quiet HEAD origin/main; then
cp data/db.sqlite /tmp/db_$(date +%F_%H%M).bak
git reset --hard origin/main
pip install -r requirements.txt
systemctl restart fastapi.service
fi
# backup hot timer ( /etc/systemd/system/pcpicker-backup.timer )
[Unit]
Description=Hot backup SQLite
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
[Install]
WantedBy=timers.target
# backup script ( /usr/local/bin/pcpicker-backup.sh )
#!/usr/bin/env bash
set -euo pipefail
DB=/opt/pcpicker/data/db.sqlite
BACKUP_DIR=/mnt/vault/backups
mkdir -p "$BACKUP_DIR"
sqlite3 "$DB" ".backup '$BACKUP_DIR/db_$(date +%F).bak'"
sqlite3 "$DB" "PRAGMA quick_check;" | grep -q "ok" || {
curl -X POST -d "Backup failed on $(hostname)" https://api.telegram.org/bot$TOKEN/sendMessage?chat_id=$CHAT_ID
}
# Rotación simple
find "$BACKUP_DIR" -type f -mtime +14 -delete
# rclone sync timer ( /etc/systemd/system/pcpicker-rclone.timer )
[Unit]
Description=Sincronización off‑site de backups
[Timer]
OnCalendar=*-*-* 04:30:00
Persistent=true
[Install]
WantedBy=timers.target
# rclone sync script ( /usr/local/bin/pcpicker-rclone.sh )
#!/usr/bin/env bash
set -euo pipefail
rclone sync /mnt/vault/backups remote:pcpicker_backups --log-file=/var/log/rclone.log
if grep -q "ERROR" /var/log/rclone.log; then
curl -X POST -d "Rclone sync error on $(hostname)" https://api.telegram.org/bot$TOKEN/sendMessage?chat_id=$CHAT_ID
fi
Verificación
- Conexión externa: abrir
https://api.pcpicker.ma/healthy comprobar que responde200 OK. - SQLite WAL: ejecutar
sqlite3 db.sqlite "PRAGMA journal_mode;"y validar que el resultado seawal. - Backup: revisar que el archivo
/mnt/vault/backups/db_$(date).bakexista y quesqlite3 backup.db "PRAGMA quick_check;"devuelvaok. - Rclone: inspeccionar el log
/var/log/rclone.logy confirmar que la última línea seaTransferred: 0 / 0, 0 B/s, done. - Alertas: provocar un fallo (por ejemplo, cambiar permisos del directorio de backup) y verificar que llega el mensaje a Telegram.
Notas adicionales
- Optimizar SQLite: usar índices en columnas de búsqueda frecuente (nombre del producto, SKU) y limitar la longitud de transacciones del daemon de sincronización a menos de 100 ms para evitar picos de bloqueo.
- Ajuste de Docker: establecer `mem