Self-hosted Outline Wiki with Keycloak & MinIO

Outline is a pretty cool wiki project, but it requires a lot of other services to properly use it. Namely, it lacks an authentication service; instead, Outline relies on external SSO services, such as Google or Slack. I prefer self-hosting everything, so we must provide an one ourselves. Outline supports authentication with the OpenID Connect standard from any third-party that supports it. One self-hosted option is Red Hat’s Keycloak software, which we will be using today.

Before we start, I will give you a small list of what we will be using:

  • Keycloak - An open source identity provider with support for OpenID Connect, OAuth 2.0, and SAML 2.0.
  • Redis - An open source, NoSQL database/cache.
  • Minio - An open source, selfhosted, S3 storage database.
  • Postgres - An open source SQL database.
  • Nginx - An open source webserver.
  • Outline - A clean, modern, note-taking, live collaboration, wiki software. Not open source: non-commerical use only.
  • Docker - Containerization software that allows easy installation of programs without cluttering up your bare system with dependencies. If you do not already have Docker, please look here. You want Docker Engine/Server, not Desktop.

Prerequisites

  • A server
  • Docker installed on server
  • Domains or subdomains for Minio, Keycloak, and Outline.
  • Your DNS records are already pointed to your server with ports 80 and 443 open.
  • You have Certbot/SSL certifcates already.

This guide assumes you are using Ubuntu Server 22.04.

Setting up our Keycloak service

We will set up Keycloak in using Docker Compose. I personally have set it up separately from the Outline compose stack as I would like to run indepedently of Outline. You may combine the Docker Compose files if you so choose.

I won’t be using an external databsae with Keycloak (because I couldn’t get it to work with Postgres).

version: '3'

volumes:
  keycloak-db:

services:
  keycloak:
      image: quay.io/keycloak/keycloak:latest
      environment:
        KC_PROXY: edge
        KEYCLOAK_ADMIN: YOUR_USERNAME
        KEYCLOAK_ADMIN_PASSWORD: YOUR_PASSWORD
        KC_HOSTNAME: YOUR_DOMAIN_NAME
        KC_HOSTNAME_STRICT_HTTPS: true
        KC_HOSTNAME_STRICT: true
      volumes:
        - keycloak-db:/opt/keycloak/data/h2
      ports:
        - 7214:8080
      command: "start --optimized"

Okay, so I will now explain why I chose these options. If you wanna copy, go ahead, but here’s the reasoning behind each.

Enviroment Variables

Env Var Value Description
KC_PROXY edge This tells Keycloak that we have placed it behind a reverse proxy (Nginx).
KEYCLOAK_ADMIN YOUR_USERNAME Change this. This will be the username for your first admin user.
KEYCLOAK_ADMIN_PASSWORD YOUR_PASSWORD Change this. This will be the password for your first admin user. I recommend generating a password with pwgen 64 1.
KC_HOSTNAME YOUR_DOMAIN_NAME This will be the domain name where Keycloak is hosted behind. For example, auth.example.org.
KC_HOSTNAME_STRICT_HTTPS true This makes sure that we are accessing Keycloak with an HTTPS connection.
KC_HOSTNAME_STRICT true Ensures Keycloak is accessed through only the KC_HOSTNAME.

Service Parameters

Parameter Value Description
image quay.io/keycloak/keycloak:latest Fetches the latest version of Keycloak on the Docker Hub
volumes - keycloak-db:/opt/keycloak/data/h2 This will allow the data in Keycloak to persist in case you destroy the stack.
ports 7214:8080 We forward the port 8080 in the container to 7214 on our bare system. You can change to 7214 to whatever you wish, really, as long as you point Nginx to the right port.
commmand “start –optimized” Automatically runs this command on container start. Makes Keycloak actually run.

Now to actually run the server, take the big block of code above and paste it into a docker-compose.yml. I placed my compose file in /opt/keycloak/.

After that, run:

$ docker compose up -d

Setting up nginx for Keycloak

I don’t really like to use Nginx inside of a container, so we will be installing it on the bare server.

# Nginx can simply be installed with the command:
$ sudo apt install nginx
# Then we can change directory into the newly-created sites-enabled directory
$ cd /etc/nginx/sites-enabled
# Create a new configuration for Keycloak
$ sudo nano keycloak.conf

Getting the OIDC configuration from Keycloak

We will now be exposing Keycloak to the world using nginx. Be careful.

Paste the configuration below with modifications with Ctrl+Shift+V and save with Ctrl+S. Exit with Ctrl+X.

server {
    listen 80;
    server_name KEYCLOAK_DOMAIN_NAME;
    rewrite ^ https://$server_name$request_uri? permanent;
}

server {
    listen 443 ssl;

    server_name KEYCLOAK_DOMAIN_NAME;

    # Logging (optional, but probably want it for debugging)
    access_log /var/log/nginx/keycloak-access.log;
    error_log /var/log/nginx/keycloak-error.log info;

    ## Keep alive timeout set to a greater value for SSL/TLS.
    keepalive_timeout 75 75;

    # Locations of certs using Certbot
    ssl_certificate /etc/letsencrypt/live/KEYCLOAK_DOMAIN_NAME/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/KEYCLOAK_DOMAIN_NAME/privkey.pem;
    ssl_session_timeout 5m;

    # Forces browsers to redirect to HTTPS themselves for 31536000 seconds (1 year).
    # includeSubDomains will cause this to upgrade HTTP on all subdomains of the given domain.
    # preload adds your domain to a list of domains for browsers to automatically upgrade to HTTPS, no mattter what.
    # always will append the header no matter the return code.
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    # SSO requests tend to be very long, will fail on default settings.
    proxy_buffer_size 12k;

    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-Proto https;

    location /js/ {
        proxy_pass http://127.0.0.1:7214;
    }

    location /realms/ {
        proxy_pass http://127.0.0.1:7214;
    }

    location /resources/ {
        proxy_pass http://127.0.0.1:7214;
    }

    location /robots.txt {
        proxy_pass http://127.0.0.1:7214;
    }

    # We don't really care about anything else.
    location / {
        return 404;
    }

    # -------------------
    # I recommend deleting the /admin/ endpoint once done for security reasons.
    # -------------------

    location /admin/ {
        proxy_pass http://127.0.0.1:7214;
    }

}

Test the configuration with:

$ sudo nginx -t

Then apply the configuration with:

$ sudo service nginx restart

Now that our admin paths are exposed, we can access Keycloak’s admin panel. Go to the domain where Keycloak is hosted and visit the /admin path. You should be greeted with your “master” realm after logging in. Click the hamburger menu in the top left and select “Create Realm” in the dropdown menu. All you will need to do is type out any Realm Name you like (e.g. “Wenkdth Outline”, note that this will appear at the top of your OIDC login pages!), then select “Create.”

You will be greeted by yet another welcome page. Click the hamburger menu again, select “Clients,” then “Create Client.” Here, we will get our login endpoints for Outline.

1. General Settings Choose any Client ID you want, probably something descriptive, like “outline-wiki.” This will be our OIDC_CLIENT_ID. Then, fill out the Name and Description fields if you want. Keep Client Type as is.

2. Capability config Enable “Client authentication” (you can read why here] and disable “Direct access grants” (Outline does not need to know our login information).

After this, create the realm. You should be placed in your realms Settings tab. Select the Credentials tab and copy the Client secret down somewhere. We will be using as our OIDC_CLIENT_SECRET later.

Return to “Settings” and scroll down to “Access Settings.” Set the “Root URL,” “Home URL,” and “Web origins” as your Outline domain (with the “https://” at the start). Set the “Valid redirect URIs” to /auth/oidc.callback.

Our authentication endpoints that we need, OIDC_AUTH_URI, OIDC_TOKEN_URI, OIDC_USERINFO_URI can be found under “Realm Settings” in your realm’s hamburger menu. Click “OpenID Endpoint Configuration” under “Endpoints” in the “General” tab. These endpoints will be authorization_endpoint, token_endpoint, and userinfo_endpoint, respectively.

Creating a user

Now we will need to create a new user identity so we can log into Outline. Go the the Keycloak sidebar for our realm, select “Users,” the click “Add user.” Give them a username and create the user. We cannot login yet as this user lacks a password, so click the “Credentials” tab and click “Set password.” Enter a password and confirm it. You can mark it as “Temporary” so that the user must change the password when they log in for the first time. Click “Save” and confirm the change.

Outline also requires an email and first name to login, so make sure to add those in the “Details” tab. You may force-verify the email by enabling “Email verified,” though this is not necessary. If you don’t supply both an email and name, attempting to log into web app will simply refresh the login page.

Once you created your user(s), you may now logout of Keycloak and (optionally) remove the admin endpoint from your Nginx configuration.

Setting up Outline

And now we can set up Outline. For a self-hosted application, this program is a pain to set up considering how many other services it needs to run.

I have the version numbers pinned in my compose file. Make sure to update them when you set it up yourself.

version: "3"
services:

  outline:
    image: outlinewiki/outline:0.68.1
    env_file: ./docker.env
    networks:
      - outline-network
    ports:
      - "7215:3000"
    depends_on:
      - postgres
      - redis
      - minio
    command: sh -c "yarn db:migrate --env production-ssl-disabled && yarn start"

  redis:
    image: redis
    env_file: ./docker.env
    networks:
      - outline-network
    volumes:
      - ./redis.conf:/redis.conf
    command: ["redis-server", "/redis.conf"]

  postgres:
    image: postgres
    env_file: ./docker.env
    networks:
      - outline-network
    volumes:
      - outline-postgres-data:/var/lib/postgresql/data

  minio:
    image: minio/minio:RELEASE.2023-02-17T17-52-43Z
    env_file: ./docker.env
    networks:
      - outline-network
    ports:
      - "7216:9000"
      - "9001:9001"
    deploy:
      restart_policy:
        condition: on-failure
    volumes:
      - outline-minio-data:/data
    entrypoint: sh
    command: -c 'minio server --console-address ":9001" /data'

volumes:
  outline-minio-data:
  outline-postgres-data:

networks:
  outline-network:
    name: outlinewiki-network

Parameters

Outline

Our data is stored in Postgres, so we won’t need to supply a volume.

Parameter Description
image Fetches the 0.68.1 version of Keycloak on the Docker Hub. Make sure to update this.
env_file Path to file containing env vars to load in the containers.
networks Specifies what Docker networks this container can access
ports Forwards port 3000 in the container to 7215 on the host.
depends_on Waits for Minio, Postgres, and Redis to start.
command Migrate the DB on start, disable SSL (we will be terminating SSL at Nginx), then start Outline

Redis

Parameter Description
image Fetches latest Redis version from Docker Hub.
env_file Path to file containing env vars to load in the containers.
networks Specifies what Docker networks this container can access
volumes Passes the redis.conf file stored in the same directory as docker-compose.yml into the Redis container. I don’t think I actually change anything in the config, so this is probably useless.
command Starts Redis with the config file supplied in volumes.

Postgres

Parameter Description
image Fetches latest Postgres version from Docker Hub.
env_file Path to file containing env vars to load in the containers.
networks Specifies what Docker networks this container can access
ports Forwards port 9000 in the container to 7216 on the host (primary S3 port) and port 9001 to 9001 (Minio Admin Panel).
volumes Persists the Postgres data if you ever destroy the Docker compose stack.

Minio

Parameter Description
image Fetches RELEASE.2023-02-17T17-52-43Z Minio version from Docker Hub. Recommend updating to the latest stable release.
env_file Path to file containing env vars to load in the containers.
networks Specifies what Docker networks this container can access
deploy Not sure what this does. Probably like the regular restart: until-stopped Docker compose parameter.
volumes Persists the Minio data if you ever destroy the Docker compose stack.
entrypoint I believe this determines what shell the container uses to execute commands.
command Starts up Minio with the admin panel on port 9001.

Docker enviroment file

We will specify our enourmous amount of enviroment variables in a file named docker.env, stored in the same directory as our docker-compose.yml. The file below is derived from the official sample.

# Fill these out with "pwgen 64 1" or "openssl rand -hex 32".
SECRET_KEY=CHANGE_THIS_1
UTILS_SECRET=CHANGE_THIS_2

# Please see "Enviroment Variables" for more information.
# Make sure to change "user" and "pass".
POSTGRES_USER=user
POSTGRES_PASSWORD=pass
POSTGRES_DB=outline

# Please see "Enviroment Variables" for more information.
# Make sure to change "user" and "pass".
# If you changed POSTGRES_DB, change "outline", too.
DATABASE_URL=postgres://user:pass@postgres:5432/outline

# Was in the official Docker.env. Probably not needed?
DATABASE_URL_TEST=postgres://user:pass@postgres:5432/outline-test

# This is the domain name to access Outline. Include the "https://"
URL=https://YOUR_DOMAIN_NAME

# Please see "Enviroment Variables" for more information.
# Make sure to change "user" and "pass".
MINIO_ROOT_USER=CHANGE_THIS_3
MINIO_ROOT_PASSWORD=CHANGE_THIS_4

# You will need to get this from the Minio admin panel.
AWS_ACCESS_KEY_ID=REPLACE_THIS_1
AWS_SECRET_ACCESS_KEY=REPLACE_THIS_2
AWS_REGION=REPLACE_THIS_3

AWS_S3_UPLOAD_BUCKET_URL=YOUR_S3_DOMAIN_NAME
AWS_S3_UPLOAD_BUCKET_NAME=outline
AWS_S3_UPLOAD_MAX_SIZE=26214400
## Probably shouldn't change these
AWS_S3_FORCE_PATH_STYLE=true
AWS_S3_ACL=private

# You will need to get this from the Keycloak admin panel. Please see "Keycloak OIDC Setup".
OIDC_CLIENT_ID=KEYCLOAK_CLIENT_ID
OIDC_CLIENT_SECRET=KEYCLOAK_CLIENT_SECRET
OIDC_AUTH_URI=https://KEYCLOAK_DOMAIN/realms/KEYCLOAK_REALM_NAME/protocol/openid-connect/auth
OIDC_TOKEN_URI=https://KEYCLOAK_DOMAIN/realms/KEYCLOAK_REALM_NAME/protocol/openid-connect/token
OIDC_USERINFO_URI=https://KEYCLOAK_DOMAIN/realms/KEYCLOAK_REALM_NAME/protocol/openid-connect/userinfo
## Changes what shows up on the login button.
OIDC_DISPLAY_NAME=Keycloak
## Probably shouldn't change
OIDC_USERNAME_CLAIM=preferred_username
OIDC_SCOPES=openid profile email

# Telemtry?!?!! :O
# Enable if you want.
ENABLE_UPDATES=false

# Check official docs for this. Has to be with processes or whatever.
WEB_CONCURRENCY=1

# Limits size of documents (esp. with images) in (I think) bytes,
MAXIMUM_IMPORT_SIZE=5120000

# Default langauage. Supported codes at translate.getoutline.com.
DEFAULT_LANGUAGE=en_US

# ----------------------------------
# STUFF YOU PROBABLY SHOULD NOT CHANGE.
# ----------------------------------

# Self-explanatory.
NODE_ENV=production

# Docker will forward this specific port to the host as the one specified in the compose file.
PORT=3000

# Accessing self-hosetd redis through the Docker network.
REDIS_URL=redis://redis:6379

# We will be using Postgres through the Docker network. So no HTTPS.
PGSSLMODE=disable

# We will terminate SSL on nginx.
FORCE_HTTPS=false

Enviroment Variables

Env Var Description
POSTGRES_USER Change this. Outline will use this username to access Postgres.
POSTGRES_PASSWORD Change this. Outline will use this password to access Postgres. I recommend generating a password with pwgen 64 1.
POSTGRES_DB Name of the database that Outline will use in Postgres.
DATABASE_URL URL of the Postgres database. Change “user” to what you set in POSTGRES_USER and “password” to POSTGRES_PASSWORD. If you changed POSTGRES_DB, change “outline” to that.
URL The full URL to where Outline will be hosted. Includes the “https://”.
MINIO_ROOT_USER Change this. First admin’s username for Minio’s admin panel on port 9001.
MINIO_ROOT_PASSWORD Change this. First admin’s password for Minio’s admin panel on port 9001. I recommend generating a password with pwgen 64 1.
AWS_* Please see “Getting the S3 configuation from Minio”
OIDC_* Please see “Getting the OIDC configuation from Keycloak”

Setting up nginx for Outline and Minio

Technically, you don’t need Minio be working right now for Outline to work, so go ahead and docker compose up -d the Outline stack.

For the nginx configurations, follow the same steps as you did with Keycloak, but replace the locations.

So basically just copy/paste for both mino.conf and outline.conf, modifying for what you need. Note you will probably need two subdomains, one for Minio’s actual S3 endpoint and its admin panel (so you can access it). Unless you want to access it over an unsecured IP, which I can’t recommend.

server {
    listen 80;
    server_name OUTLINE_DOMAIN;
    rewrite ^ https://$server_name$request_uri? permanent;
}

server {
    listen 443 ssl;

    # copy/pasted stuff...

    server_name OUTLINE_DOMAIN;

    # copy/pasted stuff...
    
    # Make sure to change the certificate locations
    ssl_certificate /etc/letsencrypt/live/OUTLINE_DOMAIN/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/OUTLINE_DOMAIN/privkey.pem;
    
    # copy/pasted stuff...

    # For outline, you'd probably want something like this.
    location / {
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;

        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_set_header Host $http_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;
        # Set to the correct ports for Outline
        proxy_pass http://127.0.0.1:7215;
    }

}

Minio S3 & Console configuration For Minio’s nginx configuration, let us not reinvent the wheel and use their configs. Everything should be relatively the same. Below is only the S3 configuration. The console shouldn’t be too different.

server {
    listen 443 ssl;

    # copy/pasted stuff...

    ignore_invalid_headers off;
    client_max_body_size 0;
    proxy_buffering off;
    proxy_request_buffering off;

    location / {
        proxy_set_header Host $http_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_connect_timeout 300;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        chunked_transfer_encoding off;

        proxy_pass http://127.0.0.1:7216;
    }

Test the configuration with:

$ sudo nginx -t

Then apply the configuration with:

$ sudo service nginx restart

Getting the S3 configuation from Minio

We will have to expose Minio to the Internet so we can store stuff like account images and such.

First, we will need to access the admin panel. Login to the panel (either on the subdomain you created for it and serve using nginx, or through direct IP on port 9001) and click “Settings” on the sidebar. Click “Region” and change “Server Location” to whatever you want, like “mondstadt.” This will be our AWS_REGION. Minio will now need to restart after making this change. There should be a header that appears at the top that allows you to do so.

Log back in and now go to “Buckets” in the sidebar. “Create Bucket,” give it a name (this is AWS_S3_UPLOAD_BUCKET_NAME), then create the bucket. Outline will create a folder titled “public” inside our bucket, but does not seem to automatically make it accessible to the public. Thus, we must enter our newly-created bucket’s configuration, go to “Anonymous,” and “Add Access Rule.” The prefix should be public and the access should be readonly.

Now return to the sidebar and select “Access Keys.” We will click “Create access key.” Copy the “Access Key” and “Secret Key” and place them into our docker.env as AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. You probably should restrict the access key, but I’m not versed enough in the permissions to suggest anything. Now create the key.

Your AWS_S3_UPLOAD_BUCKET_URL should be the Minio S3 URL.

This is optional, but I recommend going back into the Minio Configuration Settings (where you changed the Region), click “API,” and set “Cors Allow Origin” to your Outline domain instead of *.

After that, we’re done with Minio.

Done!

We have accomplished the following:

  • Set up Keycloak for OpenID Connect authentication,
  • Set up Nginx and forwarded all the services we must to expose to the web,
  • Set up Outline, Postgres, Redis, and Minio using a single Docker compose stack,
  • Configured Minio properly to accept images,
  • And configured Outline properly to use Minio for S3 and Keycloak for OIDC.

That’s a lot of work for a decent-looking notes web application. I probably messed something up while explaining what I did to set Outline up myself, but if you managed to get Outline working yourself regardless, good work I guess.