Guides

Setting up Outline with Docker, Digital Ocean, and Cloudflare

A full guide on selfhosting Outline with Docker, Digital Ocean, Cloudflare, and Discord OAuth - Blog by SH3LL

SH3LL
· 10 min read
Send by email

Setting up Outline with Docker, Digital Ocean, and Cloudflare

A full guide on selfhosting Outline with Docker, Digital Ocean, Cloudflare, and Discord OAuth - Blog by SH3LL

Connect with me!

INTRODUCTION

Thanks for tuning in to my blog! We are going to be setting up a self-hosted Outline platform using Docker. This guide will walk you through from start to finish, including an example OAuth2 setup with Discord.


We are going to use the following:

  • Digital Ocean Ubuntu Droplet
  • Cloudflare Reverse Proxy/Origin Certificates
  • NGINX Docker Container
  • Outline Docker Container
  • PostgreSQL Docker Container
  • Redis Docker Container
  • Discord OAuth2 Authentication

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 left hand navigation pane.

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.

  1. Install ufw if not already installed.
sudo apt install ufw
  1. Install openssl.
sudo apt install openssl
  1. Allow ssh.
sudo ufw allow ssh
  1. Allow 80/443 tcp.
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
  1. Enable ufw.
sudo ufw enable
  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, create an "A" record pointing to the IP address of your droplet. Under name put your root domain name (ex: "@" or "domain.com") or subdomain (ex: for "sub.domain.com" just put "sub"). Make sure that the proxy is toggled on.
  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 dockeruser and add to docker group.
sudo adduser dockeruser

sudo usermod -a -G docker dockeruser
  1. Create an Outline directory and add correct permissions/ownership for our dockeruser.

sudo mkdir -p /etc/docker/outline

sudo chown -R dockeruser:dockeruser /etc/docker/outline

sudo chmod -R 770 /etc/docker/outline
  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.env.
id -u dockeruser
  1. Find the PGID of the dockeruser and take note of it. We will need it for the docker.env.
id -g dockeruser

Directory & File Setup

  1. Navigate to the root outline directory.
cd /etc/docker/outline
  1. Create a docker-compose.yml file.
touch docker-compose.yml
  1. Create a database directory.
mkdir database-data
  1. Create an NGINX directory.
mkdir nginx
  1. Create a storage directory.
mkdir storage-data
  1. Create a redis directory.
mkdir redis
  1. Create a docker.env file.
touch docker.env
  1. Navigate to nginx directory and create conf and key files.
cd /etc/docker/outline/nginx

touch http.conf

touch origin.pem

touch key.pem

Your directory and files should look like this:

.
└── etc/
    └── docker/
        └── outline/
            ├── docker-compose.yml
            ├── docker.env
            ├── database-data/
            │   └── ..
            ├── storage-data/
            │   └── ..
            ├── redis/
            │   └── ..
            └── nginx/
                ├── http.conf
                ├── origin.pem
                └── key.pem

docker-compose.yml Setup

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

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

version: "3"
services:
  outline:
    image: docker.getoutline.com/outlinewiki/outline:latest
    restart: always
    env_file: docker.env
    ports:
      - "127.0.0.1:3000:3000"
    volumes:
      - storage-data:/var/lib/outline/data
    depends_on:
      - postgres
      - redis
    networks:
      - default
      - internal
   
  nginx:
    image: nginx:stable
    restart: always
    env_file: docker.env
    volumes:
      - ./nginx/http.conf:/etc/nginx/nginx.conf
      - ./nginx/origin.pem:/certificates/origin.pem
      - ./nginx/key.pem:/certificates/key.pem
      - ./nginx/dhparam.pem:/certificates/dhparam.pem
    ports:
      - 80:80
      - 443:443
    depends_on:
      - outline
    networks:
      - default
   
  redis:
    image: redis:latest
    restart: always
    env_file: docker.env
    expose:
      - "6379"
    volumes:
      - ./redis/redis.conf:/redis.conf
    command: ["redis-server", "/redis.conf"]
    networks:
      internal:
   
  postgres:
    image: postgres:latest
    restart: always
    env_file: docker.env
    expose:
      - "5432"
    volumes:
      - database-data:/var/lib/postgresql/data
    networks:
      internal:
      
volumes:
  storage-data:
  database-data:
  
networks:
    default:
    internal:
       internal: true

Here, we specifically declare that all containers except NGINX run on an internal network and are not publicly accessible. Outline is binded to localhost so that nginx can proxy traffic to it, but outline itself is only accessible on the local network.

docker.env Setup

  1. Navigate to the root Outline directory.
cd /etc/docker/outline
  1. Edit the docker.env file.
nano docker.env

Here is an example of how we have our docker.env configured. If you need more examples, see Outline's official sample here.

Make sure to update the URL value to your actual domain.

## Required Variables
SECRET_KEY= {Random_Key1}
UTILS_SECRET= {Random_Key2}
FORCE_HTTPS=true
ENABLE_UPDATES=true
WEB_CONCURRENCY=2
PUID=1000     # User ID (replace with dockeruser PUID)
PGID=1000     # Group ID (replace with dockeruser PGID)
 
## Postgres Variables
POSTGRES_USER=outlineusr
POSTGRES_PASSWORD= {Random_Key3}
POSTGRES_DB=outline
DATABASE_URL=postgres://outlineusr:{POSTGRES_PASSWORD}@postgres:5432/outline
PGSSLMODE=disable
 
## Redis Variables
REDIS_URL=redis://redis:6379
 
## Domain Variables
URL=https://mydomain.com
PORT=3000
 
## Rate Limiting Variables
RATE_LIMITER_ENABLED=true
RATE_LIMITER_DURATION_WINDOW=60
RATE_LIMITER_REQUESTS=600

## Local File Storage Variables - ONLY USE IF STORING ASSETS LOCALLY
FILE_STORAGE=local
FILE_STORAGE_LOCAL_ROOT_DIR=/var/lib/outline/data
FILE_STORAGE_UPLOAD_MAX_SIZE=26214400

# Discord Variables
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
DISCORD_SERVER_ID=
DISCORD_SERVER_ROLES=
  1. Generate 3 different random keys with the following command, or use a random key generator, and insert them in the relevant values.
openssl rand -hex 32
  1. Be sure to replace {POSTGRES_PASSWORD} on line 12 with its relevant key.
  2. Make sure to configure the discord variables from the next section with their relevant values.

Discord OAuth Setup

For instructions on setting up and obtaining the Discord values for OAuth, use Outline’s official documentation here.

NOTE: you will need to enable developer mode in discord before you can view/copy the server ID. Then, you need to setup an application in the developer portal. You can find developer documentation here.

Configuring NGINX

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

Here is what our nginx config file looks like:

worker_processes auto;
events {
    worker_connections 1024;
}
http {
    upstream outline {
        server outline:3000;
    }
    server {
        listen 80 default_server;
        listen [::]:80 default_server;
        server_name _;
        return 444; # Connection closed without response
    }
    server {
        listen 443 default_server ssl;
        listen [::]:443 default_server ssl;
        server_name _;
        ssl_reject_handshake on; # Reject SSL connection
    }
    server {
        listen 80;
        listen [::]:80;
        server_name mydomain.com;
        return 301 https://$host$request_uri;
    }
    server {
        listen 443 ssl http2;
        listen [::]:443 ssl http2;
        server_name mydomain.com;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS;
        ssl_prefer_server_ciphers on;
        ssl_session_timeout 24h;
        ssl_stapling on;
        ssl_stapling_verify on;
        ssl_certificate /certificates/origin.pem;
        ssl_certificate_key /certificates/key.pem;
        ssl_dhparam /certificates/dhparam.pem;
        ssl_session_cache shared:OutlineSSL:10m;
        ssl_session_tickets off;
        location / {
            proxy_pass http://outline;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "Upgrade";
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Scheme $scheme;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_redirect off;
        }
    }
}

This config is set up to deny any incoming requests that are not going to mydomain.com and forwards all http traffic to https and then to the outline docker application on localhost. You will need to replace every occurrence of mydomain.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.


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

  1. Make sure you are in the correct directory.
cd /etc/docker/outline/nginx
  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

Now let’s generate our dhparams.

sudo openssl dhparam -out /etc/docker/outline/nginx/dhparam.pem 2048

FINISHING UP

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

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

Once the application is up, you should now be able to access your Outline platform. 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 the outline app. Then add a custom hosts file (linux | windows) entry to map the FQDN in order to view the app on your system's browser.

Upgrading Outline

Upgrading is easy! Just use

docker pull docker.getoutline.com/outlinewiki/outline:latest

and then

docker compose down -v
docker compose up -d

Thanks for reading!

- SH3LL

SOURCES

CONNECT