Skip to main content

Plane + Outline + Authentik: Self-Hosted Team OS on Liquid Web (2026)

· 15 min read
Yassine El Haddad
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.

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:

What you get

ToolRoleReplaces
PlaneIssue tracking, sprints, roadmaps, cyclesLinear, Jira
OutlineTeam wiki, documentation, meeting notesNotion, Confluence
AuthentikOIDC identity provider, single sign-onOkta, Auth0, Google Workspace SSO
PostgreSQL 16Shared database — three schemas (authentik, plane, outline)
RedisShared queue/cache (DB 0 = Authentik, DB 1 = Plane, DB 2 = Outline)
MinIOS3-compatible object storage — Plane file uploadsAWS S3
CaddyTLS 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

  1. Go to Applications → Providers → Create → OAuth2/OpenID Provider
  2. Configure:
    • Name: Outline
    • Client type: Confidential
    • Redirect URIs: https://wiki.yourdomain.com/auth/oidc.callback
    • Signing Key: select the default certificate
  3. Copy the Client ID and Client Secret
  4. Go to Applications → Applications → Create
    • Name: Outline
    • Slug: outline
    • Provider: select the Outline provider

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).

  1. Log in to Plane at https://issues.yourdomain.com with the admin account you created in Step 5
  2. Go to Settings → Members → SSO (or navigate to https://issues.yourdomain.com/god-mode/)
  3. 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

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:

  1. In Authentik, go to Directory → Groups → Create a group named engineering-team (or similar)
  2. Add all team members to the group
  3. 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.

ServiceIdle RAMPeak RAM
plane-api400 MB800 MB
plane-worker250 MB500 MB
plane-beat150 MB250 MB
plane-web350 MB550 MB
plane-space200 MB350 MB
plane-admin200 MB350 MB
outline300 MB600 MB
authentik-server450 MB700 MB
authentik-worker150 MB250 MB
db (shared postgres)400 MB650 MB
redis (shared)50 MB80 MB
minio150 MB300 MB
caddy15 MB25 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 trackingLimited✓ (Plane)
Team wiki✓ (Outline)
SSO/OIDC✓ (Authentik)
RoadmapsManual✓ (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_dump backup first.
Frequently Asked Questions

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/`.

Photo of Yassine El Haddad

Yassine El Haddad

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.