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

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:
- Find your OAuth2 application settings at
https://your-forgejo-root/user/settings/applications. - Create a new application using
https://your-outline-root/auth/oidc.callbackas the redirect URI. - Note the client ID and secret.
- In your Outline
.envfile, fill out theOIDC_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!
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.

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.
- Create a new token for your export process at
https://your-outline-root/settings/tokens.
- Use that token in your requests against the API
export_all,fileOperations.redirectandfileOperations.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

Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.