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.
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
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.
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
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: [email protected] SMTP_HOST: smtp.your-mail-provider.tld SMTP_PORT: "587" SMTP_REPLY_EMAIL: [email protected] SMTP_USERNAME: [email protected] 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 as
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 like
eu-east-1is your region slug. For non-Amazon S3, you can either set this as
localhost:9000if 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.:
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.
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
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.
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
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!