Use Docker and Traefik to Deploy BitWarden on VPS

I can’t remember what was the reason I started using password management software but it has become such an important tool over the years that I can’t live without. After trying so many different tools, initially I settled down with KeePass which is open source. However, the lacking of software and plugins support made we switch to SafeInCloud. I had been using it for quite a while until I found BitWarden from reading a post on Reddit. After the initial testing, I immediately made my jump, especially its self-hosting solutions ignited my passion in learning docker. However, Bitwarden’s official docker image consumes a lot of resources which my tiny VPS can’t afford. Luckily someone made Bitwarden_rs which is perfect for personal use. After several years of using BitWarden on my own VPS, I think it is time to write an article to not just for my own study but also in hopes of helping others to start using this great tool.

Let’s get started, shall we?

Installation of Docker

By default I will assume you’ve installed docker and docker compose already. If you don’t know how to, please refer back to the official documents below:

Configuration of Traefik 2

In the following I will be presenting 3 files for Traefik 2 configurations, docker compose file, static and dynamic configuration files. In my example, I would be separating the Traefik and BitWarden into two docker-compose file in their own folder. We would have better control of when we want the container to be created. I also gave up using external GitHub gist for my code as it is not well supported by some Hexo themes.

Docker Compose - docker-compose.yml

I will explain my configurations in detail in case I can reference back in my future blog posts. Please note as I am learning all this by my self in my free time, please excuses my mistakes…

Attributes Name Information
Image traefik:latest I am using the latest traefik image here
container_name traefik I am naming this container as traefik
security_opt no-new-privileges:true To prevent containers getting unnecessary privileges
ports 80:80 Open http Port 80
443:443 Open https Port 443
volumes /etc/localtime:/etc/localtime:ro Resolve the potentially mismatch of the host time and container time
/etc/timezone:/etc/timezone:ro Resolve the potentially mismatch of the host time and container time
/var/run/docker.sock:/var/run/docker.sock:ro Monitor Docker daemon
./data/traefik.yml:/traefik.yml:ro Attachstatic configurationfile traefik.yml to the container
./data/acme.json:/acme.json Attachacme.json file to the container for storing SSL certificates
./data/configurations:/configurations Attachconfigurationfolder, our dynamic configurationfile stores in this folder
networks proxy Add Traefik to an external Docker bridge Network name proxy
labels traefik.enable=true Allowed Traefik to access this container
traefik.http.routers.traefik.entrypoints=http Define http entry point. In my static configuration I defined http access port 80
traefik.http.routers.traefik.rule=Host(traefik.yourdomain) Define host name on http access
traefik.http.routers.traefik.middlewares=https-redirect@file Used https-redirectmiddleware which provide redirect the http traffic to https. We will define our middlewares in dynamic configuration
traefik.http.routers.traefik-secure.entrypoints=https Define https entry point. In my static configuration I defined https access port 443
traefik.http.routers.traefik-secure.rule=Host(traefik.yourdomain) Define host name on https access
traefik.http.routers.traefik-secure.middlewares=user-auth@file Used user-authmiddleware which provide basic auth to protect Traefik Dashboard. We will define our middlewares in dynamic configuration
traefik.http.routers.traefik-secure.tls=true We will be using TLS (SSL)
traefik.http.routers.traefik-secure.tls.certresolver=letsencrypt Define which provider you want to use to serve the certificate. We will define our certificate provider in static configuration
traefik.http.routers.traefik-secure.service=api@internal We connect our https access to Traefik Dashboard
networks: Proxy: external: true Define network proxy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
version: '3.3'

services:
traefik:
image: traefik:latest
container_name: traefik
restart: always
security_opt:
- no-new-privileges:true
ports:
- 80:80
- 443:443
volumes:
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./data/traefik.yml:/traefik.yml:ro
- ./data/acme.json:/acme.json
# Add folder with dynamic configuration yml
- ./data/configurations:/configurations
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.entrypoints=http"
- "traefik.http.routers.traefik.rule=Host(`traefik.yourdomain`)"
# Entry Point for https
- "traefik.http.routers.traefik.middlewares=https-redirect@file"
- "traefik.http.routers.traefik-secure.entrypoints=https"
- "traefik.http.routers.traefik-secure.rule=Host(`traefik.yourdomain`)"
- "traefik.http.routers.traefik-secure.middlewares=user-auth@file"
# ACME Certificate configuration
- "traefik.http.routers.traefik-secure.tls=true"
- "traefik.http.routers.traefik-secure.tls.certresolver=letsencrypt"
- "traefik.http.routers.traefik-secure.service=api@internal"

networks:
proxy:
external: true

It looks very complicated but all you need to do is to copy my code, change the host name and it should be good to go. ✌

Static Configuration - traefik.yml

This configuration is pretty straight forward. We declare that we will be using docker and file provider. As we are using docker so I don’t need to explain why we need to use docker provider, file provider will attach our Dynamic configuration to Traefik.

I’ve also defined two certificate providers here. One is Let’s Encrypt, the other one is BuyPass from Norway. Both providers provide free SSLs. The former one provide 3 months validity DV SSL and you can combine whatever you want on the certificate (single domain, wildcard, multiple domain, multiple wildcard). BuyPass only provides single domain DV SSL valid for 6 months. Although it is more than enough for personal use, BuyPass does have rate limits. for more information, please go to their official website

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
api:
dashboard: true

entryPoints:
http:
address: ":80"
https:
address: ":443"

providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
file:
filename: /configurations/dynamic.yml

certificatesResolvers:
letsencrypt:
acme:
email: [email protected]
storage: acme.json
keyType: EC384
httpChallenge:
entryPoint: http

buypass:
acme:
email: [email protected]
storage: acme.json
caServer: https://api.buypass.com/acme/directory
keyType: EC256
httpChallenge:
entryPoint: http

Dynamic Configuration - dynamic.yml

We defines our middlewares and SSL options in Dynamic configuration. These middleware are quite easy to under.

Attributes Name Information
middleware https-redirect Used to redirect http to https
secureHeaders We used this to boost our SSLLabs rating from A to A+…
bw-stripPrefix This is required by BitWarden WebSocket if you use it
user-auth Basic auth for Traefik Dashboard
tls cipherSuites Defines what cipher suites we want to use, turn off some weak ciphers
minVersion Defines the minimum TLS version to 1.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# Dynamic configuration
http:
middlewares:
https-redirect:
redirectScheme:
scheme: https
permanent: true

secureHeaders:
headers:
frameDeny: true
sslRedirect: true
browserXssFilter: true
contentTypeNosniff: true
forceSTSHeader: true
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 31536000

bw-stripPrefix:
stripPrefix:
prefixes:
- "/notifications/hub"
forceSlash: false

user-auth:
basicAuth:
users:
- "admin:$apr1$tm53ra6x$FntXd6jcvxYM/YH0P2hcc1"

tls:
options:
default:
cipherSuites:
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
minVersion: VersionTLS12

Docker Compose of BitWarden - docker-compose.yml

This docker-compose file is similar to Traefik’s. I only include extra lines for WebSocket.

Frankly speaking I am not using WebSocket for my own BitWarden setup. However most tutorials include WebSocket in their conguration, so as the official examples. You don’t have to use it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
version: '3'

services:
bitwarden:
image: bitwardenrs/server:latest
container_name: bitwarden
restart: always
volumes:
- ./bw-data:/data
environment:
- SIGNUPS_ALLOWED=false
- WEBSOCKET_ENABLED=true
networks:
- proxy
labels:
- "traefik.enable=true"
# Entry point for http
- "traefik.http.routers.bitwarden.entrypoints=http"
- "traefik.http.routers.bitwarden.rule=Host(`bw.yourdomain`)"
- "traefik.http.routers.bitwarden.service=bitwarden"
- "traefik.http.services.bitwarden.loadbalancer.server.port=80"
# Entry Point for https
- "traefik.http.routers.bitwarden.middlewares=https-redirect@file"
- "traefik.http.routers.bitwarden-secure.middlewares=secureHeaders@file"
- "traefik.http.routers.bitwarden-secure.entrypoints=https"
- "traefik.http.routers.bitwarden-secure.rule=Host(`bw.yourdomain`)"
- "traefik.http.routers.bitwarden-secure.service=bitwarden-secure"
- "traefik.http.services.bitwarden-secure.loadbalancer.server.port=80"
# Enable TLS
- "traefik.http.routers.bitwarden-secure.tls=true"
- "traefik.http.routers.bitwarden-secure.tls.certresolver=letsencrypt"
# websocket
- "traefik.http.routers.bitwarden-ws.entrypoints=https"
- "traefik.http.routers.bitwarden-ws.rule=Host(`bw.sslfor.fun`) && Path(`/notifications/hub`)"
- "traefik.http.middlewares.bitwarden-ws=bw-stripPrefix@file"
- "traefik.http.middlewares.bitwarden-ws=secureHeaders@file"
- "traefik.http.routers.bitwarden-ws.tls=true"
- "traefik.http.routers.bitwarden-ws.tls.certresolver=letsencrypt"
- "traefik.http.routers.bitwarden-ws.service=bitwarden-ws"
- "traefik.http.services.bitwarden-ws.loadbalancer.server.port=3012"

networks:
proxy:
external: true

At Last

After we save all these files, we need to run

  1. docker network create proxy to create the bridge network.
  2. docker-compose up -d in each folders to create containers

Please note these two docker compose files need to be stored in different folders. For instance, store Traefik’s file at~/docker-compose.yml,stores BitWarden’s to let’s say~/bitwarden/docker-compose.yml.

Do I have a single file to deploy both containers? Yes, I have😁

Create Traefik and BitWarden at the same time

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
version: '3'

services:
traefik:
image: traefik:latest
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
ports:
- 80:80
- 443:443
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./data/traefik.yml:/traefik.yml:ro
- ./data/acme.json:/acme.json
# Add folder with dynamic configuration yml
- ./data/configurations:/configurations
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.entrypoints=http"
- "traefik.http.routers.traefik.rule=Host(`traefik.yourdomain`)"
# Entry Point for https
- "traefik.http.routers.traefik.middlewares=https-redirect@file"
- "traefik.http.routers.traefik-secure.entrypoints=https"
- "traefik.http.routers.traefik-secure.rule=Host(`traefik.yourdomain`)"
- "traefik.http.routers.traefik-secure.middlewares=user-auth@file"
- "traefik.http.routers.traefik-secure.tls=true"
- "traefik.http.routers.traefik-secure.tls.certresolver=letsencrypt"
- "traefik.http.routers.traefik-secure.service=api@internal"

bitwarden:
image: bitwardenrs/server:latest
container_name: bitwarden
restart: always
volumes:
- ./bw-data:/data
environment:
- SIGNUPS_ALLOWED=false
- WEBSOCKET_ENABLED=true
labels:
- "traefik.enable=true"
# Entry point for http
- "traefik.http.routers.bitwarden.entrypoints=http"
- "traefik.http.routers.bitwarden.rule=Host(`bw.yourdomain`)"
- "traefik.http.routers.bitwarden.service=bitwarden"
- "traefik.http.services.bitwarden.loadbalancer.server.port=80"
# Entry Point for https
- "traefik.http.routers.bitwarden.middlewares=https-redirect@file"
- "traefik.http.routers.bitwarden-secure.middlewares=secureHeaders@file"
- "traefik.http.routers.bitwarden-secure.entrypoints=https"
- "traefik.http.routers.bitwarden-secure.rule=Host(`bw.yourdomain`)"
- "traefik.http.routers.bitwarden-secure.service=bitwarden-secure"
- "traefik.http.services.bitwarden-secure.loadbalancer.server.port=80"
# Enable TLS
- "traefik.http.routers.bitwarden-secure.tls=true"
- "traefik.http.routers.bitwarden-secure.tls.certresolver=letsencrypt"
# websocket
- "traefik.http.routers.bitwarden-ws.entrypoints=https"
- "traefik.http.routers.bitwarden-ws.rule=Host(`bw.yourdomain`) && Path(`/notifications/hub`)"
- "traefik.http.middlewares.bitwarden-ws=bw-stripPrefix@file"
- "traefik.http.middlewares.bitwarden-ws=secureHeaders@file"
- "traefik.http.routers.bitwarden-ws.tls=true"
- "traefik.http.routers.bitwarden-ws.tls.certresolver=letsencrypt"
- "traefik.http.routers.bitwarden-ws.service=bitwarden-ws"
- "traefik.http.services.bitwarden-ws.loadbalancer.server.port=3012"

I hope this tutorial will help people start using password management software or even host their own instances on their server.

Thanks for your time.