RSS Amplifier

ello learns · Nov 7, 2023

to host my own outline

0
Sign in to vote or save

ello learns · ello learns

Outline is a very fast, very pretty notes/wiki system, and I've got my very own now.

Outline screenshot

It has a well-earned reputation for being hard to self-host, for a couple reasons:

  • Until recently, Outline needed an S3-compatible storage backend
  • It (still) needs an external authentication provider
  • SSL is mostly left as an exercise for the reader

The Outline team has dedicated documentation for self-hosting with Docker, but like a gajillion other snowflakes, I did things a little differently. Yes, I'm adding to the towering pile of custom Outline configurations. Yes, I'm a little sorry about that.

whatever just gimme your docker-compose

Fine. Jeez.

storage

Local storage is supported, so I used it!

# .env
# Specify what storage system to use. Possible value is one of "s3" or "local".
# For "local", the avatar images and document attachments will be saved on
# local disk.
FILE_STORAGE=local
# If "local" is configured for FILE_STORAGE above, then this sets the parent
# directory under which all attachments/images go. Make sure that the
# process has permissions to create this path and also to write files to it.
FILE_STORAGE_LOCAL_ROOT_DIR=/var/lib/outline/data
# Maximum allowed size for the uploaded attachment.
FILE_STORAGE_UPLOAD_MAX_SIZE=209715200

In your docker-compose.yml, mount a volume for that /var/lib/outline/data path, and you'll be all set:

volumes:
  - /var/outline/data:/var/lib/outline/data

I did miss this note in the documentation:

If you get permissions errors writing files, ensure that Outline has permission to write to the directory, by giving the user ID access: chown 1001 /location/on/host/filesystem

and ended up scrambling a bit when uploads failed. The official container runs with a non-root user, so if you're mounting a local filesystem from your host, the container's user needs write access to it. You can inspect the user yourself once your get your container running:

$ sudo docker-compose exec outline id
uid=1001(nodejs) gid=65533(nogroup) groups=65533(nogroup)

So just like the docs said, chown -R 1001 /your/host/storage/root.

authentication

Outline supports logins with a “magic” email link, but only after the first login, so you need an authentication provider to get started.

This is where I almost bailed on the project entirely, until I realized I could cheat: Forgejo is one of the projects in my hosting zoo, and Forgejo can be an OpenID Connect (OIDC) provider:

  1. Find your OAuth2 application settings at https://your-forgejo-root/user/settings/applications.
  2. Create a new application using https://your-outline-root/auth/oidc.callback as the redirect URI.
  3. Note the client ID and secret.
  4. In your Outline .env file, fill out the OIDC_ bits with your Forgejo details.

(These instructions should all work for Gitea too.)

# .env
OIDC_CLIENT_ID=<the client id from forgejo>
OIDC_CLIENT_SECRET=<the secret from forgejo>
OIDC_AUTH_URI=https://your-forgejo-root/login/oauth/authorize
OIDC_TOKEN_URI=https://your-forgejo-root/login/oauth/access_token
OIDC_USERINFO_URI=https://your-forgejo-root/login/oauth/userinfo
# Specify which claims to derive user information from
# Supports any valid JSON path with the JWT payload
OIDC_USERNAME_CLAIM=preferred_username
# Display name for OIDC authentication
OIDC_DISPLAY_NAME=Forgejo login
# Space separated auth scopes.
OIDC_SCOPES=openid profile email

After I set all this up, I found a section of the docs that talks about provisioning an admin user for local development. Would that work on a production install to bootstrap a user for magic-link login later? No idea, but you should try it and let me know!

mail

You need outgoing mail to be set up properly if you want to use magic links for login. This is another spot where the official docs were telling me what I needed, but I didn't see it, and instead I trial-and-error-ed my way to success: If your mail server uses STARTTLS, use port 587 and set SMTP_SECURE to false.

SMTP_HOST=your.smtp.host
SMTP_NAME=something-to-keep-google-happy
SMTP_PORT=587
SMTP_USERNAME=your@smtp.email
SMTP_PASSWORD=your-smtp-password
SMTP_FROM_EMAIL=Your Cool Outline <docs@your.domain>
SMTP_SECURE=false

(Don't panic, STARTTLS starts with plain-text and upgrades itself, so your mail will still be secure. It's all fine.)

Once I had mail working, I dropped the Forgejo OIDC, so magic links are my only login method.

Screenshot of Outline settings

web server

I've got A System at this point for serving my Docker zoo: Every project sits behind its own Nginx reverse proxy with SSL controlled by acmetool, and all the proxy configs are virtually identical. I had to make some additions for Outline because web sockets need help:

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

Again, the official docs told me to do this, but I didn't even read them because I have A System and I know better.

cronjob

This time I did a better job than the official docs, because I don't want my token in the web access logs. Outline's API endpoints generally accept GET and POST requests interchangeably, so you can POST your token to the cron endpoint. I created an executable script at /etc/cron.daily/outline-cron on my host that looks like this:

#!/bin/bash
curl -LSs -X POST \
    -d '{"token": "<my UTILS_SECRET key>"}' \
    -H 'content-type: application/json' \
    https://my-outline-root/api/cron.daily

backups

I've got some backup scaffolding already set up for the zoo, so I had to add Outline. Grabbing the storage location was easy enough, but there are a lot of ways to get the rest. The docs recommend a Postgres dump, but that seemed a little heavy to me. I went with a daily JSON export, using the API to create the archive, “download” it, and delete the internal copy.

  1. Create a new token for your export process at https://your-outline-root/settings/tokens. Outline API token screenshot
  2. Use that token in your requests against the API export_all, fileOperations.redirect and fileOperations.delete (undocumented) endpoints.

My script ended up looking like this:

response=$(curl -sS -X POST --fail \
    -H "authorization: Bearer $BACKUP_KEY" \
    -H "content-type: application/json" \
    -H "accept: application/json" \
    -d '{"format": "json", "includeAttachments": false}' \
    https://my-outline-root/api/collections.export_all)
exportid=$(echo "$response" | jq -r '.data.fileOperation.id')
curl -sL --retry 10 --retry-max-time 90 --retry-delay 5 --retry-all-errors \
    --fail \
    -o /var/outline/backup.zip \
    -H "authorization: Bearer $BACKUP_KEY" \
    -H "content-type: application/json" \
    "https://my-outline-root/api/fileOperations.redirect?id=$exportid"
curl -sS -X POST --fail \
    -H "authorization: Bearer $BACKUP_KEY" \
    -H "content-type: application/json" \
    -H "accept: application/json" \
    -d "{\"id\": \"$exportid\"}" \
    https://my-outline-root/api/fileOperations.delete > /dev/null

putting it all together

docker-compose.yml

version: "3"
services:
  outline-redis:
    image: redis:latest
    restart: always
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 30s
      retries: 3
    networks:
      - outline-internal
  outline-postgres:
    image: postgres:latest
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: outline
    volumes:
      - ./data/pgdata:/var/lib/postgresql/data
    restart: always
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "user", "-d", "outline"]
      interval: 30s
      timeout: 20s
      retries: 3
    networks:
      - outline-internal
  outline:
    image: outlinewiki/outline:0.72.2
    ports:
      - "127.0.0.1:6000:3000"
    env_file:
      - .env
      - .secrets
    volumes:
      - ./data/outline:/var/lib/outline/data
    restart: always
    healthcheck:
      test: ["CMD", "curl", "-SsIf", "http://localhost:3000"]
      interval: 30s
      timeout: 10s
      retries: 3
    depends_on:
      - outline-postgres
      - outline-redis
    networks:
      - outline-internal
      - outline-external
networks:
  outline-internal:
    internal: true
  outline-external:
    driver: bridge

.env

NODE_ENV=production
# hostname is the docker service name
DATABASE_URL=postgres://user:pass@outline-postgres:5432/outline
DATABASE_URL_TEST=postgres://user:pass@outline-postgres:5432/outline-test
DATABASE_CONNECTION_POOL_MIN=
DATABASE_CONNECTION_POOL_MAX=
PGSSLMODE=disable
# again, hostname is the docker service name
REDIS_URL=redis://outline-redis:6379
# nginx on the host reverse-proxies the container
URL=https://docs.my.cloud
PORT=3000
FILE_STORAGE=local
FILE_STORAGE_LOCAL_ROOT_DIR=/var/lib/outline/data
FILE_STORAGE_UPLOAD_MAX_SIZE=209715200
# forgejo config
OIDC_AUTH_URI=https://git.my.cloud/login/oauth/authorize
OIDC_TOKEN_URI=https://git.my.cloud/login/oauth/access_token
OIDC_USERINFO_URI=https://git.my.cloud/login/oauth/userinfo
OIDC_USERNAME_CLAIM=preferred_username
OIDC_DISPLAY_NAME=Forgejo login
OIDC_SCOPES=openid profile email
# https is handled on the host
FORCE_HTTPS=false
ENABLE_UPDATES=true
WEB_CONCURRENCY=16
MAXIMUM_IMPORT_SIZE=5120000
DEBUG=cache,presenters,events,emails,mailer,utils,multiplayer,server,services
LOG_LEVEL=info
SMTP_HOST=pony.my.cloud
SMTP_NAME=docs.my.cloud
SMTP_PORT=587
SMTP_USERNAME=docs@my.cloud
SMTP_FROM_EMAIL=Docs <docs@my.cloud>
SMTP_SECURE=false
DEFAULT_LANGUAGE=en_US
RATE_LIMITER_ENABLED=true
RATE_LIMITER_REQUESTS=500
RATE_LIMITER_DURATION_WINDOW=60

.secrets

SECRET_KEY="generate on setup with openssl rand -hex 32"
UTILS_SECRET="generate on setup with openssl rand -hex 32"
SMTP_PASSWORD="your email password"
BACKUP_KEY="the token i created for automated exports"

#outline #wiki #selfhost #docker #sysadmin

from @ello@void.ello.tech

Read the original on til.ello.tech

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.