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.

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:
| Servicio | Funcion |
|---|---|
| Traefik | Reverse proxy, maneja el routing de todos los subdominios |
| Homepage | Dashboard centralizado con estado de cada container |
| Portainer | Gestion visual de Docker |
| Cloudflared | Tunnel de Cloudflare para acceso externo |
| n8n | Automatizaciones (el "Zapier" self-hosted) |
| Vaultwarden | Gestor de passwords compatible con Bitwarden |
| Home Assistant | Domotica y automatizacion del hogar |
| Firefly III | Finanzas personales con base de datos MariaDB |
| Gitea | Servidor Git privado (mi propio GitHub) |
| Calibre-web | Biblioteca de ebooks |
| Kiwix | Wikipedia offline completa en espanol |
| Grocy | Gestion de despensa y productos del hogar |
| DBGate | Cliente de base de datos via web |
| Excalidraw | Pizarra de dibujo colaborativa |
| DrawDB | Disenador de diagramas de bases de datos |
| Mermaid | Renderizador de diagramas en markdown |
| Mailhog | Servidor de email para testing |
| Backup | Backups automatizados de todos los volumes |
| Rclone-sync | Sincronizacion 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:
| Subdominio | Tipo | Destino |
|---|---|---|
n8n.neanderhub.com | HTTP | n8n:5678 |
homepage.neanderhub.com | HTTP | homepage:3000 |
portainer.neanderhub.com | HTTP | portainer:9000 |
vaultwarden.neanderhub.com | HTTP | vaultwarden:80 |
gitea.neanderhub.com | HTTP | gitea:3000 |
homeassistant.neanderhub.com | HTTP | homeassistant:8123 |
firefly.neanderhub.com | HTTP | firefly: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:
- A las 3AM,
backupcomprime todo/home/src-dkr/docker-data/en un.tar.gz - Cada 4 horas,
rclone-syncsube los archivos nuevos a Google Drive - Se mantienen los ultimos 7 dias de backups locales
- 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."
Referencias

