💾 Archived View for gmi.runtimeterror.dev › silverbullet-self-hosted-knowledge-management › index.gm… captured on 2024-08-25 at 00:52:06. Gemini links have been rewritten to link to archived content

View Raw

More Information

-=-=-=-=-=-=-

💻 [runtimeterror $]

2024-08-22

SilverBullet: Self-Hosted Knowledge Management Web App

I recently posted on my other blog [1] about trying out SilverBullet [2], an open-source self-hosted web-based note-keeping app. SilverBullet has continued to impress me as I use it and learn more about its features [3]. It really fits my multi-device use case much better than Obsidian ever did (even with its paid sync plugin).

[1] recently posted on my other blog

[2] SilverBullet

[3] features

In that post, I shared a brief overview of how I set up SilverBullet:

I deployed my instance in Docker alongside both a Tailscale sidecar [4] and Cloudflare Tunnel sidecar [5]. This setup lets me easily access/edit/manage my notes from any device I own by just pointing a browser at `https://silverbullet.tailnet-name.ts.net/`. And I can also hit it from any *other* device by using the public Cloudflare endpoint which is further protected by an email-based TOTP challenge. Either way, I don't have to worry about installing a bloated app or managing a complicated sync setup. Just log in and write.

[4] Tailscale sidecar

[5] Cloudflare Tunnel sidecar

This post will go into a bit more detail about that configuration.

Preparation

I chose to deploy SilverBullet on an Ubuntu 22.04 VM in my homelab [6] which was already set up for serving Docker workloads so I'm not going to cover the Docker installation process [7] here. I tend to run my Docker workloads out of `/opt/` so I start this journey by creating a place to hold the SilverBullet setup:

[6] homelab

[7] installation process

sudo mkdir -p /opt/silverbullet 

I set appropriate ownership of the folder and then move into it:

sudo chown john:docker /opt/silverbullet 
cd /opt/silverbullet

SilverBullet Setup

The documentation offers easy-to-follow guidance on installing SilverBullet with Docker Compose [8], and that makes for a pretty good starting point. The only change I make here is setting the `SB_USER` variable from an environment variable instead of directly in the YAML:

[8] installing SilverBullet with Docker Compose

# docker-compose.yml
services:
  silverbullet:
    image: zefhemel/silverbullet
    container_name: silverbullet
    restart: unless-stopped
    environment:
      SB_USER: "${SB_CREDS}"
    volumes:
      - ./space:/space
    ports:
      - 3000:3000
  watchtower:
    image: containrrr/watchtower
    container_name: silverbullet-watchtower
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

I used a password manager to generate a random password *and username*, and I stored those in a `.env` file alongside the Docker Compose configuration; I'll need those credentials to log in to each SilverBullet session. For example:

# .env
SB_CREDS='alldiaryriver:XCTpmddGc3Ga4DkUr7DnPBYzt1b'

That's all that's needed for running SilverBullet locally, and I *could* go ahead and `docker compose up -d` to get it running. But I really want to be able to access my notes from other systems too, so let's move on to enabling remote access right away.

Remote Access

Tailscale

It's no secret that I'm a big fan of Tailscale [9] so I use Tailscale Serve to enable secure remote access through my tailnet. I just need to add in a Tailscale sidecar [10] and update the `silverbullet` service to share Tailscale's network:

[9] big fan of Tailscale

[10] Tailscale sidecar

# docker-compose.yml
services:
  tailscale: 
    image: tailscale/tailscale:latest
    container_name: silverbullet-tailscale
    restart: unless-stopped
    environment:
      TS_AUTHKEY: ${TS_AUTHKEY:?err}
      TS_HOSTNAME: ${TS_HOSTNAME:-ts-docker}
      TS_EXTRA_ARGS: ${TS_EXTRA_ARGS:-}
      TS_STATE_DIR: /var/lib/tailscale/
      TS_SERVE_CONFIG: /config/serve-config.json
    volumes:
      - ./ts_data:/var/lib/tailscale/
      - ./serve-config.json:/config/serve-config.json
  silverbullet:
    image: zefhemel/silverbullet
    container_name: silverbullet
    restart: unless-stopped
    environment:
      SB_USER: "${SB_CREDS}"
    volumes:
      - ./space:/space
    ports: 
      - 3000:3000
    network_mode: service:tailscale 
  watchtower: 
    image: containrrr/watchtower
    container_name: silverbullet-watchtower
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

That of course means adding a few more items to the `.env` file:

[11] pre-authentication key

# .env
SB_CREDS='alldiaryriver:XCTpmddGc3Ga4DkUr7DnPBYzt1b'
TS_AUTHKEY=tskey-auth-[...] 
TS_HOSTNAME=silverbullet
TS_EXTRA_ARGS=--ssh

And I need to create a `serve-config.json` file to configure Tailscale Serve [12] to proxy port `443` on the tailnet to port `3000` on the container:

[12] Tailscale Serve

// serve-config.json
{
  "TCP": {
    "443": {
      "HTTPS": true
    }
  },
  "Web": {
    "silverbullet.tailnet-name.ts.net:443": {
      "Handlers": {
        "/": {
          "Proxy": "http://127.0.0.1:3000"
        }
      }
    }
  }
}

Cloudflare Tunnel

But what if I want to consult my notes from *outside* of my tailnet? Sure, I *could* use Tailscale Funnel [13] to publish the SilverBullet service on the internet, but (1) funnel would require me to use a URL like `https://silverbullet.tailnet-name.ts.net` instead of simply `https://silverbullet.example.com` and (2) I've seen enough traffic logs to not want to expose a login page directly to the public internet if I can avoid it.

[13] Tailscale Funnel

Cloudflare Tunnel [14] is able to address those concerns without a lot of extra work. I can set up a tunnel at `silverbullet.example.com` and use Cloudflare Access [15] to put an additional challenge in front of the login page.

[14] Cloudflare Tunnel

[15] Cloudflare Access

I just have to add a `cloudflared` container to my stack:

# docker-compose.yml
services:
  tailscale: 
    image: tailscale/tailscale:latest
    container_name: silverbullet-tailscale
    restart: unless-stopped
    environment:
      TS_AUTHKEY: ${TS_AUTHKEY:?err}
      TS_HOSTNAME: ${TS_HOSTNAME:-ts-docker}
      TS_EXTRA_ARGS: ${TS_EXTRA_ARGS:-}
      TS_STATE_DIR: /var/lib/tailscale/
      TS_SERVE_CONFIG: /config/serve-config.json
    volumes:
      - ./ts_data:/var/lib/tailscale/
      - ./serve-config.json:/config/serve-config.json
  cloudflared: 
    image: cloudflare/cloudflared
    restart: unless-stopped
    container_name: silverbullet-cloudflared
    command:
      - tunnel
      - run
      - --token
      - ${CLOUDFLARED_TOKEN}
    network_mode: service:tailscale
  silverbullet:
    image: zefhemel/silverbullet
    container_name: silverbullet
    restart: unless-stopped
    environment:
      SB_USER: "${SB_CREDS}"
    volumes:
      - ./space:/space
    network_mode: service:tailscale
  watchtower: 
    image: containrrr/watchtower
    container_name: silverbullet-watchtower
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

To get the required `$CLOUDFLARED_TOKEN`, I create a new `cloudflared` tunnel [16] in the Cloudflare dashboard and add the generated token value to my `.env` file:

[16] create a new `cloudflared` tunnel

# .env
SB_CREDS='alldiaryriver:XCTpmddGc3Ga4DkUr7DnPBYzt1b'
TS_AUTHKEY=tskey-auth-[...]
TS_HOSTNAME=silverbullet
TS_EXTRA_ARGS=--ssh
CLOUDFLARED_TOKEN=eyJhIjo[...]BNSJ9 

Back in the Cloudflare Tunnel setup flow, I select my desired public hostname (`silverbullet.example.com`) and then specify that the backend service is `http://localhost:3000`.

Now I'm finally ready to start up my containers:

docker compose up -d 
[+] Running 5/5
 ✔ Network silverbullet_default        Created
 ✔ Container silverbullet-watchtower   Started
 ✔ Container silverbullet-tailscale    Started
 ✔ Container silverbullet              Started
 ✔ Container silverbullet-cloudflared  Started

Cloudflare Access

The finishing touch will be configuring a bit of extra protection in front of the public-facing login page, and Cloudflare Access makes that very easy. I'll just use the wizard to add a new web application [17] through the Cloudflare Zero Trust dashboard.

[17] add a new web application

The first part of that workflow asks "What type of application do you want to add?". I select **Self-hosted**.

The next part asks for a name (**SilverBullet**), Session Duration (**24 hours**), and domain (`silverbullet.example.com`). I leave the defaults for the rest of the Configuration Application step and move on to the next one.

I'm then asked to Add Policies, and I have to start by giving a name for my policy. I opt to name it **Email OTP** because I'm going to set up email-based one-time passcodes. In the Configure Rules section, I choose **Emails** as the selector and enter my own email address as the single valid value.

And then I just click through the rest of the defaults.

Recap

So now I have SilverBullet running in Docker Compose on a server in my homelab. I can access it from any device on my tailnet at `https://silverbullet.tailnet-name.ts.net` (thanks to the magic of Tailscale Serve). I can also get to it from outside my tailnet at `https://silverbullet.example.com` (thanks to Cloudflare Tunnel), and but I'll use a one-time passcode sent to my approved email address before also authenticating through the SilverBullet login page (thanks to Cloudflare Access).

I think it's a pretty sweet setup that gives me full control and ownership of my notes and lets me read/write my notes from multiple devices without having to worry about synchronization.

---

📧 Reply by email

Related articles

Taking Taildrive for a Testdrive

Automate Packer Builds with GithHub Actions

Building Proxmox Templates with Packer

---

Home

This page on the big web