How to Deploy Outline Wiki on Kubernetes
A year ago, I was tasked with finding a knowledge base/wiki that could be self-hosted, was intuitive to use, and was visually appealing. In that time, I tried BookStack, DokuWiki, and WikiJS; while these wikis all did what they needed to, they didn't quite fit what we were looking for. DokuWiki wasn't as intuitive for the non-technical users in our organization, BookStack felt a little to rigid in what you could and couldn't achieve, and WikiJS took quite a while for everyone to wrap their heads around.
At the beginning of this process, I stumbled across an interesting project called Outline Wiki. I loved how this looked, especially considering how visually similar it is to Notion, which I use a lot in my personal projects and for general note-taking. The issue with Outline, was that its documentation for the installation process was less than ideal. The GitHub repo has better information on it now than it did when I first stumbled across it, but it still doesn't provide straight to the point instructions on a simple deployment; this is partially due to the fact that it has mandatory dependencies such as PostgreSQL, Redis, and an S3-compatible storage. For someone looking to deploy a simple wiki, this sounds pretty heavy.
Most of my organization's internal resources are hosted on a Kubernetes cluster. We utilize Digital Ocean's managed offering for simplicity, and I can confirm that this guide works perfectly on version 1.16.15-do.2
of this service, and there's no reason for me to believe that it won't work just as well on future versions.
Prerequisites
You should already have the following resources setup:
- A domain (or subdomain) pointed to your cluster's external IP
- Cert Manager [ClusterIssuer type] (to issue SSL certs for your domains)
- GSuite Subscription or Slack Workspace (Outline requires one of these for authentication)
- Kubernetes cluster
- Kubectl command-line tool
- Nginx Ingress
Setup
For this guide, we'll be using MinIO as our S3-compatible storage provider. You can just as easily swap out a few parts of this guide to utilize Amazon S3 instead. We'll also be using the official Docker images for PostgreSQL and Redis.
For starters, ensure that you have kubectl
configured for your Kubernetes cluster. To test this, try running the following from your command-line:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-nginx-ingress-controller-79d9c4cd58-m6lb6 1/1 Running 0 78d
nginx-nginx-ingress-default-backend-6d96c457f6-ptqkg 1/1 Running 0 78d
As long as the command doesn't return an error, then you should be good to go!
I'll be providing a full YAML file at the bottom of this guide, but I'll walk through each component of it first to give you a better idea of how it all works. You can paste of each of these code samples into individual YAML files and create each resource individually by running kubectl apply -f resource.yaml
.
Namespace
The first resource we'll need to create is a namespace for our resources to live within. It's important to keep different projects within different namespaces for safety and for organization. A namespace can be created from YAML like so:
apiVersion: v1
kind: Namespace
metadata:
name: outline
ConfigMap
Next, we'll create a ConfigMap. This will provide the environment variables that Outline (and its dependencies) will read from at runtime. In this guide, we opted to combine all of the resources' ConfigMaps into a single ConfigMap. We chose to do this as there were a few resources that positively overlapped with no side-affects. You may decide to do this differently. Here's an example ConfigMap for Outline:
apiVersion: v1
kind: ConfigMap
metadata:
name: outline
namespace: outline
data:
AWS_REGION: xx-xxxx-x
AWS_S3_ACL: private
AWS_S3_FORCE_PATH_STYLE: "true"
AWS_S3_UPLOAD_BUCKET_NAME: wiki
AWS_S3_UPLOAD_BUCKET_URL: http://localhost:9000
AWS_S3_UPLOAD_MAX_SIZE: "26214400"
CDN_URL: ""
DATABASE_URL: postgres://outline:Outline123@localhost:5432/outline
DATABASE_URL_TEST: postgres://outline:Outline123@localhost:5432/outline-test
DEBUG: cache,presenters,events,emails,mailer,utils,multiplayer,server,services
DEFAULT_LANGUAGE: en_US
ENABLE_UPDATES: "true"
FORCE_HTTPS: "true"
GOOGLE_ALLOWED_DOMAINS: your-domain.tld
GOOGLE_ANALYTICS_ID: ""
PGSSLMODE: disable
PORT: "80"
REDIS_URL: redis://localhost:6379
SENTRY_DSN: ""
SLACK_MESSAGE_ACTIONS: "true"
SMTP_FROM_EMAIL: address@your-domain.tld
SMTP_HOST: smtp.your-mail-provider.tld
SMTP_PORT: "587"
SMTP_REPLY_EMAIL: address@your-domain.tld
SMTP_USERNAME: address@your-domain.tld
TEAM_LOGO: https://domain.tld/logo.png
URL: https://wiki.your-domain.tld
There are quite a few variables to talk about here. Here are the variables you should know about:
AWS_REGION
: if you're using Amazon S3, change this to your region slug.AWS_S3_ACL
: you'll probably want to keep this asprivate
.AWS_S3_UPLOAD_BUCKET_NAME
: this is the storage bucket that Outline will use to store assets. Make sure that this is not shared by any other resources.AWS_S3_UPLOAD_BUCKET_URL
: for Amazon S3, this should be something likes3.eu-east-1.amazonaws.com
whereeu-east-1
is your region slug. For non-Amazon S3, you can either set this aslocalhost:9000
if you're creating MinIO for Outline, or you can set it as a public domain if you're planning on utilizing MinIO for other services too (e.g.:https://s3.your-domain.tld
).GOOGLE_ALLOWED_DOMAINS
: a list of allowed domains that users can sign-up with.SMTP_FROM_EMAIL
: this is the address that Outline will use to send emails to users from. Setup an email account with your email provider and fill in the SMTP-related variables in this list. If you're using GSuite, you'll need to make sure that unsecure app access is enabled for the account and that app passwords are enabled.TEAM_LOGO
: this is the logo that Outline will use in-place of its own logo throughout your install. This should be hosted externally.
Secret
The secret will contain similar information to the ConfigMap above, but this information is sensitive and should therefore not be stored in plaintext. You will need to encode all of these values as Base64 before entering them into the secret. You can use online tools such as base64encode.org to achieve this:
apiVersion: v1
kind: Secret
metadata:
name: outline
namespace: outline
type: Opaque
data:
AWS_ACCESS_KEY_ID: key_here
AWS_SECRET_ACCESS_KEY: key_here
GOOGLE_CLIENT_ID: key_here
GOOGLE_CLIENT_SECRET: key_here
MINIO_ACCESS_KEY: key_here
MINIO_SECRET_KEY: key_here
SECRET_KEY: key_here
SLACK_APP_ID: key_here
SLACK_KEY: key_here
SLACK_SECRET: key_here
SLACK_VERIFICATION_TOKEN: key_here
SMTP_PASSWORD: password_here
UTILS_SECRET: key_here
You'll want to generate most of these values using a tool such as bitwarden.com/password-generator for security. Most of these variables are pretty self-explanatory, so I won't go into detail for them. You can get the SLACK
values from here. You do not need to provide the MINIO
variables if you're using something other than MinIO in this example. If you are using it, you'll want to populate the AWS
variables with the exact same values.
Persistent Volume Claim
We'll need a storage volume for MinIO and PostgreSQL to write to. We'll create one with 10GB of available space, but you can change this to anything that your cluster provider allows. We can achieve this by create a persistent volume claim as is shown below.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: outline
namespace: outline
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
Deployment
The deployment is where we specify and create the Docker containers that Outline will utilize and rely upon. It's important to note that this should be created after the ConfigMap, Secret, and Persistent Volume Claim.
apiVersion: apps/v1
kind: Deployment
metadata:
name: outline
namespace: outline
spec:
selector:
matchLabels:
app: outline
strategy:
type: Recreate
template:
metadata:
labels:
app: outline
spec:
volumes:
- name: data
persistentVolumeClaim:
claimName: outline
containers:
- name: outline
image: outlinewiki/outline:latest
command: ["sh", "-c", "yarn sequelize:migrate --env production-ssl-disabled && yarn start"]
envFrom:
- configMapRef:
name: outline
- secretRef:
name: outline
ports:
- containerPort: 80
- name: postgres
volumeMounts:
- name: data
mountPath: "/var/lib/postgresql/data"
subPath: postgres
image: postgres:latest
env:
- name: POSTGRES_USER
value: "outline"
- name: POSTGRES_PASSWORD
value: "Outline123"
- name: POSTGRES_DB
value: "outline"
ports:
- containerPort: 5432
- name: redis
image: redis:latest
ports:
- containerPort: 6379
- name: minio
volumeMounts:
- name: data
mountPath: "/data"
subPath: minio
image: minio/minio:latest
args:
- server
- /data
envFrom:
- secretRef:
name: outline
ports:
- containerPort: 9000
readinessProbe:
httpGet:
path: /minio/health/ready
port: 9000
initialDelaySeconds: 120
periodSeconds: 20
livenessProbe:
httpGet:
path: /minio/health/live
port: 9000
initialDelaySeconds: 120
periodSeconds: 20
You can remove lines 51 to 76 if you do not intend to self-host S3 via MinIO.
Service
The service is a very simple resource, which directs ingress traffic through to our chosen container. This will only expose port 80 for our Outline pod, which the Outline container listens on.
apiVersion: v1
kind: Service
metadata:
name: outline
namespace: outline
spec:
ports:
- port: 80
targetPort: 80
protocol: TCP
selector:
app: outline
Ingress
The ingress resource is the final part required to deploy Outline as a full service.
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: outline
namespace: outline
annotations:
kubernetes.io/ingress.class: "nginx"
cert-manager.io/cluster-issuer: "letsencrypt-production"
spec:
tls:
- hosts:
- wiki.your-domain.tld
secretName: outline-tls
rules:
- host: wiki.your-domain.tld
http:
paths:
- path: /
backend:
serviceName: outline
servicePort: 80
This ingress utilizes a pre-established ClusterIssuer titled letsencrypt-production
to obtain the required SSL certificate. Be sure to set the host
variable to a valid domain/subdomain pointing to your cluster's public IP.
Let's Deploy It!
As promised above, here is a concatenated version of the above YAML components. You'll still need to swap out the placeholder values with your own prior to deploying this, otherwise things won't work.
If you have each YAML component in a separate file, you'll need to create them in a specific order (Namespace, ConfigMap, Secret, Persistent Volume Claim, Deployment, Service, Ingress). If they're all in a single file, you'll need to make sure they're still in the same order from top to bottom.
To deploy, simply run the following command for each of your YAML files (or the single one):
$ kubectl apply -f outline.yaml
Assuming there are no errors, the next thing you'll want to do is wait for the SSL certificate to be issued. You can monitor this process like so:
$ kubectl -n outline get certs
NAME READY SECRET AGE
outline-tls False outline-tls 11s
A certificate is usually issued within 60 seconds if your Issuer has been configured correctly, though it can sometimes take up to 5 minutes. If it still isn't ready after this time, run kubectl -n outline describe certs
to see if there's any debugging information you can find.
Once the certificate has been successfully issued (denoted by the READY
header in the table given above), you should be able to visit your domain/subdomain in your browser and see the Outline Wiki login page!