Administration

Hosting SilverBullet.md read-only & admin with Docker, DigitalOcean, Cloudflare, and NGINX

Admin and read-only SilverBullet.md setup using Docker, Cloudflare reverse proxy, a DigitalOcean droplet, and NGINX while sharing the same data source. - Blog by SH3LL

SH3LL
· 11 min read
Send by email

Hosting SilverBullet.md read-only & admin with Docker, DigitalOcean, Cloudflare, and NGINX.

This guide will walk you through my personal codex infrastructure using SilverBullet.md, Cloudflare reverse proxy, a DigitalOcean droplet, and NGINX. I will also provide an out-of-the-box method for having a locked-down public read-only site while still having the ability to access admin using the same source files. - Blog by SH3LL

Connect with me!

INTRODUCTION

Thanks for tuning in to my blog! We are going to be setting up a self-hosted SilverBullet.md PKM. This guide will walk you through from start to finish. We will have a public-facing read-only SilverBullet instance that shares the same source directory as our admin password-protected SilverBullet instance. This allows us to have a site domain.com that we can share, but no one can alter or run commands. However, we will be able to access the admin.domain.com SilverBullet instance and make notes and configuration changes that are instantly reflected on the read-only site. Additionally, we will setup a Watchtower container that will automagically update our docker images.

We are going to use the following:

  • A Digital Ocean Ubuntu Droplet
  • Cloudflare Reverse Proxy/Origin Certificates
  • Official NGINX Docker Container
  • Two Official SilverBullet.md Docker Containers
  • Official Watchtower Docker Container

This guide assumes you have a decent understanding of Linux, a good amount of patience, and some basic troubleshooting skills. I will try to lay everything out for you the best I can.

Additionally, I will provide some basic steps to harden your droplet and docker setup. This is by no means the most secure build, but a decent baseline based on what I've learned. I highly recommend you do additional research to further secure your setup as needed such as SSH MFA, IPS/IDS, etc..


DEPLOY DROPLET

  • If you haven't already, create a Digital Ocean account. Once logged in, on your dashboard find the "Droplets" tab, under the "Manage" category, on the lefthand navigation plane.

Screenshot 2024-08-11 211214.png

Creation

  • Next, let's click "Create Droplet."
  • Choose the closest region to you, or the region where a majority of your users are located.
  • Under "Choose an image" we are going to use Ubuntu 24.04 (LTS) x64 for this guide.
  • Under "Droplet Type" we used "Basic" (optional).
  • Under "CPU options" we chose "Premium AMD" (optional) and 2GBmem/1cpu (minimum).
  • Under "Choose Authentication Method" select "SSH Key."

Let's create a key

On the device you are going to use to access your droplet, use this guide to create a key pair. This works on both Linux CLI (Command-line interface) and Windows Powershell and is a modern, secure key that works on most systems.

Upload the public key

Now that the keys have been generated and the private key has been added to the authentication agent, we can copy the contents of our public key and add it to the Digital Ocean droplet setup.

  • Click on "New SSH Key" in the droplet setup menu.
  • Paste the content of your full public key, making sure there is no whitespace or additional formatting.
  • Add a friendly name.
  • Click "Add SSH Key."

Finalize

Now let's finish up our setup with the last few options.

  • Under "We recommend these options" I recommend selecting "Add improved metrics monitoring and alerting (free)."
  • Under "Hostname" choose your desired droplet name.
  • Click "Create Droplet" at the bottom right.

Access

Now, from your local device, open up a powershell or terminal window as admin and connect to your droplet. In your Digital Ocean panel, you will see an IP for your Droplet. Use the IP in the following command:

ssh root@IP

if you run into issues, try to manually provide your private key (not the one that ends in .pub) with the following command:

Linux

ssh -i "/path/to/private/key" root@IP

Windows

ssh -i "\path\to\private\key" root@IP

Now, if you password-protected your private key, supply the password and you should now have a root shell on your droplet.


HARDENING DROPLET

My basic hardening guide can be referenced here.

Follow everything but ignore the "UFW setup" portion of that hardening guide and, instead, use this one:

UFW setup

We are going to be using UFW for this guide, it is easy to use and setup and does its job well. However, since most of our traffic is going to be going in and out of docker, setting up the firewall rules is a little tricky unless you want to do them by hand. For nginx, we will have to install it on the host in order to add the firewall rules. Once we've added the rules, we can remove nginx.

  1. Install ufw if not already installed.
sudo apt install ufw
  1. Install nginx.
sudo apt install nginx
  1. Install openssl.
sudo apt install openssl
  1. Install Apache HTTP Server tools (used for generating a .htpasswd to protect our admin site).
sudo apt-get install apache2-utils
  1. Allow ssh.
sudo ufw allow ssh
  1. Allow nginx.
sudo ufw allow 'NGINX Full'
  1. Enable ufw.
sudo ufw enable
  1. Uninstall nginx.
sudo apt purge nginx nginx-common -y
  1. Do not disconnect from your SSH session in case something went wrong. This is a good time to open a new terminal and verify you can still connect via SSH.

CLOUDFLARE

If you haven't already created a cloudflare account, you can create one here.

Once logged in, you will need to add your domain to cloudflare. You can reference their documentation.

Once your site is added, navigate to your website in the cloudflare dashboard.

  1. Under the DNS tab, we need to create two "A" records pointing to the IP address of your droplet. For the root domain name put @ or domain.com. For your admin subdomain just put admin for the A record's name. Make sure that the proxy is toggled on for both records.
  2. Under the SSL/TLS main tab, make sure current encryption mode is set to "Full (strict)". Then navigate to the "Edge Certificates" child tab and enable "Always use HTTPS."
  3. Still under the SSL/TLS section, find the "Origin Server" tab. Create a new origin certificate. You can use a wildcard *.domain.com to cover all your subdomains. During setup, make sure you download or copy the contents of the certificate and the key and name them accordingly because we will need to be able to distinguish between the key and the origin cert later.

SETTING UP DOCKER APPLICATIONS

Since docker changes quite frequently, I'm going to refer you to the official documentation for installation. You will need to install docker engine and docker compose. Since docker does change, and there are multiple ways to install it, you may have to transpose some of these commands to the correct/modern syntax depending on when you are reading this guide.

Initial Environment

Now that docker engine and docker compose are installed, we can work on getting the docker environment setup.

Create a docker user

Let's first create a docker user so the containers aren't running under a privileged user.

  1. Create new user: dockeruser and add to docker group.
sudo adduser dockeruser
sudo usermod -a -G docker dockeruser
  1. Create a SilverBullet directory and add correct permissions/ownership for our dockeruser. We will use sb for our directory throughout this guide.
sudo mkdir /etc/docker
sudo mkdir /etc/docker/sb
sudo chown dockeruser:dockeruser /etc/docker
sudo chown dockeruser:dockeruser /etc/docker/sb
sudo chmod 770 /etc/docker
sudo chmod 770 /etc/docker/sb
  1. Switch to the docker user.
sudo su dockeruser
  1. Find the PUID of the dockeruser and take note of it. We will need it for the docker-compose.yml.
id -u dockeruser
  1. Find the PGID of the dockeruser and take note of it. We will need it for the docker-compose.yml.
id -g dockeruser

Directory & File Setup

  1. Navigate to the SilverBullet directory.
cd /etc/docker/sb
  1. Create a docker-compose.yml file.
touch docker-compose.yml
  1. Create a keys directory.
mkdir keys
  1. Create an NGINX directory.
mkdir nginx
  1. Create a SilverBullet space directory.
mkdir space
  1. Navigate to the nginx directory and create a nginx.conf file.
cd /etc/docker/sb/nginx
touch nginx.conf
  1. Navigate to the keys directory and create your .pem files.
cd /etc/docker/sb/keys
touch origin.pem
touch key.pem

Your directory and files should look like this:

sb/
├── nginx/
│   └── nginx.conf
├── keys/
│   ├── origin.pem
│   └── key.pem
├── space/
└── docker-compose.yml

docker-compose.yml Setup

  1. Navigate to the sb directory.
cd /etc/docker/sb
  1. Edit the docker-compose.yml file.
nano docker-compose.yml

Here is an example of how we have our docker compose yaml configured:

services:
  nginx:
    image: nginx:latest
    container_name: nginx
    restart: always
    environment:
      - PUID=1000     # User ID (replace with dockeruser PUID)
      - PGID=1000     # Group ID (replace with dockeruser PGID)
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx:/config/nginx
      - ./keys:/config/keys
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - sbpublic
      - sbadmin
    networks:
      - default
      - internal

  sbpublic:
    image: zefhemel/silverbullet:latest
    container_name: sbpublic
    restart: always
    environment:
      - PUID=1000     # User ID (replace with dockeruser PUID)
      - PGID=1000     # Group ID (replace with dockeruser PGID)
      - SB_READ_ONLY=true
    ports:
     - "127.0.0.1:8000:3000"
    volumes:
      - ./space:/space
    networks:
      - default
      - internal

  sbadmin:
    image: zefhemel/silverbullet:latest
    container_name: sbadmin
    restart: always
    environment:
      - PUID=1000     # User ID (replace with dockeruser PUID)
      - PGID=1000     # Group ID (replace with dockeruser PGID)
      - SB_USER=admin:Password123 # replace (no special characters supported)
      - ADMIN=true
    ports:
     - "127.0.0.1:9000:3000"
    volumes:
      - ./space:/space
    networks:
      - default
      - internal

  watchtower:
    image: containrrr/watchtower
    container_name: watchtower
    restart: always
    environment:
      - PUID=1000     # User ID (replace with dockeruser PUID)
      - PGID=1000     # Group ID (replace with dockeruser PGID)
      - WATCHTOWER_CLEANUP=true
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - default
      - internal
    depends_on:
      - sbpublic
      - sbadmin
      - nginx

networks:
    default:
    internal:
        internal: true

The SilverBullet containers are mapped only to ports accessible on localhost and are not exposed to the outside world. I designed the config this way so that no one can bypass nginx. They are forced to go through nginx in order to route to the localhost.

Make sure to replace the admin username and password. Alternatively, you can remove the SB_USER line entirely if you do not want dual authentication since we will be securing our admin site with nginx/htpasswd. Also be sure to update all PUID and PGID entries with the dockeruser's IDs respectively. Additionally, confirm that all your paths are mapped correctly per your configuration. If you followed my schema, then this should already be good.

Configuring nginx

  1. Navigate to the nginx directory.
cd /etc/docker/sb/nginx
  1. Edit the nginx.conf file.
nano nginx.conf

Here is what our nginx config file looks like:

worker_processes auto;

events {
  worker_connections 1024;
}

http {
    upstream sbpublic {
    server sbpublic:3000;
    }
    
    upstream sbadmin {
    server sbadmin:3000;
    }

    server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    return 444; # Connection closed without response
    }

    server {
    listen 443 default_server;
    listen [::]:443 default_server;
    server_name _;
    ssl_reject_handshake on; # Reject SSL connection
    }

    server {
    listen 80;
    listen [::]:80;
    server_name domain.com;
    return 301 https://$server_name$request_uri;
    }

    server {
    listen 80;
    listen [::]:80;
    server_name admin.domain.com;
    return 301 https://$server_name$request_uri;
    }

    server {
        listen 443 ssl;
        listen [::]:443 ssl;
        server_name domain.com;
        ssl_certificate /config/keys/origin.pem;
        ssl_certificate_key /config/keys/key.pem;
        ssl_dhparam /config/keys/dhparams.pem;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers on;
        gzip on;
        client_max_body_size 4G;
        location / {
            # Proxy connections to the application servers
            proxy_pass http://sbpublic;
            proxy_redirect off;
            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-Host $server_name;
        }

        # deny access to .htaccess/.htpasswd files
        location ~ /\.ht {
            deny all;
        }
    }

    server {
        listen 443 ssl;
        listen [::]:443 ssl;
        server_name admin.domain.com;
        ssl_certificate /config/keys/origin.pem;
        ssl_certificate_key /config/keys/key.pem;
        ssl_dhparam /config/keys/dhparams.pem;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers on;
        gzip on;
        client_max_body_size 4G;
        location / {
            auth_basic "Restricted Area: I'm watching you :)";
            auth_basic_user_file /config/keys/.htpasswd;
            proxy_pass http://sbadmin;
            proxy_redirect off;
            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-Host $server_name;
        }

        # deny access to .htaccess/.htpasswd files
        location ~ /\.ht {
            deny all;
        }
    }
}

This config is set up to deny any incoming requests that are not going to domain.com or admin.domain.com and forwards all http traffic to https and then to the respective SilverBullet's container port on localhost. Additionally, we are enabling auth for the admin site and blocking access to the .htpasswd file. You will need to replace every occurrence of domain.com with your actual domain. This can be easily done by copying the config into a source-code editor like Visual Studio Code and using the find and replace feature.

Certificates & Keys

In the keys directory, we are going to insert our cloudflare origin and key files.

  1. Make sure you are in the correct directory.
cd /etc/docker/sb/keys
  1. Copy the contents of your origin cert to the origin.pem.
echo "content of origin cert goes here in quotes" > origin.pem
  1. Copy the contents of your key to the key.pem file.
echo "content of key goes here in quotes" > key.pem

In the same keys directory, we need to generate our Diffie-Hellman parameters.

  1. Generate dhparams.pem.
openssl dhparam -out ./dhparams.pem 4096
  1. Verifiy dhparams was generated.
ls -la

Again, in the same directory, we need to generate our .htpasswd file. Replace username with your preferred username. You will be prompted to enter and confirm a password.

  1. Generate .htpasswd.
htpasswd -c ./.htpasswd username
  1. Verify .htpasswd was generated.
ls -la

FINISHING UP

Now that everything is configured, we can compose the docker application.

  1. Navigate to the root SilverBullet directory.
cd /etc/docker/sb
  1. Compose the docker application.
docker compose up -d

Once the application is up, you should now be able to access your SilverBullet platforms. Please note that DNS records take some time to propagate. If you are unable to access your sites please wait a few hours and try again. If you have any issues, please join our discord, we are happy to assist. If you found any issues with this guide, please let me know in the comments below and I will get it corrected.

If you need to troubleshoot, you can check the docker compose logs or use SSH tunneling to the localhost port for one of the SilverBullet apps. Then add a custom hosts file (linux | windows) entry to map the FQDN in order to view the app on your system's browser.

Thanks for reading!

- SH3LL

CONNECT