Como instalar o Vaultwarden em uma VPS com Docker
Veja como hospedar senhas com Vaultwarden em uma VPS: HTTPS antes do acesso, token de administrador, Fail2ban e backups testados para os dados do cofre.
O que está a construir
Um gestor de palavras-passe que controla totalmente: o Vaultwarden a funcionar num contentor pequeno, atrás de um reverse proxy que termina o HTTPS, com as aplicações oficiais do Bitwarden no telemóvel, portátil e navegador apontadas para ele. O Vaultwarden reimplementa a API de servidor do Bitwarden em Rust e utiliza o mesmo protocolo que bitwarden.com. Por isso, todos os clientes oficiais funcionam com ele sem alterações, mas o consumo é de cerca de 100 MB de RAM, em vez da stack oficial com vários contentores.
A instalação consiste em uma dúzia de linhas do Compose. Os três pontos que realmente importam e que causam problemas são estes: o TLS tem de existir antes de abrir o cofre Web, os registos públicos têm de ser encerrados assim que a sua própria conta existir e o volume de dados tem de ter cópias de segurança que sejam restauradas em testes, porque esse único diretório contém todas as palavras-passe que possui.
Pré-requisitos e as limitações importantes
- Uma VPS com Docker Engine e o plugin Compose, numa instalação nova do Ubuntu 24.04 em KVM, com root ou sudo. 512 MB de RAM são realmente suficientes; 1 GB oferece uma margem confortável. Esta é uma das aplicações mais leves que pode executar e aparece no topo da lista curta de serviços que vale a pena alojar por conta própria. Dimensione a VPS para tudo o que partilhar os recursos com ela: colocar uma biblioteca de fotografias alojada por conta própria, como PhotoPrism ou Immich na mesma VPS eleva o mínimo de RAM para a ordem dos gigabytes, enquanto o Vaultwarden quase não altera esse requisito. O mesmo cálculo aplica-se às interfaces multimédia que adicionar mais tarde, porque transformar uma biblioteca do Jellyfin numa videoclub dos anos 90 onde se pode circular implica outro contentor sempre ativo, além de margem para transcodificação, dentro do mesmo orçamento.
- Um domínio com um registo A (e AAAA, se tiver IPv6) a apontar
vault.example.compara a VPS. O certificado TLS é emitido para este nome exato, por isso o DNS tem de resolver o nome antes de iniciar. - As portas 80 e 443 abertas para a Internet e terminadas pelo reverse proxy, nunca diretamente pelo Vaultwarden. A porta 80 é usada apenas para o desafio do certificado ACME e para o redirecionamento de HTTP para HTTPS.
- A principal limitação é esta: os clientes Bitwarden recusam comunicar com um servidor que não use HTTPS. Não existe uma fase de "testar primeiro por http"; esse caminho não funciona por uma razão concreta, explicada na secção seguinte.
Por que usar o Vaultwarden em vez da stack oficial do Bitwarden
Os mesmos clientes, com uma fração do consumo de recursos. O Bitwarden oficial para alojamento próprio é distribuído como um conjunto de contentores (MSSQL, Nginx, Identity, Api, Admin e outros) e requer cerca de 2 GB de RAM. O Vaultwarden é um único binário que, por predefinição, armazena tudo numa base de dados SQLite e permanece inativo com um consumo de algumas dezenas de megabytes. Para uma pessoa, uma família ou uma equipa pequena, é a escolha óbvia. Como implementa fielmente a API do Bitwarden, os seus dados permanecem portáteis entre o Vaultwarden e bitwarden.com.
O que perde é a maior parte das funcionalidades empresariais: não há aprovisionamento SCIM (embora o suporte experimental para SSO com OpenID Connect tenha sido introduzido na versão 1.35.0). Além disso, é o operador. A aplicação de patches, o HTTPS e as cópias de segurança ficam a seu cargo. Este guia aborda estas três tarefas.
Por que o HTTPS é obrigatório
O cofre web do Bitwarden e as extensões do navegador derivam as chaves de encriptação no navegador usando a Web Crypto API (window.crypto.subtle). Os navegadores só disponibilizam crypto.subtle num contexto seguro, através de HTTPS ou no caso especial de http://localhost. Em http://vault.example.com simples, isso é undefined. Por isso, assim que a aplicação tenta derivar uma chave, ocorre uma exceção e a consola mostra:
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'importKey')A página fica bloqueada ou apresenta um erro criptográfico genérico, e nenhum utilizador consegue iniciar sessão. Os clientes para desktop, dispositivos móveis e navegadores executam a sua própria verificação contra um URL autoalojado. Perante um endpoint http ou inacessível, recusam a ligação com:
This is not a recognized Bitwarden server. You may need to check with your provider or update your server.A causa é a mesma nos dois casos: não existe HTTPS válido. Por isso, configuramos o TLS primeiro e nunca abrimos o cofre através de http, nem sequer uma vez para uma verificação rápida.
Passo 1, DNS e o reverse proxy (TLS primeiro)
Aponte o registo para o seu VPS e confirme que resolve para o endereço correto:
dig +short vault.example.comA linha apresentada deve ser o IP do seu VPS. Se estiver vazia ou incorreta, corrija o DNS e aguarde o TTL. A emissão do certificado falha quando o nome não resolve.
Para o frontend HTTPS, este guia usa o Traefik. O Traefik emite e renova certificados Let's Encrypt automaticamente e integra-se diretamente no Compose. Se ainda não o executar, siga primeiro a configuração do reverse proxy Traefik e do TLS automático. Essa configuração cria uma rede Docker externa (proxy abaixo) e um resolver ACME (letsencrypt) ao qual o serviço Vaultwarden se liga. O nginx simples com um certificado emitido manualmente funciona da mesma forma do lado do Vaultwarden.
Prefere nginx e Certbot em vez de Traefik? Coloque o Vaultwarden em 127.0.0.1:8080 (adicione ports: ["127.0.0.1:8080:80"] ao serviço e remova os labels do Traefik). Depois, emita um certificado e configure o proxy para esse serviço. A parte do certificado está descrita em emitir certificados Let's Encrypt com Certbot e nginx. O elemento adicional crítico é o upgrade de WebSocket no caminho de notificações:
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";
}
}Tenha em atenção a linha X-Real-IP. É ela que permite ao Fail2ban ver o atacante real em vez de 127.0.0.1. Tudo o resto neste guia é idêntico, quer o Traefik quer o nginx esteja à frente.
Etapa 2, o ficheiro Compose
Crie primeiro o diretório do projeto. Este guia usa /opt/vaultwarden, que torna previsível o nome do projeto Compose e, portanto, o volume de dados vaultwarden_vw-data; as etapas seguintes do Fail2ban e das cópias de segurança dependem desse nome exato.
sudo mkdir -p /opt/vaultwarden
cd /opt/vaultwardenCrie um .env para o segredo de administração e o ficheiro Compose nesse diretório.
# .env
ADMIN_TOKEN=paste-a-strong-token-hereGere esse token com openssl rand -base64 48 e cole-o no ficheiro. (A forma com hash mais forte é apresentada a seguir; uma cadeia aleatória longa é suficiente para começar.)
# 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: trueHá dois aspetos deste ficheiro que sustentam todo o desenho. Não existe nenhum mapeamento ports:, por isso o Vaultwarden só fica acessível através do Traefik e do respetivo TLS; publicar a porta no host é a forma de servir acidentalmente o cofre por HTTP. Além disso, DOMAIN tem de ser o URL HTTPS público completo: esse valor é incorporado nas ligações dos anexos, no 2FA WebAuthn e no endpoint de notificações, por isso um valor incorreto ou HTTP danifica essas funcionalidades mesmo quando o site carrega. A tag latest é uma exceção deliberada à regra habitual de nunca usar latest. O Vaultwarden disponibiliza as suas versões estáveis como uma única imagem contínua, com :testing como canal separado de pré-lançamento. Por isso, atualize de forma intencional e consulte brevemente as notas de versão antes de fazer o pull. Esta exceção é limitada: a maioria dos contentores de longa duração deve ser fixada numa tag exata, que é o que mantém um agente autoalojado sempre ativo no mesmo VPS previsível entre reboots e pulls.
Inicie-o e monitorize o log:
docker compose up -d
docker compose logs -f vaultwardenUm arranque correto termina com uma linha semelhante a Rocket has launched from http://0.0.0.0:80. Aguarde alguns segundos para o Traefik obter o certificado e, em seguida, carregue https://vault.example.com. Deve obter o cofre Web do Bitwarden com um cadeado válido e sem aviso de certificado.
Etapa 3, um ADMIN_TOKEN forte e a armadilha de $$
ADMIN_TOKEN protege /admin, o painel que pode ler todos os utilizadores e todas as definições da sua instância. Trate-o como uma palavra-passe de root. Existem duas formas funcionais.
A forma simples é a cadeia aleatória que já gerou com openssl rand -base64 48. Como o base64 nunca contém $, pode ser inserida diretamente em .env sem escaping.
A forma reforçada é um hash PHC Argon2. Assim, o token em texto simples nunca é armazenado no disco. Gere um usando a mesma imagem:
docker run --rm -it vaultwarden/server /vaultwarden hash --preset owaspO comando pede o valor duas vezes e imprime uma cadeia que começa por $argon2id$v=19$.... Esta é a armadilha que pode consumir uma hora: o Docker Compose trata $ como interpolação de variáveis. Por isso, tem de duplicar cada $ para $$ ao colar o hash no ficheiro Compose. Coloque-o diretamente abaixo de environment:, não através de .env, e não o coloque entre aspas:
environment:
ADMIN_TOKEN: $$argon2id$$v=19$$m=19456,t=2,p=1$$c29tZXNhbHQ$$RdescudvJCsgt3ub+b+dWRWJTmaaJObGSe deixar os sinais $ simples, o Compose emite o aviso The "argon2id" variable is not set e deixa o token vazio. Depois, /admin rejeita a sua palavra-passe correta. Execute docker compose up -d e guarde no seu gestor de palavras-passe o texto simples introduzido na solicitação.
Etapa 4, registe a sua conta e feche o acesso
Com SIGNUPS_ALLOWED: "true", abra https://vault.example.com, clique em Create account e registe-se com o seu email e uma palavra-passe mestra forte. Esta palavra-passe mestra não pode ser recuperada. Não existe reposição. Guarde-a primeiro num local durável.
Agora feche o acesso. Edite o ficheiro Compose para desativar os registos:
SIGNUPS_ALLOWED: "false"Aplique novamente com docker compose up -d. Não adie esta medida de segurança. Se ficar aberto, qualquer pessoa que encontre o URL, incluindo crawlers, pode criar uma conta no seu servidor. Essas pessoas não conseguem ler o seu cofre, mas consomem recursos e transformam a sua instância privada num serviço aberto. O sinal de que deixou esta opção ativa é: /admin lista contas que nunca criou.
Para adicionar familiares ou membros da equipa mais tarde sem reabrir os registos públicos, use o botão Invite User em /admin. Esse processo requer SMTP configurado para que o convidado receba a ligação.
Passo 5, aceder a /admin
Aceda a https://vault.example.com/admin e introduza o token de administrador em texto simples (a string aleatória ou a palavra-passe que utilizou para gerar o hash, não o próprio hash). No painel, pode listar utilizadores, ajustar definições, enviar um email de teste e criar um snapshot da base de dados.
Se a página devolver 404 Not Found, ADMIN_TOKEN está vazio ou não definido, o que desativa completamente o painel. Esta também é uma opção válida se nunca precisar dele. Se a página carregar, mas rejeitar o seu token, consulte a armadilha de escape de $$ na lista de falhas abaixo. Esqueceu-se do token? Não existe um processo de recuperação. Edite .env ou o ficheiro Compose, defina um novo token e docker compose up -d.
Etapa 6: conecte os clientes Bitwarden
Todos os clientes oficiais podem apontar para um servidor autoalojado. Instale o cliente Bitwarden para desktop, dispositivos móveis ou navegador nas lojas habituais. Não precisa de uma compilação especial do Vaultwarden.
Antes de iniciar sessão, abra a engrenagem de definições no ecrã de início de sessão (com a etiqueta Self-hosted ou Region → Self-hosted), defina Server URL como https://vault.example.com e guarde. Em seguida, inicie sessão com o endereço de e-mail e a palavra-passe mestre que registou. O cliente deve ligar-se imediatamente e permitir preencher e guardar credenciais.
Se um cliente apresentar This is not a recognized Bitwarden server. You may need to check with your provider or update your server., o URL está incorreto, usa http ou o certificado não é confiável. Confirme novamente se https://vault.example.com carrega corretamente primeiro num navegador. As atualizações lentas noutros dispositivos usam push por WebSocket, explicado abaixo.
Etapa 7, uma jail do Fail2ban para o endpoint de login
O Vaultwarden regista cada tentativa de login falhada no ficheiro definido por LOG_FILE, exatamente o que é necessário para proteger contra ataques de força bruta. Se ainda não estiver a executar o Fail2ban, a instalação e os conceitos básicos estão no guia de reforço do SSH com Fail2ban; aqui adicionamos uma jail para o cofre.
Primeiro, descubra onde o volume nomeado está localizado no host, para que o Fail2ban possa ler o log:
docker volume inspect vaultwarden_vw-data --format '{{ .Mountpoint }}'Isto apresenta algo semelhante a /var/lib/docker/volumes/vaultwarden_vw-data/_data; o log está dentro de vaultwarden.log. Crie o filtro:
# /etc/fail2ban/filter.d/vaultwarden.conf
[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =E a 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 = 3600Recarregue com sudo systemctl restart fail2ban e confirme com sudo fail2ban-client status vaultwarden.
Três detalhes do Docker determinam se esta proteção funciona. Primeiro, se o log apresentar IP: 127.0.0.1 ou o endereço do seu proxy em todas as tentativas falhadas, o Vaultwarden está a bloquear o proxy. Defina IP_HEADER como o cabeçalho que o seu proxy realmente envia: X-Forwarded-For para o Traefik, X-Real-IP para o bloco do nginx acima e CF-Connecting-IP quando estiver atrás do Cloudflare. Segundo, a cadeia do iptables correta depende do seu proxy. Com o Traefik a executar como contentor com portas publicadas, o tráfego atravessa o percurso FORWARD do Docker. Por isso, o bloqueio deve ficar em DOCKER-USER, como acima. Se tiver escolhido a opção host-nginx da Etapa 1, as ligações terminam no nginx, na cadeia INPUT do host, e um bloqueio em DOCKER-USER nunca as vê. Nesse caso, elimine a linha chain = DOCKER-USER para que o Fail2ban utilize a cadeia predefinida INPUT. Terceiro, use banaction = iptables-allports em vez da predefinição baseada na porta. Esta jail não define nenhuma porta, e um bloqueio para todas as portas em DOCKER-USER impede de forma limpa que o intruso aceda a todos os serviços publicados no servidor.
Passo 8, faça uma cópia de segurança do cofre e restaure-o de facto
O volume vw-data é o seu gestor de palavras-passe. Contém db.sqlite3 (todas as entradas), os diretórios attachments/ e sends/, os ficheiros rsa_key.* que assinam as sessões de início de sessão e config.json do painel de administração. Uma cópia de segurança que ignore qualquer um destes elementos falha quando precisar dela.
Copiar db.sqlite3 enquanto o Vaultwarden está a escrever pode capturar um ficheiro parcialmente escrito e corrompido. Por isso, faça um snapshot a frio. A indisponibilidade dura apenas alguns 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 vaultwardenExecute isto todas as noites a partir do cron e copie .tgz para fora do servidor. Uma cópia de segurança que exista apenas no servidor que está a proteger não é uma cópia de segurança. A forma correta de a enviar é uma cópia de segurança noturna do restic para outro servidor ou armazenamento de objetos, que cifra o arquivo e elimina duplicados entre snapshots automaticamente. O botão Backup Database do painel de administração é uma forma prática de obter um snapshot imediato apenas do ficheiro SQLite, mas exclui os anexos e as chaves.
Agora vem o procedimento que distingue uma cópia de segurança real de uma simples expectativa: restaure-a uma vez e confirme 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/serverA partir do seu computador portátil, crie um túnel para o servidor com ssh -L 8888:127.0.0.1:8888 you@your-vps e abra http://localhost:8888. Como localhost é um contexto seguro, crypto.subtle está disponível e o cofre é desencriptado através de http simples neste caso, o único local onde isso é permitido. Inicie sessão com a sua palavra-passe principal e confirme que as suas entradas estão presentes. Se estiverem, a sua base de dados, as chaves RSA e a palavra-passe principal foram restauradas corretamente, e pode reconstruir o serviço numa VPS nova em poucos minutos. Pare o contentor com Ctrl-C e elimine /tmp/vw-restore. Mantenha este hábito de criar túneis para qualquer outra interface de administração no servidor que nunca deva ficar exposta à Internet. É assim que também acederia a um scanner de segurança open-kritt autoalojado na porta 5173.
Modos de falha, com as strings que verá
Cannot read properties of undefined (reading 'importKey') na consola do browser. O vault foi carregado através de http, por isso crypto.subtle não está definido. Aceda-lhe apenas através de https:// e adicione o redirecionamento de HTTP para HTTPS no proxy.
This is not a recognized Bitwarden server... num cliente. O Server URL usa http, está mal escrito ou o certificado não é confiável. Confirme que https://vault.example.com apresenta um cadeado válido e introduza-o novamente nas definições de self-hosted do cliente.
/admin rejeita a palavra-passe correta. O hash Argon2 perdeu os caracteres de escape. Cada $ tem de ser $$ no Compose. Também pode ter introduzido o hash em vez do texto simples que ele representa.
Sincronização lenta entre dispositivos; a consola mostra WebSocket connection to 'wss://vault.example.com/notifications/hub' failed. O proxy não está a encaminhar os cabeçalhos Upgrade/Connection. O Traefik faz isto automaticamente. O nginx precisa das duas linhas de upgrade do Step 1. O vault continua a funcionar, mas só sincroniza quando é aberto. A porta dedicada antiga 3012 deixou de existir desde a v1.31.0. Por isso, não é necessária uma rota WebSocket separada.
O Fail2ban comunica um ban, mas o atacante continua a ligar-se. Está a bloquear 127.0.0.1 porque IP_HEADER está incorreto, ou o ban está na chain errada do iptables. Defina chain = DOCKER-USER e banaction = iptables-allports.
Atualizações
Obtenha a nova imagem e recrie o contentor; o volume nomeado e todos os seus dados permanecem:
docker compose pull
docker compose up -dO Vaultwarden lança versões com frequência. Consulte as notas de lançamento do projeto em vez de fixar uma versão de correção, porque algumas versões incluem instruções de migração. Faça um backup atualizado antes de qualquer atualização principal; pode reverter restaurando o tarball num novo volume.
FAQ
O Vaultwarden é igual ao Bitwarden?
É um servidor compatível e independente, não o servidor oficial. O Vaultwarden reimplementa a API do servidor Bitwarden em Rust. Por isso, os clientes oficiais para desktop, dispositivos móveis, browser e CLI funcionam com ele, usando uma fração dos recursos da stack oficial. O formato do cofre é o mesmo. Pode migrar nos dois sentidos através de exportação e importação.
Preciso mesmo de HTTPS ou posso executá-lo em http na minha LAN?
Precisa de HTTPS para tudo, exceto um teste localhost. O cofre Web e as extensões do Bitwarden usam a API Web Crypto do browser. Essa API só está disponível num contexto seguro. Por isso, através de http simples, o cliente gera Cannot read properties of undefined e nunca inicia sessão. O único endereço http que funciona é http://localhost. É por isso que o teste de restauro no Passo 8 usa um túnel SSH.
Como impeço que pessoas desconhecidas criem contas no meu servidor?
Defina SIGNUPS_ALLOWED: "false" no ficheiro Compose e execute docker compose up -d imediatamente depois de criar a sua própria conta. A partir desse momento, adicione novas pessoas através do botão Invite User em /admin. Isto requer SMTP configurado para que recebam a ligação de convite. Consulte ocasionalmente a lista de utilizadores administradores para confirmar que não apareceram contas inesperadas.
Como faço uma cópia de segurança do meu cofre Vaultwarden?
Pare temporariamente o contentor e arquive todo o volume vw-data, db.sqlite3, attachments/, sends/, config.json e os ficheiros rsa_key.*. Depois, copie o arquivo para fora do servidor, idealmente através de um cron noturno. Copiar o ficheiro SQLite enquanto o servidor está em execução pode produzir um snapshot corrompido. Por isso, faça a cópia com o serviço parado. Mais importante ainda, restaure-a uma vez num contentor descartável e inicie sessão. Assim, confirma que a cópia de segurança é válida antes de depender dela.
É realmente seguro alojar as minhas palavras-passe?
Sim, se fizer as três coisas abrangidas por este guia: HTTPS real, registos fechados e um token de administrador forte, além de cópias de segurança testadas. O seu cofre é encriptado no cliente com a sua palavra-passe principal. Por isso, mesmo o servidor nunca vê as suas palavras-passe em texto simples. Um db.sqlite3 roubado não serve de nada sem essa palavra-passe. A contrapartida é que a aplicação de atualizações e as cópias de segurança passam a ser da sua responsabilidade. É por isso que o Fail2ban e o procedimento de restauro não são opcionais neste guia. Depois de configurar estes elementos, uma análise mais detalhada dos pontos onde um cofre alojado por si pode ser efetivamente atacado é o passo seguinte mais útil. Como os registos do cofre estão encriptados no cliente, resta proteger o token de administrador e o arquivo de cópia de segurança.