Plane + Outline + Authentik: Self-Hosted Team OS on Liquid Web (2026)
TL;DR
- One
docker-compose.yml: Plane (project management) + Outline (wiki) + Authentik (SSO) + shared PostgreSQL + Redis + MinIO + Caddy - Measured idle RAM: ~3.3 GB; peak: ~5.2 GB (10 concurrent users, active file uploads)
- Minimum Liquid Web tier: 8 GB Managed VPS (~$33–$40/mo)
- Linear (10 users) + Notion Team (10) + Okta (10): ~$260/mo. This stack: ~$33/mo
Engineering teams pay for Linear to track issues, Notion to write docs, and Okta to control who logs in to both. This guide deploys the self-hosted equivalents — Plane for project management, Outline for the team wiki, and Authentik as the OIDC identity provider — on a single Liquid Web 8 GB Managed VPS. One login covers everything.
This is a meaningful integration: Authentik acts as the OIDC provider for both Plane and Outline, so adding or removing a team member in Authentik immediately controls access to both tools. No separate user management in Plane, no separate invitations in Outline.
Read the individual tool pages before deploying the combo:
- Self-Host Plane — Plane architecture, service breakdown, and standalone setup
- Self-Host Outline — Outline OIDC requirements and standalone setup
- Self-Host Authentik — Authentik OIDC provider configuration
What you get
| Tool | Role | Replaces |
|---|---|---|
| Plane | Issue tracking, sprints, roadmaps, cycles | Linear, Jira |
| Outline | Team wiki, documentation, meeting notes | Notion, Confluence |
| Authentik | OIDC identity provider, single sign-on | Okta, Auth0, Google Workspace SSO |
| PostgreSQL 16 | Shared database — three schemas (authentik, plane, outline) | — |
| Redis | Shared queue/cache (DB 0 = Authentik, DB 1 = Plane, DB 2 = Outline) | — |
| MinIO | S3-compatible object storage — Plane file uploads | AWS S3 |
| Caddy | TLS termination and routing | — |
What's not included: Git integration (Plane has a GitHub integration via API — configure it after setup), video calling, or transactional email. Bring your own SMTP (Postmark, Mailgun, or Resend work well).
Architecture
sso.yourdomain.com → Caddy → authentik-server:9000
issues.yourdomain.com → Caddy → plane-web:3000 (Plane frontend)
→ plane-api:8000 (Plane API — /api/*)
→ plane-space:3000 (/spaces/*)
wiki.yourdomain.com → Caddy → outline:3000
MinIO runs internally on port 9000 and is never exposed through Caddy. Plane communicates with MinIO directly over the Docker network.
Prerequisites
- A Liquid Web 8 GB Managed VPS — verify pricing
- Docker Engine 25+ and Docker Compose V2
- Three subdomains:
sso.yourdomain.com,issues.yourdomain.com,wiki.yourdomain.com - DNS A records for all three subdomains pointing at your VPS before starting Caddy
- SMTP credentials for both Plane and Outline (outbound email is required for invitations)
- About 90 minutes
The complete docker-compose.yml
# plane-outline-authentik-stack/docker-compose.yml
# Tested 2026-05-02 on local Docker (4 vCPU / 8 GB RAM)
# Idle: ~3.3 GB RAM | Peak: ~5.2 GB (10 concurrent users, active file uploads)
services:
# ─── Shared infrastructure ────────────────────────────────────────────────
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
- ./scripts/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
networks:
- stack_net
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
networks:
- stack_net
minio:
image: minio/minio:latest
restart: unless-stopped
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
volumes:
- minio_data:/data
networks:
- stack_net
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:9000/minio/health/live || exit 1"]
interval: 15s
timeout: 5s
retries: 5
# ─── Authentik ─────────────────────────────────────────────────────────────
authentik-server:
image: ghcr.io/goauthentik/server:${AUTHENTIK_VERSION:-2024.12.3}
restart: unless-stopped
command: server
environment:
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_REDIS__PASSWORD: ${REDIS_PASSWORD}
AUTHENTIK_REDIS__DB: 0
AUTHENTIK_POSTGRESQL__HOST: db
AUTHENTIK_POSTGRESQL__USER: authentik
AUTHENTIK_POSTGRESQL__NAME: authentik
AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
AUTHENTIK_ERROR_REPORTING__ENABLED: "false"
volumes:
- authentik_media:/media
- authentik_templates:/templates
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks:
- stack_net
healthcheck:
test: ["CMD-SHELL", "ak healthcheck"]
interval: 20s
timeout: 5s
retries: 5
start_period: 60s
authentik-worker:
image: ghcr.io/goauthentik/server:${AUTHENTIK_VERSION:-2024.12.3}
restart: unless-stopped
command: worker
environment:
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_REDIS__PASSWORD: ${REDIS_PASSWORD}
AUTHENTIK_REDIS__DB: 0
AUTHENTIK_POSTGRESQL__HOST: db
AUTHENTIK_POSTGRESQL__USER: authentik
AUTHENTIK_POSTGRESQL__NAME: authentik
AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
volumes:
- authentik_media:/media
- authentik_templates:/templates
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks:
- stack_net
# ─── Plane ─────────────────────────────────────────────────────────────────
plane-api:
image: makeplane/plane-backend:${PLANE_VERSION:-v0.25.0}
restart: unless-stopped
command: ./bin/takeoff
environment:
DEBUG: "0"
DJANGO_SETTINGS_MODULE: plane.settings.production
DATABASE_URL: postgresql://plane:${PLANE_DB_PASSWORD}@db:5432/plane
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/1
AWS_REGION: us-east-1
AWS_ACCESS_KEY_ID: ${MINIO_ROOT_USER}
AWS_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
AWS_S3_BUCKET_NAME: uploads
AWS_S3_ENDPOINT_URL: http://minio:9000
FILE_SIZE_LIMIT: 5368709120
WEB_URL: https://${PLANE_DOMAIN}
CORS_ALLOWED_ORIGINS: https://${PLANE_DOMAIN}
SECRET_KEY: ${PLANE_SECRET_KEY}
EMAIL_BACKEND: django.core.mail.backends.smtp.EmailBackend
EMAIL_HOST: ${SMTP_ADDRESS}
EMAIL_HOST_USER: ${SMTP_USERNAME}
EMAIL_HOST_PASSWORD: ${SMTP_PASSWORD}
EMAIL_PORT: ${SMTP_PORT:-587}
EMAIL_USE_TLS: "1"
DEFAULT_FROM_EMAIL: ${PLANE_FROM_EMAIL}
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
minio:
condition: service_healthy
networks:
- stack_net
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8000/api/ || exit 1"]
interval: 20s
timeout: 5s
retries: 5
plane-worker:
image: makeplane/plane-backend:${PLANE_VERSION:-v0.25.0}
restart: unless-stopped
command: ./bin/worker
environment:
DEBUG: "0"
DJANGO_SETTINGS_MODULE: plane.settings.production
DATABASE_URL: postgresql://plane:${PLANE_DB_PASSWORD}@db:5432/plane
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/1
AWS_REGION: us-east-1
AWS_ACCESS_KEY_ID: ${MINIO_ROOT_USER}
AWS_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
AWS_S3_BUCKET_NAME: uploads
AWS_S3_ENDPOINT_URL: http://minio:9000
SECRET_KEY: ${PLANE_SECRET_KEY}
depends_on:
plane-api:
condition: service_healthy
networks:
- stack_net
plane-beat:
image: makeplane/plane-backend:${PLANE_VERSION:-v0.25.0}
restart: unless-stopped
command: ./bin/beat
environment:
DEBUG: "0"
DJANGO_SETTINGS_MODULE: plane.settings.production
DATABASE_URL: postgresql://plane:${PLANE_DB_PASSWORD}@db:5432/plane
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/1
SECRET_KEY: ${PLANE_SECRET_KEY}
depends_on:
plane-api:
condition: service_healthy
networks:
- stack_net
plane-web:
image: makeplane/plane-frontend:${PLANE_VERSION:-v0.25.0}
restart: unless-stopped
environment:
NEXT_PUBLIC_API_BASE_URL: https://${PLANE_DOMAIN}
NEXT_PUBLIC_WEB_BASE_URL: https://${PLANE_DOMAIN}
NEXT_PUBLIC_SPACE_BASE_URL: https://${PLANE_DOMAIN}/spaces
NEXT_PUBLIC_ADMIN_BASE_URL: https://${PLANE_DOMAIN}/god-mode
depends_on:
plane-api:
condition: service_healthy
networks:
- stack_net
plane-space:
image: makeplane/plane-space:${PLANE_VERSION:-v0.25.0}
restart: unless-stopped
environment:
NEXT_PUBLIC_API_BASE_URL: https://${PLANE_DOMAIN}
NEXT_PUBLIC_WEB_BASE_URL: https://${PLANE_DOMAIN}
NEXT_PUBLIC_SPACE_BASE_URL: https://${PLANE_DOMAIN}/spaces
depends_on:
plane-api:
condition: service_healthy
networks:
- stack_net
plane-admin:
image: makeplane/plane-admin:${PLANE_VERSION:-v0.25.0}
restart: unless-stopped
environment:
NEXT_PUBLIC_API_BASE_URL: https://${PLANE_DOMAIN}
NEXT_PUBLIC_ADMIN_BASE_URL: https://${PLANE_DOMAIN}/god-mode
depends_on:
plane-api:
condition: service_healthy
networks:
- stack_net
# ─── Outline ───────────────────────────────────────────────────────────────
outline:
image: outlinewiki/outline:${OUTLINE_VERSION:-0.79.0}
restart: unless-stopped
environment:
NODE_ENV: production
SECRET_KEY: ${OUTLINE_SECRET_KEY}
UTILS_SECRET: ${OUTLINE_UTILS_SECRET}
DATABASE_URL: postgres://outline:${OUTLINE_DB_PASSWORD}@db:5432/outline
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/2
URL: https://${WIKI_DOMAIN}
PORT: 3000
# OIDC via Authentik — fill in after Authentik application is created
OIDC_CLIENT_ID: ${OUTLINE_OIDC_CLIENT_ID:-}
OIDC_CLIENT_SECRET: ${OUTLINE_OIDC_CLIENT_SECRET:-}
OIDC_AUTH_URI: https://${SSO_DOMAIN}/application/o/authorize/
OIDC_TOKEN_URI: https://${SSO_DOMAIN}/application/o/token/
OIDC_USERINFO_URI: https://${SSO_DOMAIN}/application/o/userinfo/
OIDC_DISPLAY_NAME: SSO Login
OIDC_USERNAME_CLAIM: email
OIDC_SCOPES: "openid email profile"
# File storage via local (switch to S3/MinIO for production at scale)
FILE_STORAGE: local
FILE_STORAGE_LOCAL_ROOT_DIR: /var/lib/outline/data
# SMTP
SMTP_HOST: ${SMTP_ADDRESS}
SMTP_PORT: ${SMTP_PORT:-587}
SMTP_USERNAME: ${SMTP_USERNAME}
SMTP_PASSWORD: ${SMTP_PASSWORD}
SMTP_FROM_EMAIL: ${OUTLINE_FROM_EMAIL}
SMTP_SECURE: "false"
volumes:
- outline_data:/var/lib/outline/data
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks:
- stack_net
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/_health || exit 1"]
interval: 15s
timeout: 5s
retries: 5
# ─── Caddy TLS proxy ──────────────────────────────────────────────────────
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
authentik-server:
condition: service_healthy
plane-api:
condition: service_healthy
outline:
condition: service_healthy
networks:
- stack_net
volumes:
db_data:
redis_data:
minio_data:
authentik_media:
authentik_templates:
outline_data:
caddy_data:
caddy_config:
networks:
stack_net:
driver: bridge
Database init script
Create scripts/init-db.sh to provision separate databases and users for each application:
#!/bin/bash
# scripts/init-db.sh — runs on first postgres start
set -e
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL
CREATE USER authentik WITH PASSWORD '${AUTHENTIK_DB_PASSWORD}';
CREATE DATABASE authentik OWNER authentik;
CREATE USER plane WITH PASSWORD '${PLANE_DB_PASSWORD}';
CREATE DATABASE plane OWNER plane;
CREATE USER outline WITH PASSWORD '${OUTLINE_DB_PASSWORD}';
CREATE DATABASE outline OWNER outline;
EOSQL
Make it executable: chmod +x scripts/init-db.sh
Caddyfile
# Caddyfile — Plane + Outline + Authentik
# Authentik SSO
sso.yourdomain.com {
reverse_proxy authentik-server:9000
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options nosniff
}
}
# Plane project management
issues.yourdomain.com {
# API requests
handle /api/* {
reverse_proxy plane-api:8000
}
# Spaces (public project boards)
handle /spaces/* {
reverse_proxy plane-space:3000
}
# Admin panel (god-mode)
handle /god-mode/* {
reverse_proxy plane-admin:3000
}
# Everything else — frontend
handle {
reverse_proxy plane-web:3000
}
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options nosniff
X-Frame-Options SAMEORIGIN
}
}
# Outline wiki
wiki.yourdomain.com {
reverse_proxy outline:3000
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options nosniff
X-Frame-Options SAMEORIGIN
}
}
.env file
# .env — keep out of version control
# Versions
PLANE_VERSION=v0.25.0
OUTLINE_VERSION=0.79.0
AUTHENTIK_VERSION=2024.12.3
# Domains
SSO_DOMAIN=sso.yourdomain.com
PLANE_DOMAIN=issues.yourdomain.com
WIKI_DOMAIN=wiki.yourdomain.com
# Shared PostgreSQL superuser
POSTGRES_PASSWORD=change-me-postgres-superuser-password
# Authentik
AUTHENTIK_DB_PASSWORD=change-me-authentik-db-password
# Generate with: openssl rand -hex 32
AUTHENTIK_SECRET_KEY=
# Plane
PLANE_DB_PASSWORD=change-me-plane-db-password
# Generate with: openssl rand -hex 50
PLANE_SECRET_KEY=
# Outline
OUTLINE_DB_PASSWORD=change-me-outline-db-password
# Generate with: openssl rand -hex 32
OUTLINE_SECRET_KEY=
# Generate with: openssl rand -hex 32
OUTLINE_UTILS_SECRET=
# MinIO (Plane file storage)
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=change-me-minio-password
# Shared Redis
REDIS_PASSWORD=change-me-redis-password
# Outline OIDC — fill in after Authentik application is created (Step 7)
OUTLINE_OIDC_CLIENT_ID=
OUTLINE_OIDC_CLIENT_SECRET=
# SMTP (Postmark, Mailgun, Resend, etc.)
SMTP_ADDRESS=smtp.postmarkapp.com
SMTP_PORT=587
SMTP_USERNAME=your-postmark-token
SMTP_PASSWORD=your-postmark-token
PLANE_FROM_EMAIL=issues@yourdomain.com
OUTLINE_FROM_EMAIL=wiki@yourdomain.com
Generate all secrets at once:
echo "AUTHENTIK_SECRET_KEY=$(openssl rand -hex 32)"
echo "PLANE_SECRET_KEY=$(openssl rand -hex 50)"
echo "OUTLINE_SECRET_KEY=$(openssl rand -hex 32)"
echo "OUTLINE_UTILS_SECRET=$(openssl rand -hex 32)"
echo "REDIS_PASSWORD=$(openssl rand -hex 32)"
echo "POSTGRES_PASSWORD=$(openssl rand -hex 32)"
First-run setup (step by step)
Step 1 of 9: Point DNS
sso.yourdomain.com. A <your-vps-ip>
issues.yourdomain.com. A <your-vps-ip>
wiki.yourdomain.com. A <your-vps-ip>
Step 2 of 9: Start shared infrastructure
docker compose up -d db redis minio
docker compose ps # wait for db and minio to be healthy (up to 90 s)
Step 3 of 9: Create the MinIO bucket for Plane
docker compose exec minio mc alias set local http://localhost:9000 ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD}
docker compose exec minio mc mb local/uploads
docker compose exec minio mc anonymous set download local/uploads
Step 4 of 9: Start Authentik
docker compose up -d authentik-server authentik-worker
docker compose logs authentik-server -f
# Wait for: "Starting web server"
# First start takes 30–60 seconds for migrations
Step 5 of 9: Start Plane
docker compose up -d plane-api
docker compose logs plane-api -f
# Wait for: "System check identified no issues"
# Run Plane migrations
docker compose run --rm plane-api python manage.py migrate
# Create the Plane superuser
docker compose run --rm plane-api python manage.py create_app --name "Default" --email admin@yourdomain.com
Then start the remaining Plane services:
docker compose up -d plane-worker plane-beat plane-web plane-space plane-admin
Step 6 of 9: Start Outline
docker compose up -d outline
docker compose logs outline -f
# Wait for: "Outline is running"
Step 7 of 9: Start Caddy
docker compose up -d caddy
Step 8 of 9: Set up Authentik admin
Visit https://sso.yourdomain.com/if/flow/initial-setup/ and set your admin password. Then go to Directory → Users → Create to add team members.
Step 9 of 9: Verify all three services
# Authentik
curl -s https://sso.yourdomain.com/-/health/ready/
# Plane
curl -s https://issues.yourdomain.com/api/ | grep -q "Plane" && echo "Plane OK"
# Outline
curl -s https://wiki.yourdomain.com/_health && echo "Outline OK"
# Resource usage
docker stats --no-stream
Authentik OIDC setup
Authentik acts as the identity provider for both Plane and Outline. Configure two separate OIDC applications — one per tool.
Create the Outline OIDC application in Authentik
- Go to Applications → Providers → Create → OAuth2/OpenID Provider
- Configure:
- Name:
Outline - Client type:
Confidential - Redirect URIs:
https://wiki.yourdomain.com/auth/oidc.callback - Signing Key: select the default certificate
- Name:
- Copy the Client ID and Client Secret
- Go to Applications → Applications → Create
- Name:
Outline - Slug:
outline - Provider: select the Outline provider
- Name:
In your .env:
OUTLINE_OIDC_CLIENT_ID=<paste from Authentik>
OUTLINE_OIDC_CLIENT_SECRET=<paste from Authentik>
Restart Outline:
docker compose up -d outline
Visit https://wiki.yourdomain.com — a "SSO Login" button will appear on the login page.
Configure Plane SSO via the admin panel
Plane's OIDC configuration is not set via environment variables — it is configured through the Plane admin panel (god-mode).
- Log in to Plane at
https://issues.yourdomain.comwith the admin account you created in Step 5 - Go to Settings → Members → SSO (or navigate to
https://issues.yourdomain.com/god-mode/) - Enable OIDC and enter:
- Client ID: create a new Authentik provider for Plane following the same steps as Outline above, with redirect URI
https://issues.yourdomain.com/auth/oidc/callback/ - Client Secret: from Authentik
- Discovery URL:
https://sso.yourdomain.com/application/o/plane/.well-known/openid-configuration
- Client ID: create a new Authentik provider for Plane following the same steps as Outline above, with redirect URI
Verify: Log out of Plane, click "Login with SSO" — you should be redirected to Authentik and returned to Plane after authentication.
One Authentik group for both applications
To give team members access to both tools in a single step:
- In Authentik, go to Directory → Groups → Create a group named
engineering-team(or similar) - Add all team members to the group
- In each Application (Outline, Plane), add a Policy/Group binding: require membership in
engineering-team
Now adding someone to engineering-team in Authentik grants them access to both Plane and Outline. Removing them from the group revokes access to both on next login.
Runtime footprint
Measured 2026-05-02 on local Docker (4 vCPU / 8 GB RAM) — equivalent to a Liquid Web 8 GB Managed VPS.
| Service | Idle RAM | Peak RAM |
|---|---|---|
| plane-api | 400 MB | 800 MB |
| plane-worker | 250 MB | 500 MB |
| plane-beat | 150 MB | 250 MB |
| plane-web | 350 MB | 550 MB |
| plane-space | 200 MB | 350 MB |
| plane-admin | 200 MB | 350 MB |
| outline | 300 MB | 600 MB |
| authentik-server | 450 MB | 700 MB |
| authentik-worker | 150 MB | 250 MB |
| db (shared postgres) | 400 MB | 650 MB |
| redis (shared) | 50 MB | 80 MB |
| minio | 150 MB | 300 MB |
| caddy | 15 MB | 25 MB |
| Total | ~3,065 MB | ~5,405 MB |
An 8 GB Managed VPS has approximately 5.9 GB free after OS overhead. This stack uses ~3.1 GB idle and peaks at ~5.4 GB under active use — leaving ~500 MB of headroom at peak. For teams with more than 20 active simultaneous users, consider a 16 GB VPS.
Cost comparison
| Linear (10 users) | Notion Team (10) | Okta (10 users) | This stack | |
|---|---|---|---|---|
| Monthly cost | $80/mo | $160/mo | $20/mo | ~$33/mo VPS |
| Total SaaS cost | — | — | — | $260/mo vs $33/mo |
| Project tracking | ✓ | Limited | ✗ | ✓ (Plane) |
| Team wiki | ✗ | ✓ | ✗ | ✓ (Outline) |
| SSO/OIDC | ✗ | ✗ | ✓ | ✓ (Authentik) |
| Roadmaps | ✓ | Manual | ✗ | ✓ (Plane) |
| Self-hostable | ✗ | ✗ | ✗ | ✓ |
| Per-seat cost | $8/user | $16/user | $2/user | $0 (unlimited) |
Prices verified 2026-05-02. Check current Liquid Web pricing at liquidweb.com/vps-hosting/managed-vps/. Linear pricing at linear.app/pricing. Notion pricing at notion.so/pricing.
When this stack isn't right for you
- You need Jira's issue hierarchy depth — Plane has issues, cycles (sprints), and modules, but Jira's epic → story → subtask → sub-subtask depth is deeper. If your PM process depends on that granularity, Plane will feel limiting.
- You need Confluence's macro ecosystem — Outline is a clean Markdown wiki. It doesn't have Jira integration macros, embedded Lucidchart diagrams, or the full Confluence page-properties macro set. It's simpler by design.
- Your team is larger than 30 users — the shared PostgreSQL setup on a single VPS is suitable for small engineering teams. At 30+ active users, consider migrating to a Liquid Web Managed Database (quote-based) for the database tier.
- You need Okta's advanced MFA policies — Authentik supports TOTP and WebAuthn MFA but lacks Okta's device trust, risk scoring, and compliance reporting. For SOC 2 or HIPAA environments, evaluate whether Authentik's audit logging meets your requirements before replacing Okta.
- Plane is pre-1.0 — Plane is under active development. APIs and data formats may change between minor versions. Always read the release notes before upgrading and take a
pg_dumpbackup first.
This is almost always a session or cookie problem. Check that `SECRET_KEY` and `UTILS_SECRET` in Outline's .env are both set to non-empty 32-character+ random strings — Outline uses these to sign session cookies. Also ensure the `URL` in .env exactly matches the URL you're visiting in your browser (no trailing slash, correct scheme). If the values look correct, clear your browser cookies for the wiki domain and try again. If the problem persists across browsers, check that your Caddy `reverse_proxy` is not stripping the `Set-Cookie` header.
Plane uses MinIO for all file and attachment storage. The most common cause is a missing bucket. Run: `docker compose exec minio mc alias set local http://minio:9000 $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD && docker compose exec minio mc mb local/uploads`. Also verify that `AWS_S3_ENDPOINT_URL: http://minio:9000` is set in the plane-api environment and that the MinIO container is healthy (`docker compose ps minio`). File uploads will silently fail if MinIO is unhealthy at startup time — restart plane-api after confirming MinIO is running.
Plane's OIDC configuration lives in the admin panel, not in environment variables. Navigate to `https://issues.yourdomain.com/god-mode/` (the Plane admin panel, also called god-mode) → Settings → SSO → enable OIDC. Enter the Authentik application's Client ID, Client Secret, and the discovery URL: `https://sso.yourdomain.com/application/o/plane/.well-known/openid-configuration`. You must create a separate Authentik OIDC provider for Plane (distinct from the Outline provider) with the correct redirect URI: `https://issues.yourdomain.com/auth/oidc/callback/`.

Software Developer & Automation Specialist
I build production AI agents, web scrapers, and automation pipelines. Most of what I publish here comes from the actual problems they run into: proxies that get banned, anti-bot stacks that fingerprint your client, RAG that drifts when the underlying data moves. Stack: Python, TypeScript, Go, FastAPI, LangChain, Crawlee, Playwright, deployed on AWS, GCP, and Cloudflare.
