Cómo instalar Vaultwarden en un VPS con Docker
Instala Vaultwarden en un VPS con Docker y clientes Bitwarden. Configura HTTPS, token de administrador, Fail2ban y copias de seguridad probadas para proteger tus datos.
Qué está construyendo
Un gestor de contraseñas bajo su control: Vaultwarden ejecutándose en un contenedor pequeño detrás de un proxy inverso que termina HTTPS, con las aplicaciones oficiales de Bitwarden en el teléfono, el portátil y el navegador configuradas para conectarse a él. Vaultwarden vuelve a implementar la API del servidor de Bitwarden en Rust y usa el mismo protocolo que bitwarden.com, por lo que todos los clientes oficiales funcionan con él sin modificaciones. Sin embargo, necesita aproximadamente 100 MB de RAM, frente a la pila oficial de varios contenedores.
La instalación se reduce a una docena de líneas de Compose. Los tres aspectos que realmente importan y que suelen fallar son los siguientes: TLS debe estar disponible antes de abrir el almacén web, los registros públicos deben cerrarse en cuanto exista su propia cuenta y el volumen de datos debe incluirse en copias de seguridad cuya restauración se haya probado, porque ese único directorio contiene todas las contraseñas que posee.
Requisitos previos y problemas importantes
- Un VPS con Docker Engine y el complemento Compose, en una instancia KVM nueva de Ubuntu 24.04 con acceso de root o sudo. 512 MB de RAM son suficientes; 1 GB ofrece un margen cómodo. Es uno de los servicios más ligeros que puede ejecutar y aparece cerca del principio de la lista de servicios que merece la pena alojar uno mismo. Aun así, dimensione la instancia según los demás servicios que compartan el sistema: alojar una biblioteca de fotos autoalojada como PhotoPrism o Immich en el mismo VPS eleva el mínimo de RAM a varios gigabytes, mientras que Vaultwarden apenas lo modifica. El mismo cálculo se aplica a los frontends multimedia que añada más adelante, ya que convertir una biblioteca de Jellyfin en un videoclub de los años 90 que se pueda recorrer implica otro contenedor permanente y margen de RAM para la transcodificación dentro del mismo presupuesto.
- Un dominio con un registro A (y AAAA si dispone de IPv6) que apunte
vault.example.comal VPS. El certificado TLS se emite para este nombre exacto, por lo que DNS debe resolverlo antes de comenzar. - Los puertos 80 y 443 abiertos a Internet y terminados por el proxy inverso, nunca directamente por Vaultwarden. El puerto 80 se utiliza sólo para el desafío del certificado ACME y para la redirección de HTTP a HTTPS.
- El problema más importante es el siguiente: los clientes de Bitwarden se niegan a comunicarse con un servidor que no use HTTPS. No existe la opción de «probarlo primero mediante http»; ese procedimiento no funciona por un motivo concreto que se explica a continuación.
Por qué Vaultwarden y no la pila oficial de Bitwarden
Los mismos clientes con una fracción del consumo. La versión oficial de Bitwarden para autoalojamiento se distribuye como un conjunto de contenedores (MSSQL, Nginx, Identity, Api, Admin y otros) y requiere aproximadamente 2 GB de RAM. Vaultwarden es un único binario que almacena todo en una base de datos SQLite de forma predeterminada y consume unas pocas decenas de megabytes en reposo. Para una persona, una familia o un equipo pequeño, es la opción evidente. Además, como implementa fielmente la API de Bitwarden, los datos siguen siendo portables entre Vaultwarden y bitwarden.com.
Lo que se pierde es la mayor parte de las funciones empresariales: no hay aprovisionamiento SCIM, aunque en 1.35.0 se incorporó compatibilidad experimental con SSO mediante OpenID Connect. Además, usted es el operador, por lo que debe encargarse de aplicar parches, configurar HTTPS y realizar copias de seguridad. Esta guía trata esas tres tareas.
Por qué HTTPS no es opcional
La bóveda web de Bitwarden y las extensiones del navegador derivan las claves de cifrado en el navegador mediante la Web Crypto API (window.crypto.subtle). Los navegadores sólo exponen crypto.subtle en un contexto seguro: HTTPS o el caso especial de http://localhost. Sobre http://vault.example.com simple, undefined, por lo que, en cuanto la aplicación deriva una clave, genera una excepción y la consola muestra:
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'importKey')La página se bloquea o muestra un error criptográfico genérico, y ningún usuario puede iniciar sesión. Los clientes de escritorio, móviles y de navegador realizan su propia comprobación contra una URL autohospedada; si el endpoint usa http o no es accesible, la rechazan con:
This is not a recognized Bitwarden server. You may need to check with your provider or update your server.Ambos casos tienen la misma causa: no hay un HTTPS válido. Por eso configuramos TLS primero y nunca abrimos la bóveda mediante http, ni siquiera una vez para comprobarla rápidamente.
Paso 1, DNS y el proxy inverso (TLS primero)
Apunte el registro a su VPS y confirme que resuelve a la dirección correcta:
dig +short vault.example.comLa línea que muestra debe ser la IP de su VPS. Si está vacía o es incorrecta, corrija el DNS y espere a que transcurra el TTL. La emisión del certificado falla si el nombre no resuelve.
Para el frontal HTTPS, esta guía usa Traefik. Traefik emite y renueva automáticamente los certificados de Let's Encrypt y se integra directamente con Compose. Si todavía no lo ejecuta, siga primero la configuración del proxy inverso Traefik y TLS automático. Esta configuración crea una red Docker externa (proxy más abajo) y un resolver ACME (letsencrypt) al que se conecta el servicio Vaultwarden. nginx sin TLS terminado y con un certificado emitido manualmente funciona igual desde el lado de Vaultwarden.
¿Prefiere nginx y Certbot en lugar de Traefik? Conecte Vaultwarden a 127.0.0.1:8080 (añada ports: ["127.0.0.1:8080:80"] al servicio y elimine las etiquetas de Traefik), emita después un certificado y configure el proxy hacia él. La parte del certificado se explica en cómo emitir certificados de Let's Encrypt con Certbot y nginx. El elemento adicional importante es la actualización de WebSocket en la ruta de notificaciones:
server {
listen 443 ssl;
server_name vault.example.com;
client_max_body_size 525M;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}Observe la línea X-Real-IP. Permite que Fail2ban vea posteriormente al atacante real en lugar de 127.0.0.1. Todo lo demás de esta guía es igual, tanto si Traefik como si nginx está delante.
Paso 2: el archivo de Compose
Cree primero el directorio del proyecto. Esta guía usa /opt/vaultwarden, que hace predecibles el nombre del proyecto de Compose y, por tanto, el volumen de datos vaultwarden_vw-data; los pasos de Fail2ban y de copia de seguridad que aparecen más abajo dependen de ese nombre exacto.
sudo mkdir -p /opt/vaultwarden
cd /opt/vaultwardenCree un .env para el secreto de administración y el archivo de Compose en ese directorio.
# .env
ADMIN_TOKEN=paste-a-strong-token-hereGenere ese token con openssl rand -base64 48 y péguelo en el archivo. (Más adelante se explica una forma con un hash más resistente; para empezar, basta con una cadena aleatoria larga).
# docker-compose.yml
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
environment:
DOMAIN: "https://vault.example.com"
SIGNUPS_ALLOWED: "true" # closed in Step 4, keep true just to register
ADMIN_TOKEN: "${ADMIN_TOKEN}"
IP_HEADER: "X-Forwarded-For" # X-Real-IP if your proxy sends that instead
LOG_FILE: "/data/vaultwarden.log"
LOG_LEVEL: "warn"
volumes:
- vw-data:/data
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.vw.rule=Host(`vault.example.com`)"
- "traefik.http.routers.vw.entrypoints=websecure"
- "traefik.http.routers.vw.tls.certresolver=letsencrypt"
- "traefik.http.services.vw.loadbalancer.server.port=80"
volumes:
vw-data:
networks:
proxy:
external: trueHay dos aspectos de este archivo que sostienen todo el diseño. No existe ninguna asignación de ports:, por lo que sólo se puede acceder a Vaultwarden a través de Traefik y su TLS; publicar su puerto en el host es la forma de servir el almacén por http por accidente. Además, DOMAIN debe ser la URL HTTPS pública completa: se incorpora a los enlaces de los adjuntos, a la autenticación de dos factores de WebAuthn y al endpoint de notificaciones, por lo que un valor incorrecto o http rompe esas funciones aunque el sitio cargue. La etiqueta latest es una excepción deliberada a la regla habitual de no usar nunca latest. Vaultwarden publica sus versiones estables como una única imagen continua, con :testing como canal independiente de versiones preliminares, así que actualice de forma intencionada y revise las notas de la versión antes de descargar la imagen. Esta excepción es limitada: para la mayoría de los contenedores de larga duración es preferible fijar una etiqueta exacta. Eso es lo que mantiene predecible un agente siempre activo alojado en el mismo VPS entre reinicios y descargas de imágenes.
Inícielo y supervise el registro:
docker compose up -d
docker compose logs -f vaultwardenUn inicio correcto termina con una línea como Rocket has launched from http://0.0.0.0:80. Espere unos segundos para que Traefik obtenga el certificado y cargue https://vault.example.com. Debería aparecer el almacén web de Bitwarden con un candado válido y sin ninguna advertencia de certificado.
Paso 3, un ADMIN_TOKEN seguro y la trampa de $$
ADMIN_TOKEN protege /admin, el panel que puede leer todos los usuarios y ajustes de la instancia. Trátelo como una contraseña de root. Hay dos formas válidas.
La forma sencilla es la cadena aleatoria que ya generó con openssl rand -base64 48. Como base64 nunca contiene $, puede copiarla directamente en .env sin ningún escape.
La forma reforzada es un hash PHC de Argon2. Así, el token en texto plano nunca se almacena en el disco. Genere uno con la misma imagen:
docker run --rm -it vaultwarden/server /vaultwarden hash --preset owaspEl comando solicita el valor dos veces y muestra una cadena que empieza por $argon2id$v=19$.... Esta es la trampa que puede hacerle perder una hora: Docker Compose interpreta $ como interpolación de variables. Por eso debe duplicar cada $ como $$ al pegar el hash en el archivo Compose. Colóquelo directamente debajo de environment:, no mediante .env, y no lo encierre entre comillas:
environment:
ADMIN_TOKEN: $$argon2id$$v=19$$m=19456,t=2,p=1$$c29tZXNhbHQ$$RdescudvJCsgt3ub+b+dWRWJTmaaJObGSi deja los signos $ simples, Compose muestra la advertencia The "argon2id" variable is not set y deja vacío el token. Entonces /admin rechaza su contraseña correcta. Ejecute docker compose up -d y guarde en su propio gestor de contraseñas el texto plano que introdujo en la solicitud.
Paso 4: registre su cuenta y cierre el acceso
Con SIGNUPS_ALLOWED: "true", abra https://vault.example.com, haga clic en Create account y regístrese con su correo electrónico y una contraseña maestra segura. Esta contraseña maestra no se puede recuperar y no existe ninguna opción para restablecerla. Guárdela primero en un lugar duradero.
Ahora cierre el acceso. Edite el archivo Compose para desactivar los registros:
SIGNUPS_ALLOWED: "false"Vuelva a aplicar la configuración con docker compose up -d. Esto no es una medida de seguridad que pueda posponer. Si permanece abierto, cualquiera que encuentre la URL, incluidos los rastreadores, puede crear una cuenta en su servidor. No podrán leer su bóveda, pero consumirán recursos y convertirán su instancia privada en un servicio abierto. La señal de que lo dejó activado es que /admin muestra cuentas que usted no creó.
Para añadir más adelante a familiares o compañeros de equipo sin volver a abrir los registros públicos, use el botón Invite User en /admin. Esta opción requiere configurar SMTP para que el invitado reciba su enlace.
Paso 5: acceder a /admin
Abra https://vault.example.com/admin e introduzca el token de administración en texto plano (la cadena aleatoria o la contraseña que convirtió en hash, no el hash). Desde ahí puede listar usuarios, ajustar la configuración, enviar un correo de prueba y crear una instantánea de la base de datos.
Si la página devuelve 404 Not Found, ADMIN_TOKEN está vacío o no está definido, lo que desactiva por completo el panel. Esta también es una opción válida si nunca lo necesita. Si la página carga, pero rechaza el token, consulte el problema de escape de $$ en la lista de fallos siguiente. ¿Ha olvidado el token? No hay ningún aviso de recuperación. Edite .env o el archivo de Compose, establezca uno nuevo y docker compose up -d.
Paso 6: conectar los clientes de Bitwarden
Todos los clientes oficiales pueden conectarse a un servidor autohospedado. Instale el cliente de Bitwarden para escritorio, móvil o navegador desde las tiendas habituales. No necesita una compilación especial de Vaultwarden.
Antes de iniciar sesión, abra el icono de configuración en la pantalla de inicio de sesión (con la etiqueta Autohospedado o Región → Autohospedado), establezca URL del servidor en https://vault.example.com y guarde los cambios. Después, inicie sesión con el correo electrónico y la contraseña maestra que registró. El cliente debería conectarse de inmediato y ofrecer rellenar y guardar las credenciales.
Si un cliente muestra This is not a recognized Bitwarden server. You may need to check with your provider or update your server., la URL es incorrecta, usa http o el certificado no es de confianza. Compruebe de nuevo que https://vault.example.com se cargue correctamente primero en un navegador. Las actualizaciones lentas en otros dispositivos se deben a las notificaciones push de WebSocket, que se explican más adelante.
Paso 7, una jail de Fail2ban para el endpoint de inicio de sesión
Vaultwarden registra cada inicio de sesión fallido en el archivo definido por LOG_FILE, exactamente lo que necesita un mecanismo de protección contra ataques de fuerza bruta. Si todavía no ejecuta Fail2ban, la instalación y los conceptos básicos se explican en la guía para reforzar SSH con Fail2ban; aquí añadiremos una jail para Vaultwarden.
Primero, busque dónde se encuentra el volumen con nombre en el host, para que Fail2ban pueda leer el registro:
docker volume inspect vaultwarden_vw-data --format '{{ .Mountpoint }}'El comando muestra algo parecido a /var/lib/docker/volumes/vaultwarden_vw-data/_data; dentro de él, el registro está en vaultwarden.log. Cree el filtro:
# /etc/fail2ban/filter.d/vaultwarden.conf
[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =Y la jail:
# /etc/fail2ban/jail.d/vaultwarden.local
[vaultwarden]
enabled = true
filter = vaultwarden
logpath = /var/lib/docker/volumes/vaultwarden_vw-data/_data/vaultwarden.log
banaction = iptables-allports
chain = DOCKER-USER
maxretry = 5
findtime = 600
bantime = 3600Recargue la configuración con sudo systemctl restart fail2ban y confirme el resultado con sudo fail2ban-client status vaultwarden.
Tres detalles de Docker determinan si esta protección funciona. Primero, si el registro muestra IP: 127.0.0.1 o la dirección de su proxy en cada intento fallido, Vaultwarden bloqueará el proxy. Configure IP_HEADER con la cabecera que realmente envía su proxy: X-Forwarded-For para Traefik, X-Real-IP para el bloque de nginx anterior y CF-Connecting-IP detrás de Cloudflare. Segundo, la cadena de iptables correcta depende del proxy. Si Traefik se ejecuta como un contenedor con puertos publicados, el tráfico atraviesa la ruta FORWARD de Docker, por lo que el bloqueo debe aplicarse en DOCKER-USER, como se muestra arriba. Sin embargo, si eligió la opción de nginx en el host del Paso 1, las conexiones terminan en nginx mediante la cadena INPUT del host y un bloqueo en DOCKER-USER nunca las verá. En ese caso, elimine la línea chain = DOCKER-USER para que Fail2ban use la cadena predeterminada INPUT. Tercero, use banaction = iptables-allports en lugar del valor predeterminado basado en el puerto. Esta jail no define ningún puerto, y un bloqueo para todos los puertos en DOCKER-USER impide correctamente que el atacante acceda a todos los servicios publicados en el equipo.
Paso 8: haga una copia de seguridad del almacén y restáurelo
El volumen vw-data es su gestor de contraseñas. Contiene db.sqlite3 (todas las entradas), los directorios attachments/ y sends/, los archivos rsa_key.* que firman las sesiones de inicio de sesión y config.json del panel de administración. Una copia de seguridad que omita cualquiera de estos elementos fallará cuando la necesite.
Copiar db.sqlite3 mientras Vaultwarden está escribiendo puede capturar un archivo incompleto y corrupto. Por eso, haga una instantánea en frío. La interrupción dura unos segundos:
#!/usr/bin/env bash
set -euo pipefail
STAMP=$(date +%F)
DEST=/root/vw-backups
VOL=$(docker volume inspect vaultwarden_vw-data --format '{{ .Mountpoint }}')
mkdir -p "$DEST"
docker compose -f /opt/vaultwarden/docker-compose.yml stop vaultwarden
tar czf "$DEST/vw-$STAMP.tgz" -C "$VOL" .
docker compose -f /opt/vaultwarden/docker-compose.yml start vaultwardenEjecútelo cada noche desde cron y copie .tgz fuera del servidor. Una copia que sólo existe en el servidor que está protegiendo no es una copia de seguridad. La forma adecuada de enviarla es una copia de seguridad nocturna con restic a otro servidor o a un almacenamiento de objetos. restic cifra el archivo y deduplica las instantáneas repetidas automáticamente. El botón Backup Database del panel de administración permite crear una instantánea práctica del archivo SQLite, pero sólo incluye la base de datos y omite los archivos adjuntos y las claves.
Ahora viene el procedimiento que distingue una copia de seguridad real de una suposición: restáurela una vez y compruebe que funciona:
mkdir -p /tmp/vw-restore
tar xzf /root/vw-backups/vw-2026-07-15.tgz -C /tmp/vw-restore
docker run --rm -p 127.0.0.1:8888:80 -v /tmp/vw-restore:/data vaultwarden/serverDesde su portátil, cree un túnel hasta el servidor con ssh -L 8888:127.0.0.1:8888 you@your-vps y abra http://localhost:8888. Como localhost es un contexto seguro, crypto.subtle está disponible y el almacén se descifra mediante http sin cifrar en este caso, el único lugar donde está permitido. Inicie sesión con su contraseña maestra y confirme que sus entradas están presentes. Si lo están, la base de datos, las claves RSA y la contraseña maestra se han restaurado correctamente, y podrá reconstruirlo en un VPS nuevo en cuestión de minutos. Detenga el contenedor con Ctrl-C y elimine /tmp/vw-restore. Mantenga esta costumbre de crear túneles para cualquier otra interfaz de administración del servidor que nunca deba quedar expuesta a Internet. Así también accedería a un escáner de seguridad open-kritt autoalojado en el puerto 5173.
Modos de fallo y cadenas que verá
Cannot read properties of undefined (reading 'importKey') en la consola del navegador. El vault se cargó mediante http, por lo que crypto.subtle no está definido. Acceda a él sólo mediante https:// y añada la redirección de HTTP a HTTPS en el proxy.
This is not a recognized Bitwarden server... en un cliente. Server URL usa http, está mal escrita o el certificado no es de confianza. Confirme que https://vault.example.com muestra un candado válido y vuelva a introducirlo en la configuración self-hosted del cliente.
/admin rechaza la contraseña correcta. El hash de Argon2 perdió su escape. Cada $ debe ser $$ en Compose. También es posible que haya introducido el hash en lugar del texto plano que representa.
Sincronización lenta entre dispositivos; la consola muestra WebSocket connection to 'wss://vault.example.com/notifications/hub' failed. El proxy no reenvía las cabeceras Upgrade/Connection. Traefik lo hace automáticamente. nginx necesita las dos líneas de upgrade del paso 1. El vault sigue funcionando, pero sólo sincroniza al abrirse. El puerto dedicado antiguo 3012 desapareció desde v1.31.0, por lo que no se necesita una ruta WebSocket independiente.
Fail2ban informa de un bloqueo, pero el atacante sigue conectándose. Está bloqueando 127.0.0.1 porque IP_HEADER es incorrecto, o el bloqueo está en la cadena iptables equivocada. Configure chain = DOCKER-USER y banaction = iptables-allports.
Actualizaciones
Descargue la imagen nueva y vuelva a crear el contenedor. El volumen con nombre y todos sus datos se conservan:
docker compose pull
docker compose up -dVaultwarden publica versiones con frecuencia. Consulte las notas de lanzamiento del proyecto en lugar de fijar una versión de parche, ya que algunas versiones incluyen instrucciones de migración. Cree una copia de seguridad reciente antes de cualquier actualización importante. Puede volver a una versión anterior restaurando el tarball en un volumen nuevo.
FAQ
¿Vaultwarden es lo mismo que Bitwarden?
Es un servidor independiente compatible, no el servidor oficial. Vaultwarden vuelve a implementar la API del servidor de Bitwarden en Rust, por lo que los clientes oficiales de escritorio, móviles, de navegador y CLI funcionan con él y consumen una fracción de los recursos de la pila oficial. El formato de la bóveda es el mismo, así que puede migrar en cualquiera de las dos direcciones mediante exportación e importación.
¿Realmente necesito HTTPS o puedo ejecutarlo mediante http en mi LAN?
Necesita HTTPS para cualquier cosa que no sea una prueba de localhost. La bóveda web y las extensiones de Bitwarden usan la API Web Crypto del navegador, que sólo está disponible en un contexto seguro. Por eso, mediante http sin cifrar, el cliente muestra Cannot read properties of undefined y nunca inicia sesión. La única dirección http que funciona es http://localhost. Por eso la prueba de restauración del paso 8 usa un túnel SSH.
¿Cómo impido que personas desconocidas se registren en mi servidor?
Establezca SIGNUPS_ALLOWED: "false" en el archivo Compose y ejecute docker compose up -d inmediatamente después de crear su propia cuenta. A partir de ese momento, añada usuarios mediante el botón Invitar usuario de /admin. Necesita configurar SMTP para que reciban el enlace de invitación. Revise de vez en cuando la lista de usuarios administradores para confirmar que no hayan aparecido cuentas inesperadas.
¿Cómo hago una copia de seguridad de mi bóveda de Vaultwarden?
Detenga brevemente el contenedor y archive todo el volumen vw-data, db.sqlite3, attachments/, sends/, config.json y los archivos rsa_key.*. Después, copie el archivo fuera del servidor, preferiblemente mediante un cron nocturno. Copiar el archivo SQLite mientras el servidor está en ejecución puede producir una instantánea dañada, así que haga una copia en frío. Lo más importante es restaurarla una vez en un contenedor desechable e iniciar sesión, para comprobar que la copia de seguridad es válida antes de depender de ella.
¿Es realmente seguro alojar mis contraseñas por mi cuenta?
Sí, si hace las tres cosas que cubre esta guía: HTTPS real, registros cerrados junto con un token de administrador seguro y copias de seguridad probadas. La bóveda se cifra en el cliente con su contraseña maestra, por lo que ni siquiera el servidor ve sus contraseñas en texto claro. Un db.sqlite3 robado no sirve sin ella. La contrapartida es que los parches y las copias de seguridad pasan a ser responsabilidad suya. Por eso Fail2ban y el procedimiento de restauración no son opcionales aquí. Cuando todo esté configurado, un análisis más detallado de los puntos en los que realmente se puede atacar una bóveda alojada por cuenta propia es el siguiente paso útil. Como las entradas están cifradas en el cliente, lo que queda por proteger es el token de administrador y el archivo de copia de seguridad.