Ir al contenido

Fotos y contraseñas: Immich y Vaultwarden — Tu primer HomeLab (powered by HP) #03

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 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.

Ventana de terminal

Ventana de terminal
mkdir -p ~/homelab/vaultwarden
cd ~/homelab/vaultwarden

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.

Ventana de terminal

Ventana de terminal
vim .env
Ventana de terminal
TZ=Europe/Madrid
TS_AUTHKEY=tskey-auth-xxxxxxxxxx

Ventana de terminal

Ventana de terminal
vim docker-compose.yml
services:
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:true

Como 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.

Vaultwarden sirve HTTP plano en el puerto 80 internamente — a diferencia de Nextcloud, no necesitamos https+insecure://, con http:// es suficiente.

Ventana de terminal

Ventana de terminal
mkdir -p ts-vaultwarden-config
vim ts-vaultwarden-config/serve.json
{
"TCP": {
"443": {
"HTTPS": true
}
},
"Web": {
"vaultwarden.tu-tailnet.ts.net:443": {
"Handlers": {
"/": {
"Proxy": "http://127.0.0.1:80"
}
}
}
}
}

Ventana de terminal

Ventana de terminal
docker compose up -d

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

Ventana de terminal
vim .env

Cambia SIGNUPS_ALLOWED a "false" (si ya estaba así, confirma que sigue estándolo) y reinicia:

Ventana de terminal
docker compose up -d app

Immich 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.

Ventana de terminal

Ventana de terminal
mkdir -p ~/homelab/immich
cd ~/homelab/immich

Mismo proceso que con los servicios anteriores en login.tailscale.com/admin/settings/keys.

Ventana de terminal

Ventana de terminal
vim .env
Ventana de terminal
TZ=Europe/Madrid
UPLOAD_LOCATION=./library
DB_DATA_LOCATION=./postgres
DB_PASSWORD=cambia_esto
DB_USERNAME=postgres
DB_DATABASE_NAME=immich
# IPs fijas de la red interna — imprescindibles porque immich-server
# comparte red con el sidecar de Tailscale y no puede resolver nombres
DB_HOSTNAME=172.28.2.10
REDIS_HOSTNAME=172.28.2.11
IMMICH_VERSION=release
TS_AUTHKEY=tskey-auth-xxxxxxxxxx

Ventana de terminal

Ventana de terminal
vim docker-compose.yml
services:
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

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.

Immich sirve HTTP plano en el puerto 2283 — como con Vaultwarden, no hace falta https+insecure://.

Ventana de terminal

Ventana de terminal
mkdir -p ts-immich-config
vim ts-immich-config/serve.json
{
"TCP": {
"443": {
"HTTPS": true
}
},
"Web": {
"immich.tu-tailnet.ts.net:443": {
"Handlers": {
"/": {
"Proxy": "http://127.0.0.1:2283"
}
}
}
}
}

Ventana de terminal

Ventana de terminal
docker compose up -d

La 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

Ventana de terminal
docker compose ps
docker compose logs -f immich-server

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.


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.

Ventana de terminal

Ventana de terminal
mkdir -p ~/homelab/ntfy
cd ~/homelab/ntfy

Mismo proceso de siempre en login.tailscale.com/admin/settings/keys.

Ventana de terminal

Ventana de terminal
vim .env
Ventana de terminal
TZ=Europe/Madrid
TS_AUTHKEY=tskey-auth-xxxxxxxxxx
NTFY_TOPIC=homelab-updates

NTFY_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.

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

Ventana de terminal
vim docker-compose.yml
services:
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/24

ntfy sirve HTTP plano en el puerto 80, como Vaultwarden.

Ventana de terminal

Ventana de terminal
mkdir -p ts-ntfy-config
vim ts-ntfy-config/serve.json
{
"TCP": {
"443": {
"HTTPS": true
}
},
"Web": {
"ntfy.tu-tailnet.ts.net:443": {
"Handlers": {
"/": {
"Proxy": "http://127.0.0.1:80"
}
}
}
}
}

Ventana de terminal

Ventana de terminal
docker compose up -d

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:

  1. Abre la app → + (añadir suscripción)
  2. Toca Use another server
  3. Introduce https://ntfy.tu-tailnet.ts.net como servidor y homelab-updates (el valor de NTFY_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.

Ventana de terminal

Ventana de terminal
mkdir -p ~/homelab/watchtower
cd ~/homelab/watchtower

Ventana de terminal

Ventana de terminal
vim .env
Ventana de terminal
NTFY_TOPIC=homelab-updates

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

Ventana de terminal
vim docker-compose.yml
services:
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: true

WATCHTOWER_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.

Ventana de terminal

Ventana de terminal
docker compose up -d

Comprueba 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

Ventana de terminal
docker compose logs -f watchtower

A 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.


  • 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.