Fotos y contraseñas: Immich y Vaultwarden — Tu primer HomeLab (powered by HP) #03
Recordando el episodio anterior
Sección titulada «Recordando el episodio anterior»En el episodio 2 instalamos Docker, montamos la estructura de carpetas de la serie y desplegamos Nextcloud siguiendo un patrón que vamos a repetir en este episodio: cada servicio vive en su propia carpeta, con un contenedor Tailscale como sidecar que le da HTTPS automático y un subdominio propio dentro de tu tailnet.
En este episodio cerramos la serie (por ahora) con dos servicios muy demandados en cualquier homelab: Vaultwarden, un gestor de contraseñas autoalojado compatible con Bitwarden, y Immich, una alternativa a Google Fotos con reconocimiento facial y búsqueda inteligente.
Empezamos por Vaultwarden, que es mucho más sencillo, y dejamos Immich para el final.
Vaultwarden
Sección titulada «Vaultwarden»Vaultwarden es un único contenedor. No necesita una base de datos externa — usa SQLite internamente — así que es el despliegue más simple de toda la serie.
Estructura de carpeta
Sección titulada «Estructura de carpeta»Ventana de terminal
mkdir -p ~/homelab/vaultwardencd ~/homelab/vaultwardenGenera la auth key de Tailscale
Sección titulada «Genera la auth key de Tailscale»Igual que en el episodio 2: ve a login.tailscale.com/admin/settings/keys, pulsa Generate auth key, márcala como Reusable y cópiala.
El .env
Sección titulada «El .env»Ventana de terminal
vim .envTZ=Europe/MadridTS_AUTHKEY=tskey-auth-xxxxxxxxxxEl docker-compose.yml
Sección titulada «El docker-compose.yml»Ventana de terminal
vim docker-compose.ymlservices: ts-vaultwarden: container_name: vaultwarden_tailscale image: tailscale/tailscale:latest hostname: vaultwarden restart: unless-stopped environment: TS_AUTHKEY: ${TS_AUTHKEY} TS_STATE_DIR: /var/lib/tailscale TS_SERVE_CONFIG: /config/serve.json volumes: - ./ts-vaultwarden:/var/lib/tailscale - ./ts-vaultwarden-config:/config cap_add: - NET_ADMIN - NET_RAW security_opt: - no-new-privileges:true
app: container_name: vaultwarden_app image: vaultwarden/server:latest restart: unless-stopped depends_on: ts-vaultwarden: condition: service_started environment: TZ: ${TZ} SIGNUPS_ALLOWED: "false" volumes: - ./data:/data network_mode: service:ts-vaultwarden security_opt: - no-new-privileges:trueComo en Nextcloud, app comparte la red de ts-vaultwarden en vez de tener la suya propia — así Tailscale puede exponerlo directamente bajo su propio nombre en la tailnet.
El fichero serve.json
Sección titulada «El fichero serve.json»Vaultwarden sirve HTTP plano en el puerto 80 internamente — a diferencia de Nextcloud, no necesitamos https+insecure://, con http:// es suficiente.
Ventana de terminal
mkdir -p ts-vaultwarden-configvim ts-vaultwarden-config/serve.json{ "TCP": { "443": { "HTTPS": true } }, "Web": { "vaultwarden.tu-tailnet.ts.net:443": { "Handlers": { "/": { "Proxy": "http://127.0.0.1:80" } } } }}Levanta el servicio
Sección titulada «Levanta el servicio»Ventana de terminal
docker compose up -dCrea tu cuenta y cierra el registro
Sección titulada «Crea tu cuenta y cierra el registro»Accede a https://vaultwarden.tu-tailnet.ts.net y crea tu cuenta de administrador. En cuanto la tengas creada, cierra el registro para que nadie más pueda darse de alta:
Ventana de terminal
vim .envCambia SIGNUPS_ALLOWED a "false" (si ya estaba así, confirma que sigue estándolo) y reinicia:
docker compose up -d appImmich necesita bastante más músculo que los servicios anteriores.
Son cuatro contenedores: database (PostgreSQL con extensión vectorial VectorChord), redis, immich-machine-learning e immich-server.
Estructura de carpeta
Sección titulada «Estructura de carpeta»Ventana de terminal
mkdir -p ~/homelab/immichcd ~/homelab/immichGenera la auth key de Tailscale
Sección titulada «Genera la auth key de Tailscale»Mismo proceso que con los servicios anteriores en login.tailscale.com/admin/settings/keys.
El .env
Sección titulada «El .env»Ventana de terminal
vim .envTZ=Europe/Madrid
UPLOAD_LOCATION=./libraryDB_DATA_LOCATION=./postgres
DB_PASSWORD=cambia_estoDB_USERNAME=postgresDB_DATABASE_NAME=immich
# IPs fijas de la red interna — imprescindibles porque immich-server# comparte red con el sidecar de Tailscale y no puede resolver nombresDB_HOSTNAME=172.28.2.10REDIS_HOSTNAME=172.28.2.11
IMMICH_VERSION=release
TS_AUTHKEY=tskey-auth-xxxxxxxxxxEl docker-compose.yml
Sección titulada «El docker-compose.yml»Ventana de terminal
vim docker-compose.ymlservices: ts-immich: container_name: immich_tailscale image: tailscale/tailscale:latest hostname: immich restart: unless-stopped environment: TS_AUTHKEY: ${TS_AUTHKEY} TS_STATE_DIR: /var/lib/tailscale TS_SERVE_CONFIG: /config/serve.json volumes: - ./ts-immich:/var/lib/tailscale - ./ts-immich-config:/config networks: - immich cap_add: - NET_ADMIN - NET_RAW security_opt: - no-new-privileges:true
database: container_name: immich_postgres image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0 restart: unless-stopped environment: POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_USER: ${DB_USERNAME} POSTGRES_DB: ${DB_DATABASE_NAME} POSTGRES_INITDB_ARGS: '--data-checksums' volumes: - ${DB_DATA_LOCATION}:/var/lib/postgresql/data shm_size: 128mb networks: immich: ipv4_address: 172.28.2.10 security_opt: - no-new-privileges:true
redis: container_name: immich_redis image: docker.io/valkey/valkey:9 restart: unless-stopped networks: immich: ipv4_address: 172.28.2.11 security_opt: - no-new-privileges:true
immich-machine-learning: container_name: immich_machine_learning image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release} restart: unless-stopped volumes: - model-cache:/cache environment: DB_HOSTNAME: database REDIS_HOSTNAME: redis networks: - immich security_opt: - no-new-privileges:true
immich-server: container_name: immich_server image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release} restart: unless-stopped depends_on: - database - redis - ts-immich volumes: - ${UPLOAD_LOCATION}:/data - /etc/localtime:/etc/localtime:ro env_file: - .env network_mode: service:ts-immich security_opt: - no-new-privileges:true
networks: immich: name: immich ipam: config: - subnet: 172.28.2.0/24
volumes: model-cache:Por qué immich-machine-learning no necesita IP fija y immich-server sí
Sección titulada «Por qué immich-machine-learning no necesita IP fija y immich-server sí»immich-machine-learning está conectado directamente a la red immich, así que resuelve database y redis por nombre sin ningún problema, igual que cualquier contenedor normal.
immich-server, en cambio, usa network_mode: service:ts-immich para compartir la red del sidecar de Tailscale y así poder exponerse con HTTPS y subdominio propio. Pero eso significa que no tiene red propia, y como vimos con Nextcloud en el episodio 2, un contenedor en esa situación no puede resolver nombres de host — ni con dns:, ni con extra_hosts: (Docker no permite ninguna de las dos opciones combinadas con network_mode: service:...). Por eso DB_HOSTNAME y REDIS_HOSTNAME en el .env son las IPs fijas, no los nombres database/redis.
El fichero serve.json
Sección titulada «El fichero serve.json»Immich sirve HTTP plano en el puerto 2283 — como con Vaultwarden, no hace falta https+insecure://.
Ventana de terminal
mkdir -p ts-immich-configvim ts-immich-config/serve.json{ "TCP": { "443": { "HTTPS": true } }, "Web": { "immich.tu-tailnet.ts.net:443": { "Handlers": { "/": { "Proxy": "http://127.0.0.1:2283" } } } }}Levanta los servicios
Sección titulada «Levanta los servicios»Ventana de terminal
docker compose up -dLa primera vez puede tardar un poco: database tiene que inicializar la extensión vectorial y immich-machine-learning descarga sus modelos. Comprueba el estado:
Ventana de terminal
docker compose psdocker compose logs -f immich-serverAccede a Immich
Sección titulada «Accede a Immich»Abre https://immich.tu-tailnet.ts.net y sigue el asistente para crear tu usuario administrador.
No olvides desactivar la caducidad del dispositivo también para immich en login.tailscale.com/admin/machines.
Tu propio servidor de notificaciones: ntfy
Sección titulada «Tu propio servidor de notificaciones: ntfy»Antes de montar Watchtower, desplegamos nuestro propio servidor de notificaciones — así no dependemos del servicio público ntfy.sh y todo el tráfico se queda dentro de tu homelab.
Estructura de carpeta
Sección titulada «Estructura de carpeta»Ventana de terminal
mkdir -p ~/homelab/ntfycd ~/homelab/ntfyGenera la auth key de Tailscale
Sección titulada «Genera la auth key de Tailscale»Mismo proceso de siempre en login.tailscale.com/admin/settings/keys.
El .env
Sección titulada «El .env»Ventana de terminal
vim .envTZ=Europe/MadridTS_AUTHKEY=tskey-auth-xxxxxxxxxxNTFY_TOPIC=homelab-updatesNTFY_TOPIC es el nombre del “canal” al que se suscribirá tu móvil. Como el servidor ya está protegido por Tailscale (nadie fuera de tu tailnet puede ni siquiera llegar a él), no hace falta que sea un nombre difícil de adivinar como en la versión pública.
El docker-compose.yml
Sección titulada «El docker-compose.yml»Aquí introducimos una novedad respecto a los servicios anteriores: además de la red del sidecar de Tailscale, ts-ntfy se conecta también a una red interna con IP fija (ntfy_internal). La usaremos justo después para que Watchtower pueda enviarle notificaciones sin salir a internet.
Ventana de terminal
vim docker-compose.ymlservices: ts-ntfy: container_name: ntfy_tailscale image: tailscale/tailscale:latest hostname: ntfy restart: unless-stopped environment: TS_AUTHKEY: ${TS_AUTHKEY} TS_STATE_DIR: /var/lib/tailscale TS_SERVE_CONFIG: /config/serve.json volumes: - ./ts-ntfy:/var/lib/tailscale - ./ts-ntfy-config:/config networks: ntfy_internal: ipv4_address: 172.28.4.10 cap_add: - NET_ADMIN - NET_RAW security_opt: - no-new-privileges:true
app: container_name: ntfy_app image: binwiederhier/ntfy:latest command: serve restart: unless-stopped depends_on: ts-ntfy: condition: service_started environment: TZ: ${TZ} NTFY_BASE_URL: https://ntfy.tu-tailnet.ts.net NTFY_CACHE_FILE: /var/cache/ntfy/cache.db volumes: - ./cache:/var/cache/ntfy network_mode: service:ts-ntfy security_opt: - no-new-privileges:true
networks: ntfy_internal: name: ntfy_internal ipam: config: - subnet: 172.28.4.0/24El fichero serve.json
Sección titulada «El fichero serve.json»ntfy sirve HTTP plano en el puerto 80, como Vaultwarden.
Ventana de terminal
mkdir -p ts-ntfy-configvim ts-ntfy-config/serve.json{ "TCP": { "443": { "HTTPS": true } }, "Web": { "ntfy.tu-tailnet.ts.net:443": { "Handlers": { "/": { "Proxy": "http://127.0.0.1:80" } } } }}Levanta el servicio
Sección titulada «Levanta el servicio»Ventana de terminal
docker compose up -dSuscríbete desde el móvil
Sección titulada «Suscríbete desde el móvil»Instala la app de ntfy (Android / iOS). A diferencia del servicio público, hay que decirle a la app que use tu propio servidor en vez del de por defecto:
- Abre la app → + (añadir suscripción)
- Toca Use another server
- Introduce
https://ntfy.tu-tailnet.ts.netcomo servidor yhomelab-updates(el valor deNTFY_TOPIC) como tema
No olvides desactivar la caducidad del dispositivo también para ntfy en login.tailscale.com/admin/machines.
Watchtower: notificaciones de actualizaciones
Sección titulada «Watchtower: notificaciones de actualizaciones»Hasta ahora hemos desplegado varios servicios (Nextcloud, Vaultwarden, Immich y sus piezas internas), cada uno en su carpeta. Mantenerlos actualizados a mano, comprobando uno por uno si hay imagen nueva, no escala. Watchtower vigila todos los contenedores del host y te avisa cuando hay una actualización disponible — usando el servidor ntfy que acabas de desplegar.
A diferencia de los servicios anteriores, Watchtower no necesita sidecar de Tailscale propio — no tiene interfaz web que exponer. Pero sí necesita hablar con ntfy, así que lo conectamos a la misma red interna ntfy_internal que creamos antes.
Estructura de carpeta
Sección titulada «Estructura de carpeta»Ventana de terminal
mkdir -p ~/homelab/watchtowercd ~/homelab/watchtowerEl .env
Sección titulada «El .env»Ventana de terminal
vim .envNTFY_TOPIC=homelab-updatesEl docker-compose.yml
Sección titulada «El docker-compose.yml»Lo configuramos en modo solo notificación (WATCHTOWER_MONITOR_ONLY: "true"): te avisa de que hay una versión nueva, pero no actualiza nada por su cuenta. Decidir cuándo actualizar cada servicio se queda en tus manos — es preferible revisar el changelog antes de actualizar algo tan delicado como una base de datos.
Ventana de terminal
vim docker-compose.ymlservices: watchtower: container_name: watchtower image: nickfedor/watchtower:latest restart: unless-stopped volumes: - /var/run/docker.sock:/var/run/docker.sock networks: - ntfy_internal environment: TZ: Europe/Madrid WATCHTOWER_MONITOR_ONLY: "true" WATCHTOWER_SCHEDULE: "0 0 9 * * *" WATCHTOWER_NOTIFICATIONS: shoutrrr WATCHTOWER_NOTIFICATION_URL: ntfy://172.28.4.10/${NTFY_TOPIC}?scheme=http env_file: - .env security_opt: - no-new-privileges:true
networks: ntfy_internal: external: trueWATCHTOWER_SCHEDULE: "0 0 9 * * *" comprueba actualizaciones todos los días a las 9:00 de la mañana. Ajusta la expresión cron a tu gusto.
Usamos la IP fija de ts-ntfy (172.28.4.10) y ?scheme=http porque, dentro de la red interna ntfy_internal, hablamos directamente en HTTP plano — el HTTPS de Tailscale solo hace falta para el tráfico que sale hacia tu móvil, no para esta comunicación interna entre contenedores del mismo host.
Levanta el servicio
Sección titulada «Levanta el servicio»Ventana de terminal
docker compose up -dComprueba los logs para confirmar que ha detectado el resto de contenedores y que la notificación de arranque te ha llegado a la app de ntfy:
Ventana de terminal
docker compose logs -f watchtowerA partir de ahora, cada vez que Nextcloud, Vaultwarden o Immich tengan una imagen nueva disponible, te llegará una notificación push a tu propio servidor — y decides tú cuándo y cómo actualizar cada uno.
Qué hemos conseguido
Sección titulada «Qué hemos conseguido»- Vaultwarden desplegado como gestor de contraseñas autoalojado, con registro cerrado tras crear tu cuenta.
- Immich desplegado con base de datos vectorial, caché Redis y motor de machine learning propios.
- Tu propio servidor de notificaciones ntfy, sin depender de ningún servicio de terceros.
- Watchtower vigilando todos los contenedores del homelab y avisándote a través de tu ntfy cuando hay actualizaciones, sin aplicarlas por su cuenta.
- Todos con subdominio propio y HTTPS automático vía Tailscale, sin abrir un solo puerto en el router.
- El mismo patrón de carpeta + sidecar Tailscale + IPs fijas que ya conoces del episodio 2, aplicado a servicios adicionales.
Con esto cerramos, por ahora, la serie de “Tu primer HomeLab (powered by HP)”. Si te ha servido, ya sabes dónde encontrar el resto de episodios: en la documentación completa en ciprigeek.com.