instalar vaultwarden en vps con docker
Guía para desplegar Vaultwarden en un VPS con Docker. Configuración de HTTPS, gestión de admin token, uso de Fail2ban y estrategias de backup para datos.
Qué vas a construir
Un gestor de contraseñas bajo tu propio control: Vaultwarden ejecutándose en un contenedor ligero detrás de un reverse proxy que termina la conexión HTTPS, con las aplicaciones oficiales de Bitwarden en tu teléfono, laptop y navegador apuntando a este servidor. Vaultwarden reimplementa la API del servidor de Bitwarden en Rust y utiliza el mismo protocolo que bitwarden.com, por lo que todos los clientes oficiales funcionan sin cambios. Su ventaja es que consume aproximadamente 100 MB de RAM, a diferencia del stack oficial de múltiples contenedores.
La instalación consiste en una docena de líneas de Compose. Los tres factores críticos —y los que suelen fallar— son estos: debe existir TLS antes de acceder al web vault, el registro público debe desactivarse en cuanto crees tu propia cuenta, y el volumen de datos debe respaldarse y probarse mediante restauraciones, ya que ese directorio contiene todas tus contraseñas.
Requisitos previos y dificultades reales
- Un VPS con Docker Engine y el plugin Compose, en un servidor Ubuntu 24.04 KVM limpio con acceso root o sudo. 512 MB de RAM son suficientes; 1 GB es ideal. Este es uno de los servicios más ligeros disponibles; se encuentra entre los primeros de la lista de servicios que vale la pena auto-alojar.
- Un dominio con un registro A (y AAAA si tiene IPv6) apuntando
vault.example.comal VPS. El certificado TLS se emite para este nombre exacto, por lo que el DNS debe resolver antes de comenzar. - Puertos 80 y 443 abiertos a internet, gestionados por su reverse proxy — nunca directamente por Vaultwarden. El puerto 80 se utiliza solo para el desafío de certificado ACME y la redirección de HTTP a HTTPS.
- El principal inconveniente inicial: los clientes de Bitwarden no se conectan a un servidor que no use HTTPS. No es posible "probarlo mediante HTTP primero"; ese método no funciona por una razón específica que se explica a continuación.
Por qué Vaultwarden y no el stack oficial de Bitwarden
Mismos clientes, una fracción del peso. El stack oficial de Bitwarden para self-hosting se distribuye como un conjunto de contenedores (MSSQL, Nginx, Identity, Api, Admin y más) y requiere aproximadamente 2 GB de RAM. Vaultwarden es un único binario que almacena todo en una base de datos SQLite por defecto y consume apenas unos pocos decenas de megabytes en reposo. Para una persona, una familia o un equipo pequeño es la opción lógica, y debido a que implementa la API de Bitwarden fielmente, sus datos son portátiles entre este y bitwarden.com.
Lo que pierde es la mayor parte de la superficie empresarial: no tiene aprovisionamiento SCIM (aunque el soporte experimental para OpenID Connect SSO se incluyó en la versión 1.35.0), y usted es el operador, por lo que el parcheo, HTTPS y los backups son su responsabilidad. Esta guía cubre esas tres tareas.
Por qué HTTPS no es opcional
El web vault de Bitwarden y sus extensiones de navegador derivan las claves de cifrado en el navegador usando la Web Crypto API (window.crypto.subtle). Los navegadores solo exponen crypto.subtle en un secure context — HTTPS, o el caso especial de http://localhost. Sobre http://vault.example.com plano es undefined, por lo que en cuanto la aplicación deriva una clave, esta falla y la consola muestra:
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'importKey')La página se bloquea o muestra un error de crypto genérico, y no se puede iniciar sesión. Los clientes de escritorio, móviles y de navegador ejecutan su propia verificación contra una URL self-hosted; si el endpoint es http (o es inalcanzable), el cliente falla con:
This is not a recognized Bitwarden server. You may need to check with your provider or update your server.Ambos tienen la misma causa: falta de HTTPS válido. Por lo tanto, configure TLS primero y nunca abra el vault sobre http, ni siquiera una vez para una consulta rápida.
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 impresa debe ser la IP de su VPS. Si está vacía o es incorrecta, corrija el DNS y espere a que expire el TTL; la emisión del certificado falla si el nombre no resuelve.
Para el front end HTTPS, esta guía utiliza Traefik, que emite y renueva certificados de Let's Encrypt automáticamente y se integra directamente con Compose. Si aún no lo utiliza, siga primero la configuración del proxy inverso Traefik y TLS automático; este crea una red de Docker externa (proxy abajo) y un resolver ACME (letsencrypt) al cual se conecta el servicio Vaultwarden. Un nginx estándar con un certificado manual funciona de la misma manera para Vaultwarden.
¿Prefiere nginx y Certbot en lugar de Traefik? Coloque Vaultwarden en 127.0.0.1:8080 (añada ports: ["127.0.0.1:8080:80"] al servicio y elimine las etiquetas de Traefik), luego emita un certificado y realice el proxy hacia él. La parte del certificado se explica en emisión de certificados Let's Encrypt con Certbot y nginx. El paso adicional crítico 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; es lo que permite que Fail2ban identifique al atacante real en lugar de 127.0.0.1. Todo lo demás en esta guía es idéntico, independientemente de si Traefik o nginx actúa como proxy frontal.
Paso 2 — el archivo Compose
Cree primero el directorio del proyecto. Esta guía utiliza /opt/vaultwarden, lo que hace que el nombre del proyecto Compose —y, por tanto, el volumen de datos, vaultwarden_vw-data— sea predecible; los pasos de Fail2ban y de respaldo detallados a continuación dependen de ese nombre exacto.
sudo mkdir -p /opt/vaultwarden
cd /opt/vaultwardenCree un .env para el secreto del administrador y el archivo Compose en ese directorio.
# .env
ADMIN_TOKEN=paste-a-strong-token-hereGenere ese token con openssl rand -base64 48 y péguelo. (El siguiente apartado trata sobre una forma de hash más segura; una cadena aleatoria larga es suficiente para empezar).
# 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: trueDos aspectos de este archivo definen todo el diseño. No hay mapeo de ports:, por lo que Vaultwarden solo es accesible a través de Traefik y su TLS; publicar su puerto en el host es la causa por la que los usuarios exponen el vault por http accidentalmente. Además, DOMAIN debe ser la URL pública completa con HTTPS: se integra en los enlaces de archivos adjuntos, en WebAuthn 2FA y en el endpoint de notificaciones; un valor incorrecto o en http romperá estas funciones aunque el sitio cargue. La etiqueta latest es una excepción deliberada a la regla habitual de nunca-latest —Vaultwarden distribuye sus versiones estables como una única imagen continua, con :testing como el canal de pre-lanzamiento separado— así que actualice de forma intencionada y revise las notas de la versión antes de hacer el pull.
Inicie el servicio y observe el log:
docker compose up -d
docker compose logs -f vaultwardenUn inicio correcto termina con una línea similar a Rocket has launched from http://0.0.0.0:80. Dé a Traefik unos segundos para obtener el certificado y luego cargue https://vault.example.com; debería ver el web vault de Bitwarden con un candado válido y sin advertencias de certificado.
Paso 3 — un ADMIN_TOKEN robusto y la trampa del $$
ADMIN_TOKEN protege /admin, el panel que puede leer cada usuario y configuración de su instancia; trátelo como una contraseña de root. Existen dos formas.
La forma simple es la cadena aleatoria que ya generó con openssl rand -base64 48. Debido a que base64 nunca contiene un $, se inserta directamente en .env sin necesidad de escaping.
La forma robusta es un hash Argon2 PHC, por lo que el token en texto plano nunca se almacena en disco. Genere uno usando la misma imagen:
docker run --rm -it vaultwarden/server /vaultwarden hash --preset owaspEl comando solicita la entrada dos veces e imprime una cadena que comienza con $argon2id$v=19$.... Esta es la trampa que hace perder una hora a los usuarios: Docker Compose trata a $ como interpolación de variables, por lo que debe duplicar cada $ a $$ al pegar el hash en el archivo Compose. Colóquelo directamente bajo 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 emite un aviso The "argon2id" variable is not set y deja el token vacío, lo que provoca que /admin rechace su contraseña correcta. Ejecute docker compose up -d y guarde el texto plano que escribió en el prompt en su propio gestor de contraseñas.
Paso 4 — registre su cuenta y luego 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 —no existe la opción de restablecimiento—, así que guárdela en un lugar seguro primero.
Ahora cierre el acceso. Edite el archivo Compose para desactivar los registros:
SIGNUPS_ALLOWED: "false"Vuelva a aplicar los cambios con docker compose up -d. Esto no es un endurecimiento crítico que pueda posponer. Si lo deja abierto, cualquier persona que encuentre la URL —incluyendo los bots de rastreo— podrá 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 público. La señal de que lo dejó abierto es: /admin muestra cuentas que usted nunca creó.
Para añadir familiares o compañeros de equipo más adelante sin reactivar el registro público, use el botón Invite User en /admin; esa función requiere tener configurado SMTP para que el invitado reciba su enlace.
Paso 5 — acceder a /admin
Navegue hasta https://vault.example.com/admin e introduzca el token de administrador en texto plano (la cadena aleatoria, o la contraseña que haya hasheado; no el hash en sí). Dentro puede listar usuarios, ajustar la configuración, enviar un correo de prueba y realizar una captura de la base de datos.
Si la página devuelve 404 Not Found, es porque ADMIN_TOKEN está vacío o no está configurado, lo que desactiva el panel por completo; esto es una opción válida si nunca necesita usarlo. Si la página carga pero rechaza su token, consulte el error de escape en $$ en la lista de fallos de abajo. ¿Olvidó el token? No existe un aviso de recuperación; edite .env o el archivo Compose, establezca uno nuevo y docker compose up -d.
Paso 6 — conectar los clientes de Bitwarden
Todos los clientes oficiales pueden apuntar a un servidor self-hosted. Instale el cliente de Bitwarden para escritorio, móvil o navegador desde las tiendas oficiales; no necesita una versión especial de Vaultwarden.
Antes de iniciar sesión, abra el icono de configuración en la pantalla de login (etiquetado como Self-hosted o Region → Self-hosted), establezca la Server URL como https://vault.example.com y guarde los cambios. Luego, inicie sesión con el correo electrónico y la contraseña maestra registrados; el cliente debería conectarse de inmediato y ofrecer la opción de autocompletar y guardar 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, utiliza http o el certificado no es de confianza; verifique primero que https://vault.example.com cargue correctamente en un navegador. Las actualizaciones lentas en otros dispositivos se deben a WebSocket push, lo cual se explica más adelante.
Paso 7 — una cárcel de Fail2ban para el endpoint de login
Vaultwarden registra cada intento de login fallido en el archivo definido por LOG_FILE; esto es lo que requiere un sistema de protección contra brute-force. Si aún no tiene instalado Fail2ban, puede consultar la instalación y los conceptos básicos en la guía de endurecimiento de SSH con Fail2ban; aquí añadiremos una cárcel para el vault.
Primero, localice la ubicación del volumen con nombre en el host para que Fail2ban pueda leer el log:
docker volume inspect vaultwarden_vw-data --format '{{ .Mountpoint }}'Esto imprimirá algo similar a /var/lib/docker/volumes/vaultwarden_vw-data/_data; el log se encuentra en vaultwarden.log dentro de dicho volumen. Cree el filtro:
# /etc/fail2ban/filter.d/vaultwarden.conf
[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =Y la cárcel:
# /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 con sudo systemctl restart fail2ban y confirme con sudo fail2ban-client status vaultwarden.
Tres detalles de Docker determinan si esta configuración ofrece protección. Primero, si el log muestra IP: 127.0.0.1 o la dirección de su proxy en cada intento fallido, Vaultwarden baneará el proxy; configure IP_HEADER con el encabezado que su proxy envíe realmente (X-Forwarded-For para Traefik, X-Real-IP para el bloque nginx anterior, CF-Connecting-IP si está detrás de Cloudflare). Segundo, la cadena de iptables correcta depende de su proxy: si Traefik se ejecuta como un contenedor con puertos publicados, el tráfico pasa por la ruta FORWARD de Docker, por lo que el baneo debe aplicarse en DOCKER-USER como se indicó arriba; pero si eligió la opción de nginx en el host del Paso 1, las conexiones terminan en nginx en la cadena INPUT del host y un baneo en DOCKER-USER nunca las detectará; en ese caso, elimine la línea chain = DOCKER-USER para que Fail2ban utilice la cadena INPUT por defecto. Tercero, utilice banaction = iptables-allports en lugar del valor por defecto basado en puertos; esta cárcel no define ningún puerto, y un baneo de todos los puertos en DOCKER-USER bloquea limpiamente al infractor de todos los servicios publicados en el servidor.
Paso 8 — realizar la copia de seguridad del vault y restaurarlo
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 alguno de estos elementos fallará cuando sea necesaria.
Copiar db.sqlite3 mientras Vaultwarden está escribiendo puede capturar un archivo corrupto a medio escribir; por tanto, realice una instantánea en frío (cold snapshot), el tiempo de inactividad es de pocos 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 vaultwardenEjecute esto mediante cron cada noche y copie el .tgz fuera del equipo; una copia de seguridad que solo reside en el servidor que se está protegiendo no es una copia de seguridad real. La forma más limpia de enviarlo es mediante una copia de seguridad nocturna con restic a otro servidor o almacenamiento de objetos, lo cual cifra el archivo y realiza la deduplicación de instantáneas repetidas. El botón Backup Database del panel de administración es una instantánea rápida y útil solo del archivo SQLite, pero omite los archivos adjuntos y las llaves.
Ahora, el proceso que diferencia una copia de seguridad real de una basada en la esperanza: restáurela una vez para confirmar 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 hacia el servidor con ssh -L 8888:127.0.0.1:8888 you@your-vps y abra http://localhost:8888. Debido a que localhost es un contexto seguro, crypto.subtle está disponible y el vault se descifra mediante http plano en este punto; es el único lugar permitido. Inicie sesión con su contraseña maestra y confirme que sus entradas están presentes: si lo están, su base de datos, las llaves RSA y la contraseña maestra han completado el ciclo de ida y vuelta, y podrá reconstruir el servicio en un VPS nuevo en cuestión de minutos. Detenga el contenedor con Ctrl-C y elimine /tmp/vw-restore.
Modos de fallo y los mensajes 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 es undefined; acceda solo 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. La Server URL es http, tiene un error tipográfico o el certificado no es de confianza; confirme que https://vault.example.com muestra un candado válido y vuelva a introducirla en la configuración self-hosted del cliente.
/admin rechaza la contraseña correcta. El hash Argon2 perdió su escaping — cada $ debe ser $$ en Compose — o ha 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 está reenviando los headers Upgrade/Connection; Traefik lo hace automáticamente, nginx requiere las dos líneas upgrade del Paso 1. El vault sigue funcionando, pero la sincronización ocurre al abrirlo. El antiguo puerto dedicado 3012 ya no existe desde la v1.31.0, por lo que no es necesaria una ruta WebSocket independiente.
Fail2ban reporta un ban pero el atacante sigue conectando. Está bloqueando 127.0.0.1 porque IP_HEADER es incorrecto, o el ban se encuentra en la cadena de iptables errónea — configure chain = DOCKER-USER y banaction = iptables-allports.
Actualizaciones
Descargue la nueva imagen y recree el contenedor; el volumen con nombre y todos sus datos persistirán:
docker compose pull
docker compose up -dVaultwarden lanza actualizaciones frecuentes. Revise las notas de lanzamiento del proyecto en lugar de fijar una versión de parche, ya que algunas versiones incluyen notas de migración. Realice un respaldo completo antes de cualquier actualización importante; puede revertir los cambios restaurando el archivo tar en un nuevo volumen.
FAQ
¿Es Vaultwarden lo mismo que Bitwarden?
Es un servidor independiente y compatible, no es el oficial. Vaultwarden reimplementa la API del servidor de Bitwarden en Rust. Por esto, los clientes oficiales de escritorio, móviles, navegador y CLI funcionan con él consumiendo una fracción de los recursos del stack oficial. El formato del vault es el mismo, por lo que puede migrar en cualquier dirección mediante exportación e importación.
¿Es necesario usar HTTPS o puedo usar http en mi LAN?
Es necesario usar HTTPS para cualquier caso que no sea una prueba de localhost. El vault web y las extensiones de Bitwarden utilizan la Web Crypto API del navegador. Esta API solo está disponible en contextos seguros; por tanto, mediante http plano el cliente lanza un error Cannot read properties of undefined y no permite el inicio de sesión. La única dirección http que funciona es http://localhost, razón por la cual la prueba de restauración en el Paso 8 utiliza un túnel SSH.
¿Cómo evito que extraños se registren en mi servidor?
Configure 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 a nuevas personas mediante el botón Invite User en /admin. Esto requiere tener configurado SMTP para que reciban el enlace de invitación. Revise la lista de usuarios administradores periódicamente para confirmar que no han aparecido cuentas inesperadas.
¿Cómo realizo una copia de seguridad de mi vault de Vaultwarden?
Detenga el contenedor brevemente y archive todo el volumen vw-data —los archivos db.sqlite3, attachments/, sends/, config.json y rsa_key.*— y luego copie el archivo fuera del servidor, idealmente mediante un cron nocturno. Copiar el archivo SQLite mientras el servidor está en ejecución puede corromper la instantánea; realice la copia con el servicio detenido. Lo más importante es realizar una restauración de prueba en un contenedor temporal e iniciar sesión para confirmar que la copia es válida antes de depender de ella.
¿Es seguro alojar mis contraseñas por mi cuenta?
Sí, siempre que realice las tres acciones que cubre esta guía: HTTPS real, registros cerrados con un token de administrador fuerte y copias de seguridad probadas. Su vault se cifra en el lado del cliente con su contraseña maestra; por lo tanto, el servidor nunca ve sus contraseñas en texto plano. Un db.sqlite3 robado es inútil sin ella. La contrapartida es que el parcheo y las copias de seguridad son ahora su responsabilidad, por lo que Fail2ban y el protocolo de restauración no son opcionales.