Saltar al contenido principal

Soberania digital: Cómo convertí una laptop vieja en mi propio servidor

· 14 min de lectura
Oscar Adrian Ortiz Bustos
Contando lecturas...

Introduccion

Todo empezo con una laptop HP Pavilion juntando polvo en un cajon. Ya no servia para el dia a dia, era lenta para programar, se calentaba de mas y la bateria estaba muerta. La mayoria de la gente la hubiera vendido o tirado. Yo decidi convertirla en mi propio servidor.

WelcomeBanner

La idea no era nueva. Llevaba meses dependiendo de servicios en la nube para todo: notas, passwords, archivos, automatizaciones. Y cada vez que una empresa cambiaba sus terminos, subia precios o simplemente desaparecia un servicio, me quedaba con la misma sensacion: no soy dueno de mis datos. Eso tenia que cambiar.

Este articulo documenta todo el proceso: desde la primera conexion SSH hasta tener 20 servicios corriendo con acceso desde cualquier parte del mundo. Sin pagar un solo servidor en la nube.

Soberania digital

La primera vez que escuché el término "soberanía digital" fue en un podcast de Lorenzo Carbonell de atareao.

El término se refiere a la capacidad para ejercer control efectivo sobre tus datos, tu infraestructura tecnológica y sistemas digitales, asegurando que estos operen bajo su propia jurisdicción y no dependan de terceros que puedan restringir, censurar o monetizar su uso. Es la idea de ser el dueño de tu propia "nube" en lugar de alquilar espacio en la nube de alguien más.

La filosofia detras: Self-Hosting

Self-hosting es la practica de alojar tus propias aplicaciones y servicios en hardware que tu controlas. En lugar de depender de Google Drive, usas Nextcloud. En lugar de Bitwarden Cloud, corres tu propia instancia de Vaultwarden. En lugar de confiar tus automatizaciones a Zapier, usas n8n.

Las ventajas son claras:

  • Privacidad: Tus datos nunca salen de tu red
  • Control: Tu decides que corre, como se configura y cuando se actualiza
  • Costo: El unico gasto es la electricidad. No hay suscripciones mensuales
  • Aprendizaje: Cada problema que resuelves te ensena algo nuevo sobre redes, Linux, Docker y seguridad

Por supuesto, tambien hay responsabilidades: tu eres el administrador, el soporte tecnico y el equipo de seguridad. Pero eso es parte de la diversion.

El hardware: una laptop que nadie queria

No necesitas un rack de servidores para empezar. Mi setup completo es:

  • HP Pavilion Notebook con Linux Mint
  • Conectada por cable Ethernet al router
  • Sin monitor, sin teclado, sin raton. Solo SSH.

Eso es todo. Una laptop que estaba destinada a la basura ahora corre 20 containers de Docker 24/7.

La base: Docker Compose

Cada servicio vive en su propia carpeta con su propio docker-compose.yml. La estructura del proyecto se ve asi:

/home/src-dkr/DockerCompose/
├── backup/
│ └── docker-compose.yml
├── calibre-web/
│ └── docker-compose.yml
├── cloudflared/
│ └── docker-compose.yml
├── dbgate/
│ └── docker-compose.yml
├── drawdb/
│ └── docker-compose.yml
├── excalidraw/
│ └── docker-compose.yml
├── firefly/
│ └── docker-compose.yml
├── gitea/
│ └── docker-compose.yml
├── grocy/
│ └── docker-compose.yml
├── homeassistant/
│ └── docker-compose.yml
├── homepage/
│ └── docker-compose.yml
├── kiwix/
│ └── docker-compose.yml
├── mailhog/
│ └── docker-compose.yml
├── mermaid/
│ └── docker-compose.yml
├── n8n/
│ └── docker-compose.yml
├── portainer/
│ └── docker-compose.yml
├── traefik/
│ ├── docker-compose.yml
│ ├── config/
│ │ └── traefik.yml
│ ├── certs/
│ └── tls.yml
├── vaultwarden/
│ └── docker-compose.yml
├── deploy.sh
├── manage.sh
├── .env
├── .env.example
└── .gitignore

Cada servicio esta aislado, es independiente y se puede levantar o tirar sin afectar a los demas. Todos comparten una red Docker que permite la comunicacion entre containers y el reverse proxy.

Ejemplo: Homepage (el dashboard)

services:
homepage:
image: ghcr.io/gethomepage/homepage
container_name: homepage
restart: unless-stopped
volumes:
- /home/src-dkr/docker-data/homepage:/app/config
- /var/run/docker.sock:/var/run/docker.sock:ro
labels:
- "traefik.enable=true"
- "traefik.http.routers.homepage.rule=Host(`homepage.${DOMAIN}`)"
- "traefik.http.routers.homepage.entrypoints=web,websecure"
- "traefik.http.routers.homepage.tls=true"
- "traefik.http.services.homepage.loadbalancer.server.port=3000"
networks:
- ${PROXY}
environment:
- DOMAIN=${DOMAIN}
- HOMEPAGE_ALLOWED_HOSTS=homepage.neanderhub.com,neanderhub.com

networks:
${PROXY_NAME}:
external: true
name: ${PROXY_NAME}

El patron es el mismo para todos los servicios: imagen, volumes, labels de Traefik para el routing y la red. La variable ${DOMAIN} viene del archivo .env, lo que permite cambiar el dominio en un solo lugar.

Los servicios: 20 containers, un solo servidor

Estos son los servicios que corren actualmente:

ServicioFuncion
TraefikReverse proxy, maneja el routing de todos los subdominios
HomepageDashboard centralizado con estado de cada container
PortainerGestion visual de Docker
CloudflaredTunnel de Cloudflare para acceso externo
n8nAutomatizaciones (el "Zapier" self-hosted)
VaultwardenGestor de passwords compatible con Bitwarden
Home AssistantDomotica y automatizacion del hogar
Firefly IIIFinanzas personales con base de datos MariaDB
GiteaServidor Git privado (mi propio GitHub)
Calibre-webBiblioteca de ebooks
KiwixWikipedia offline completa en espanol
GrocyGestion de despensa y productos del hogar
DBGateCliente de base de datos via web
ExcalidrawPizarra de dibujo colaborativa
DrawDBDisenador de diagramas de bases de datos
MermaidRenderizador de diagramas en markdown
MailhogServidor de email para testing
BackupBackups automatizados de todos los volumes
Rclone-syncSincronizacion de backups a Google Drive

Todos levantados, todos funcionando:

backup          Up 32 hours
calibre-web Up 32 hours
cloudflared Up 2 days
dbgate Up 32 hours
drawdb Up 32 hours
excalidraw Up 32 hours (healthy)
firefly Up 32 hours (healthy)
firefly_db Up 32 hours
gitea Up 32 hours
grocy Up 32 hours
homeassistant Up 32 hours
homepage Up 32 hours (healthy)
kiwix Up 32 hours
mailhog Up 32 hours
mermaid Up 32 hours
n8n Up 32 hours
portainer Up 32 hours
rclone-sync Up 32 hours
traefik Up 32 hours
vaultwarden Up 32 hours (healthy)

El reverse proxy: Traefik

Traefik es el punto de entrada de todo el trafico. Se encarga de dirigir cada subdominio al container correcto. Su configuracion es minima:

# traefik.yml
entryPoints:
web:
address: ":80"
websecure:
address: ":443"

providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
network: ${PROXY_NAME}
file:
filename: /traefik/tls.yml

api:
dashboard: true
insecure: true

Lo que hace especial a Traefik es que descubre automaticamente los servicios a traves de Docker. Cuando un container tiene labels como traefik.http.routers.homepage.rule=Host('homepage.neanderhub.com'), Traefik lo detecta y empieza a enrutar trafico hacia el sin tocar ningun archivo de configuracion.

No hay que reiniciar Traefik cuando agregas un servicio. Simplemente haces docker compose up -d y el nuevo servicio aparece disponible en su subdominio.

El problema del CGNAT: por que no pude usar una VPN

Mi primer plan era montar un servidor WireGuard para acceder a mis servicios desde fuera de casa. Lo configure, abri puertos en el router... y no funciono. Despues de mucho diagnostico descubri el problema: CGNAT.

Mi ISP (Totalplay) usa Carrier-Grade NAT, lo que significa que la IP "publica" que me asignan no es realmente publica. Es una IP compartida entre multiples clientes. No hay forma de recibir conexiones entrantes.

Ademas, mi topologia de red era un doble NAT:

Internet -> Modem (192.168.100.x) -> Router FiberHome (192.168.101.x) -> Laptop

Incluso si el ISP me diera una IP publica, tendria que hacer port forwarding en dos dispositivos. No era viable.

La solucion: Cloudflare Tunnel

Cloudflare Tunnel resuelve el problema del CGNAT de forma elegante. En lugar de que el trafico entre a tu red (lo cual requiere puertos abiertos), tu servidor sale hacia Cloudflare y establece un tunnel persistente. El trafico fluye asi:

Usuario -> Cloudflare Edge -> Tunnel -> cloudflared container -> Servicio

No necesitas abrir puertos. No necesitas IP publica. No necesitas tocar el router.

La configuracion del container es minima:

services:
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
command: tunnel --no-autoupdate run
environment:
- TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}
networks:
- ${PROXY_NAME}

networks:
${PROXY_NAME}:
external: true
name: ${PROXY_NAME}

El token se genera desde el dashboard de Cloudflare Zero Trust. Una vez que el container esta corriendo, configuras los Public Hostnames en el dashboard. Cada hostname apunta directamente al container y su puerto interno:

SubdominioTipoDestino
n8n.neanderhub.comHTTPn8n:5678
homepage.neanderhub.comHTTPhomepage:3000
portainer.neanderhub.comHTTPportainer:9000
vaultwarden.neanderhub.comHTTPvaultwarden:80
gitea.neanderhub.comHTTPgitea:3000
homeassistant.neanderhub.comHTTPhomeassistant:8123
firefly.neanderhub.comHTTPfirefly:8080
.........

Un detalle importante: el tunnel envia trafico via HTTP al container, no HTTPS. Cloudflare se encarga del TLS entre el usuario y su edge. Esto simplifica todo porque no necesitas certificados en tu servidor.

Seguridad: Cloudflare Access

Tener servicios expuestos a internet sin autenticacion seria irresponsable. Cloudflare Access permite agregar una capa de autenticacion antes de que cualquier request llegue a tus servicios.

Configure una politica con GitHub OAuth como identity provider. Cuando alguien intenta acceder a cualquier subdominio de neanderhub.com, Cloudflare le pide que se autentique con GitHub antes de dejarlo pasar. Solo las cuentas autorizadas pueden acceder.

Esto significa que servicios como Mailhog o Portainer, que no tienen autenticacion propia, estan protegidos sin necesidad de modificarlos.

Los volumes: donde viven los datos

Uno de los errores mas comunes al empezar con Docker es no pensar en la persistencia de datos. Si un container se destruye y no tenias volumes configurados, perdiste todo.

Centralice todos los datos en una sola ruta:

/home/src-dkr/docker-data/
├── backup/
│ ├── local/ # Backups comprimidos
│ └── rclone/ # Configuracion de rclone
├── calibre-web/
├── dbgate/
├── firefly-db/
├── firefly-upload/
├── gitea/
├── grocy/
├── homeassistant/
├── homepage/
├── kiwix/
├── n8n/
├── portainer/
├── vaultwarden/
└── wireguard/

Cada servicio tiene su carpeta. Todo esta en un solo lugar. Esto hace que los backups sean triviales y que migrar a otro servidor sea cuestion de copiar una carpeta.

Backups automatizados a Google Drive

De nada sirve tener tus datos en tu servidor si no tienes backups. Un disco duro puede fallar en cualquier momento.

El sistema de backups funciona con dos containers trabajando en equipo:

services:
# Crea un tar.gz de todo docker-data cada dia a las 3AM
backup:
image: offen/docker-volume-backup:latest
container_name: backup
restart: unless-stopped
environment:
BACKUP_CRON_EXPRESSION: "0 3 * * *"
BACKUP_FILENAME: "backup-%Y-%m-%dT%H-%M-%S.tar.gz"
BACKUP_PRUNING_PREFIX: "backup-"
BACKUP_RETENTION_DAYS: "7"
BACKUP_ARCHIVE: "/archive"
volumes:
- /home/src-dkr/docker-data:/backup/docker-data:ro
- /home/src-dkr/docker-data/backup/local:/archive

# Sincroniza los backups a Google Drive cada 4 horas
rclone-sync:
image: rclone/rclone
container_name: rclone-sync
restart: unless-stopped
entrypoint: /bin/sh
volumes:
- /home/src-dkr/docker-data/backup/rclone:/config/rclone
- /home/src-dkr/docker-data/backup/local:/data:ro
command:
- -c
- |
while true; do
echo "[rclone] Syncing backups to Google Drive...";
rclone sync /data gdrive:NeanderHub-Backups --transfers 1 --log-level INFO;
echo "[rclone] Done. Next sync in 4 hours.";
sleep 14400;
done

El flujo es simple:

  1. A las 3AM, backup comprime todo /home/src-dkr/docker-data/ en un .tar.gz
  2. Cada 4 horas, rclone-sync sube los archivos nuevos a Google Drive
  3. Se mantienen los ultimos 7 dias de backups locales
  4. Google Drive guarda una copia remota

Si la laptop muere manana, puedo comprar cualquier maquina, instalar Docker, descargar el backup de Drive y tener todo funcionando en menos de una hora.

Scripts de gestion

Para no tener que recordar rutas ni flags de Docker Compose, cree dos scripts.

deploy.sh: levantar todo

Levanta todos los servicios en paralelo:

#!/bin/bash

COMPOSE_DIR="$HOME/DockerCompose"
DOMAIN="${1:-neanderhub.com}"

echo "=== Deploying services ==="
cd "$COMPOSE_DIR"

# Network
docker network create ${PROXY_NAME} 2>/dev/null || echo "Proxy network already exists"

# Master compose
docker compose up -d

# Services in parallel
pids=()
for service_dir in */; do
service_name="${service_dir%/}"
if [ "$service_name" != "." ] && [ -f "$service_dir/docker-compose.yml" ]; then
DOMAIN="$DOMAIN" docker compose -f "$service_dir/docker-compose.yml" up -d 2>/dev/null &
pids+=($!)
fi
done

for pid in "${pids[@]}"; do
wait "$pid"
done

echo "Services deployed -> https://neanderhub.com"

manage.sh: control individual

Permite levantar, tirar o reiniciar cualquier servicio por nombre:

#!/bin/bash

COMPOSE_DIR="$HOME/DockerCompose"

usage() {
echo "Usage: $0 <up|down|restart> <service|all>"
echo ""
echo "Available services:"
for d in "$COMPOSE_DIR"/*/; do
[ -f "$d/docker-compose.yml" ] && echo " - $(basename "$d")"
done
exit 1
}

[ $# -lt 2 ] && usage

ACTION="$1"
SERVICE="$2"

run_action() {
local svc="$1"
local file="$COMPOSE_DIR/$svc/docker-compose.yml"
[ ! -f "$file" ] && echo "Error: $svc not found" && return 1

case "$ACTION" in
up) docker compose -f "$file" up -d ;;
down) docker compose -f "$file" down ;;
restart) docker compose -f "$file" down && docker compose -f "$file" up -d ;;
*) usage ;;
esac
}

if [ "$SERVICE" = "all" ]; then
for d in "$COMPOSE_DIR"/*/; do
svc="$(basename "$d")"
[ -f "$d/docker-compose.yml" ] && run_action "$svc"
done
else
run_action "$SERVICE"
fi

Uso:

./manage.sh restart n8n        # Reinicia n8n
./manage.sh down vaultwarden # Tira vaultwarden
./manage.sh up all # Levanta todo

Versionado con Git

Todo el directorio DockerCompose esta en un repositorio Git alojado en Gitea (que corre en el mismo servidor). El .gitignore se asegura de no versionar secretos ni datos:

# Secrets
.env
!.env.example

# TLS certs
traefik/certs/
traefik/letsencrypt/

# Data volumes
*/data/

# OS files
.DS_Store
Thumbs.db

Asi, si necesito replicar el setup, clono el repo, creo el .env con las variables necesarias y ejecuto deploy.sh.

Seguridad

Ademas de Cloudflare Access, el servidor tiene medidas basicas de hardening:

  • SSH por llave publica: autenticacion por password deshabilitada
  • Puerto SSH no estandar: reduce el ruido de bots escaneando el puerto 22
  • UFW firewall: solo los puertos necesarios estan abiertos (22, 80, 443)
  • Docker socket de solo lectura: los containers que necesitan acceso al socket de Docker lo montan como :ro

Lecciones aprendidas

Despues de montar todo esto, estas son las cosas que me hubiera gustado saber desde el principio:

1. No mezcles volumes con puertos

Docker Compose usa la misma sintaxis host:container para volumes y ports. Es facil confundirlos. Si escribes 8080:/data en la seccion de volumes, Docker no se queja pero crea un bind mount con el directorio literal 8080.

2. Centraliza los datos desde el dia uno

Empece con datos regados por todo el filesystem: /home/src-dkr/vaultwarden, /home/src-dkr/homepage, volumes anonimos de Docker. Migrar todo a /home/src-dkr/docker-data/ despues fue posible pero tedioso. Hazlo bien desde el inicio.

3. El CGNAT es mas comun de lo que crees

Si tu ISP es Totalplay, Telmex fibra o similar en Mexico, probablemente estes detras de CGNAT. No pierdas tiempo con VPNs o port forwarding. Ve directo a Cloudflare Tunnel.

4. Cloudflare Tunnel va directo al container

Si usas Cloudflare Tunnel junto con Traefik, no apuntes el tunnel a Traefik. Cloudflare no puede verificar los certificados autofirmados de Traefik y obtendras errores 502. Apunta cada hostname directamente al container y su puerto interno via HTTP.

5. Los backups no son opcionales

Es tentador decir "lo hago despues". No lo hagas. Configura los backups al mismo tiempo que levantas tus servicios. Cuando un disco falle (no es "si", es "cuando"), vas a agradecer tener un .tar.gz en Google Drive.

Conclusion

Lo que empezo como "a ver si puedo hacer algo con esta laptop vieja" se convirtio en un ecosistema completo de 20 servicios que uso todos los dias. Mi esposa usa Grocy para la despensa, yo uso n8n para automatizar tareas, nuestras passwords estan en Vaultwarden y nuestros datos no estan en servidores de nadie mas.

No necesitas hardware caro. No necesitas ser experto en Linux. Solo necesitas una maquina que encienda, Docker y ganas de aprender. Cada error que encuentres en el camino te va a ensenar algo nuevo. Y cuando termines, vas a tener algo que pocas personas tienen: soberania sobre tus datos.

Si este articulo te inspiro a montar tu propio homelab, dejame un comentario. Me encantaria saber que servicios eligieron y como les fue en el proceso.

Nos vemos en la proxxima, mis queridos cibernicolas.

"La nube es solo la computadora de alguien mas."

Free Software Foundation
Escrito por un humano