Skip to content
Go back

Outline: The Team Wiki That Needs SSO

By KingPin 13 min read
Outline: The Team Wiki That Needs SSO
Contents

The Wiki With No Front Door

Pick Outline if your team writes together, at the same time, and wants a wiki that feels like Notion. Stay on BookStack if you are one person or a household with a rack and a single login. Outline’s editor and live collaboration are better than BookStack’s. Its setup costs more: three extra services, and a login system you have to bring yourself.

That last part trips people up. Outline ships no username and password form. Its .env.sample lists Slack, Google, Microsoft Entra, Discord, and generic OIDC as sign-in providers, and says at least one is required “or you’ll have no sign-in options”. Email magic links exist, but they need an SMTP server. If you self-host and want no Google or Slack account in the loop, you run an OIDC provider next to it.

I’ll show the setup with Dex, a small OIDC provider, then cover the gotcha that eats an evening. The other wiki trade-offs are in BookStack vs Wiki.js.

Full example: Clone the working files at github.com/KingPin/sumguy-examples/self-hosting/outline-team-wiki

What Outline Does Better, and What It Costs

BookStack is a library: shelves, books, chapters, pages. Outline is a tree of collections and nested documents, with a block editor that responds to / commands like the one you already know from Notion. Two people can type in the same document and see each other’s cursors. Search is handled by Postgres (the repo has a search-postgres plugin), so there is no Elasticsearch to feed. There is also an API, so scripts can create and update documents.

Here is the bill.

OutlineBookStack
Services to runOutline, Postgres, Redis, an OIDC providerApp plus MariaDB
LoginOIDC (any provider), Slack, Google, Entra, Discord, others, or email links via SMTPBuilt-in accounts, plus LDAP/SAML/OIDC
EditingReal-time, multi-cursorEdit-conflict warnings, no live co-editing
LicenseBusiness Source License 1.1MIT

The license line deserves a read before you commit. As of Outline 1.11.0 (released October 12, 2026), the LICENSE file is BSL 1.1 from General Outline, Inc. The additional use grant lets you use the software for anything except a “Document Service”: a commercial offering where third parties, other than your employees and contractors, create their own teams and documents. Running it for your company, your club, or your homelab is fine. Hosting wikis for paying customers is not. The Change Date is October 12, 2030, when this version converts to Apache 2.0. That makes it source-available rather than open source in the OSI sense. If that matters to you, raise it before the migration.

The Stack: Four Containers

The stack is Outline, Postgres 18, Redis 8, and Dex v2.46.0. Outline’s own docs recommend Docker and say to pin image versions instead of trusting latest, so the compose file pins all four. The image is outlinewiki/outline on Docker Hub. The docs also show the same image at docker.getoutline.com/outlinewiki/outline. Both resolve; I used the Docker Hub name.

compose.yaml
services:
outline:
image: outlinewiki/outline:1.11.0
env_file: .env
ports:
- "3000:3000"
volumes:
- outline-data:/var/lib/outline/data
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
dex:
condition: service_started
restart: unless-stopped
postgres:
image: postgres:18
environment:
POSTGRES_USER: outline
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?copy .env.example to .env first}
POSTGRES_DB: outline
volumes:
- postgres-data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U outline -d outline"]
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped
redis:
image: redis:8
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped
dex:
image: ghcr.io/dexidp/dex:v2.46.0
command: ["dex", "serve", "/etc/dex/config.yaml"]
environment:
OUTLINE_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:?copy .env.example to .env first}
networks:
default:
aliases:
- dex.localtest.me
ports:
- "5556:5556"
volumes:
- ./config.yaml:/etc/dex/config.yaml:ro
- dex-data:/var/dex
restart: unless-stopped
volumes:
outline-data:
postgres-data:
dex-data:

Three details are worth a look.

  1. Postgres 18 mounts /var/lib/postgresql, not /var/lib/postgresql/data. The Postgres 18 image changed its data directory layout. Mount the old path and your data lands in an anonymous volume you will not find at 2 AM.
  2. There is no migration step. Outline runs its database migrations when the container starts. In my test the log shows Running migrations… and then a long run of Migrated ... lines before the web service starts. Postgres has to be healthy first, hence the service_healthy conditions.
  3. One Redis, no config. Outline’s Redis docs call a single server the right size for self-hosted setups and say anything more is overkill. REDIS_URL is the only setting.

The dex service has a network alias. That is the fix for the gotcha below, so keep reading.

The Outline Environment File

The minimum, per the docs, is URL, DATABASE_URL, REDIS_URL, and SECRET_KEY. UTILS_SECRET also ships in the sample file, so set both secrets.

.env.example
# Copy to .env, then fill the three secrets:
# openssl rand -hex 32
SECRET_KEY=replace_with_openssl_rand_hex_32
UTILS_SECRET=replace_with_openssl_rand_hex_32
OIDC_CLIENT_SECRET=replace_with_openssl_rand_hex_32
POSTGRES_PASSWORD=replace_with_openssl_rand_hex_32
# Outline core
URL=http://localhost:3000
PORT=3000
FORCE_HTTPS=false
DATABASE_URL=postgres://outline:${POSTGRES_PASSWORD}@postgres:5432/outline
PGSSLMODE=disable
REDIS_URL=redis://redis:6379
# Local file storage (the compose file mounts a volume here)
FILE_STORAGE=local
FILE_STORAGE_LOCAL_ROOT_DIR=/var/lib/outline/data
# OIDC via Dex (automatic discovery)
OIDC_ISSUER_URL=http://dex.localtest.me:5556/dex
OIDC_CLIENT_ID=outline
OIDC_DISPLAY_NAME=Dex

Generate the four secrets with openssl rand -hex 32 and paste them in. Two settings are there only because this is a localhost demo. FORCE_HTTPS=false stops Outline from redirecting to HTTPS (the default is true). PGSSLMODE=disable is the sample file’s own advice when Postgres and the app share a machine. On a real deployment behind TLS, drop FORCE_HTTPS=false and put the public URL in URL.

Discovery beats four URLs

The .env.sample lists OIDC_AUTH_URI, OIDC_TOKEN_URI, OIDC_USERINFO_URI, and OIDC_LOGOUT_URI. You do not need them. Outline’s OIDC docs describe automatic discovery: set OIDC_ISSUER_URL, and Outline fetches /.well-known/openid-configuration from it at startup and configures the rest. Manual mode needs the three endpoint variables, and nothing else. I used discovery, and the Outline log confirms it fetched the config and mounted the endpoints.

OIDC_DISPLAY_NAME is the label on the login button, OIDC_USERNAME_CLAIM defaults to preferred_username, and OIDC_SCOPES defaults to openid profile email. The docs add one warning: set either OIDC_LOGOUT_URI or OIDC_DISABLE_REDIRECT, or logging out gets awkward. With a single OIDC provider, Outline sends you straight to it, so a logged-out user can land back in a session before noticing.

Dex: The Smallest OIDC Provider That Works

Dex is a small OIDC provider that sits in front of other identity sources. You give it static clients and either static passwords or connectors to LDAP, GitHub, or SAML. For a team of ten, a static password list in a YAML file is a legitimate answer. For a team of two hundred, you want connectors.

config.yaml
issuer: http://dex.localtest.me:5556/dex
storage:
type: sqlite3
config:
file: /var/dex/dex.db
web:
http: 0.0.0.0:5556
oauth2:
skipApprovalScreen: true
staticClients:
- id: outline
name: Outline
secretEnv: OUTLINE_CLIENT_SECRET
redirectURIs:
- http://localhost:3000/auth/oidc.callback
enablePasswordDB: true
staticPasswords:
# password for this demo user: password
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
username: admin
userID: 08a8684b-db88-4b73-90a9-3cd1661f5466

The pieces map one to one onto Outline’s env file:

Want to see users from a real directory instead? Add a connector (LDAP, GitHub, generic OIDC upstream) in place of the password list. If you already run Authelia, Authentik, or Keycloak, skip Dex entirely and point OIDC_ISSUER_URL at that provider. Create an OIDC client there with Outline’s callback URL, and Outline will not care who is on the other end. Authentik vs Authelia covers the choice between those two.

The Gotcha: Two Clients Need the Same Issuer URL

OIDC puts a URL in the issuer field, and every party must read the same string. The browser follows redirects to Dex. The Outline container calls Dex directly to discover endpoints and exchange the login code. Both need http://dex.localtest.me:5556/dex to work, from two different places.

On a laptop, that is a trap. localhost works in the browser and breaks in the container, where localhost is the container itself. The Docker service name dex works in the container and breaks in the browser, which has never heard of it. Change one to suit the other and you fail on the first request or the last.

My first attempt used extra_hosts with host-gateway, pointing a hostname at the Docker host. Outline logged Failed to fetch OIDC configuration with Request timeout after 10000ms, then hit an unhandled promise rejection, logged Gracefully quitting, and restarted. In the container, the hostname resolved to ::1 instead of the host gateway. I did not trace why. The observation is enough: that route did not work here.

The version in the compose file works for a simple reason. localtest.me is a public DNS name that resolves to 127.0.0.1, so the browser reaches the published port 5556 with no /etc/hosts edit. The network alias on the dex service makes Docker’s own DNS answer dex.localtest.me with the Dex container’s address, so Outline reaches port 5556 directly. One name, one issuer string, two paths. The catch is that localtest.me needs public DNS, so the demo fails on a machine with no internet.

On a real server, this problem mostly disappears. Give Dex a real hostname behind your reverse proxy, such as auth.example.com, with TLS. The browser and the container then both resolve it through public DNS. Use the same string for the issuer, OIDC_ISSUER_URL, and the Dex redirect list.

Bring It Up and Check It Works

Terminal window
cp .env.example .env
for k in SECRET_KEY UTILS_SECRET OIDC_CLIENT_SECRET POSTGRES_PASSWORD; do
sed -i "s/^$k=.*/$k=$(openssl rand -hex 32)/" .env
done
docker compose up -d

Give Outline 20 to 40 seconds while migrations run (about 20 seconds on my machine with images already pulled, longer on a first pull or slow disks), then check three things. Dex answers discovery:

Terminal window
curl -s http://dex.localtest.me:5556/dex/.well-known/openid-configuration | head -3

Outline lists the provider on its login config endpoint:

Terminal window
curl -s -X POST http://localhost:3000/api/auth.config

You should see a provider with id oidc and the name Dex. Last, the login URL should answer with a 302 to Dex:

Terminal window
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://localhost:3000/auth/oidc

Open http://localhost:3000, click the Dex button, and sign in as [email protected] with password. The first person to sign in creates the workspace and gets the admin role. In my test, I ran the whole login with curl: Outline redirect, Dex login form, Dex code, Outline callback. It ended in a 302 to the welcome collection, and api/auth.info returned [email protected] with the admin role.

Whoever can sign in at Dex can reach Outline. I only tested one account, so keep the Dex user list to people you want in the wiki, and put Outline behind your VPN or a reverse proxy before you expose it.

File Storage: Local Volume or S3

Outline stores uploaded images and attachments outside Postgres, controlled by FILE_STORAGE. The two values are local and s3.

Local is what the compose file uses. Files land in /var/lib/outline/data inside the container, backed by the outline-data volume. If you bind mount a host folder instead, Outline’s docs say to give user ID 1001 write access with chown 1001 /your/folder. This is the right default for a single server: no extra service, and it backs up with the rest of your volumes.

Pick S3 when you want uploads off the app host, or you run more than one Outline instance. The vars from the sample file are AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_S3_UPLOAD_BUCKET_URL, AWS_S3_UPLOAD_BUCKET_NAME, AWS_S3_FORCE_PATH_STYLE, and AWS_S3_ACL. Path-style addressing matters for non-AWS stores such as MinIO or Garage, so set AWS_S3_FORCE_PATH_STYLE=true for those.

One mismatch to watch: the docs show FILE_STORAGE_UPLOAD_MAX_SIZE=26214400 (25 MB), while the sample file shows 262144000 (250 MB). Set the value yourself instead of trusting a default you have not read.

Upgrades and Backups

Pin the tag, read the release notes, bump the number, and run docker compose up -d. Migrations run on boot, so a rollback means restoring a Postgres dump from before the upgrade. Take one first.

Back up two things: the Postgres database (pg_dump is enough for a wiki) and the file storage volume. I treat Redis as disposable and did not test losing it. Restore the database and the files from the same point in time.

The SumGuy Take

Outline is a good fit for a team that edits together, and a poor fit for a lone admin who wants a notes app. The SSO requirement is a real cost, but it also pushes you toward one login for the whole stack, which you wanted anyway. Dex is the lightest way to get that: one YAML file, one container, and it stays out of your way. If you already run Authelia or Authentik, use that and skip Dex. If you run nothing, and the wiki is for you plus a couple of friends, BookStack still wins. Your 2 AM self will thank you for the simpler stack.

Common Questions

Can Outline run without Slack or Google login?

Yes. Outline supports generic OIDC, so a self-hosted provider such as Dex, Authelia, Authentik, or Keycloak replaces Slack and Google. Email magic-link sign-in also works for existing or invited users, but Outline needs an SMTP server configured first. No built-in username and password form exists, so one of these routes is required.

Is Outline free to self-host?

Yes, for internal use. Outline is licensed under the Business Source License 1.1, which allows any use except running a “Document Service”, a commercial offering where outside parties create their own teams and documents. Your company wiki qualifies. The 1.11.0 release converts to Apache 2.0 on October 12, 2030.

What hardware does Outline need?

Outline needs at least 512 MB of memory per its docs, which recommend 1 GB or more. In my idle test the four containers used about 515 MB together: Outline 353 MB, Postgres 122 MB, Dex 25 MB, Redis 16 MB. Plan 1 GB for a small team, and size for Postgres first.

Can I move my BookStack or Wiki.js pages into Outline?

Yes, through Markdown. Outline’s repository includes a Markdown import processor and a Notion plugin. Export your pages as Markdown from BookStack or Wiki.js, then use Outline’s import feature. Expect to rebuild the structure by hand, since BookStack’s books and chapters do not map onto collections automatically.

How do I back up Outline?

Back up the Postgres database with pg_dump and the file storage location, which is the outline-data volume or your S3 bucket. Documents live in the database and attachments live in storage. Restore the database and the files from the same point in time, and test one restore before you need it.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
Jellyfin GPU Transcoding: Prove It Works

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts