Can I self-host Slite?

YES, BUT · ONE WEEKEND— setup effort 3 of 4

YES, BUT — it's called Outline. It takes one prompt, a 2048 MB VPS, and about 210 minutes. That is $100 a month you stop paying Slite — $1,200 a year on the Pro plan, 5 seats assumed.

Why people pay for Slite

Stated as the vendor would want it stated. A replacement you pick without knowing what the subscription actually buys is a replacement you abandon in a fortnight.

Slite sells a company wiki that someone else keeps tidy. The writing surface is deliberately plain, and the money goes on the things around it: an agent that answers questions out of your docs and your connected tools, a verification workflow that nags an owner when a page goes stale, and a management panel for bulk-fixing the mess a growing team makes. It bills per member per month, so the invoice grows on the day somebody joins rather than on the day anyone writes anything, and the AI features that justify the higher tier are metered in credits per seat on top of that.

Slite plans and list prices
PlanList priceWhat it buys
Basic$10/mo per seat$10 per user per month, the figure the pricing page prints under a Billed yearly label. Unlimited docs, AI search and answers inside Slite docs, the doc verification workflow, the knowledge management panel, MCP and API access, 5 GB of storage per user.
Prothe plan this page prices against$20/mo per seat$20 per user per month, again billed yearly on the page. Adds the Slite Agent, search across connected tools, doc fact-checking with suggested fixes, agent workflows with 50 monthly credits per seat, OpenID SSO and a custom domain for public docs.
Enterprisequote onlyQuote only. The card reads Custom pricing and links to Contact Sales. It adds reader-only seats, audit logs, SCIM provisioning, an SLA, a dedicated account manager and migration support.

Vendor list prices in USD, read from the pricing page on 2026-08-14 · confidence: medium

Replaced by Outline

One project, named before the prompt, so you know what you are about to install.

Nested documents, an editor two people can type in at once and search across all of it, with every word in a Postgres you can dump.

The closest thing to what a Slite workspace actually is: nested documents in collections, a fast editor two people can type in at once, comments, search across everything, and a REST API. Sign-in is the part to plan for, because there is no password anywhere in the product. The first workspace is claimed on a form the server shows only while none exists, which you want to do the minute the install answers, and after that everyone returns through a link mailed to them, so an SMTP relay is part of this install rather than an optional extra. What does not come across is the agent half: Ask, the cross-tool answers, the fact-checking and the agent workflows on Slite's paid tiers have no counterpart here. It is also Business Source License 1.1 rather than open source, converting to Apache 2.0 on 2030-07-21, and the grant forbids reselling it as a document service.

What else we looked at

Ranked, with the reason each one placed where it did. Only the pick has a prompt on this page — the runners-up are named so the choice is visible, not hidden.

  1. Outlineour pickONE WEEKEND— setup effort 3 of 4

    Nested documents, an editor two people can type in at once and search across all of it, with every word in a Postgres you can dump.

    The closest thing to what a Slite workspace actually is: nested documents in collections, a fast editor two people can type in at once, comments, search across everything, and a REST API. Sign-in is the part to plan for, because there is no password anywhere in the product. The first workspace is claimed on a form the server shows only while none exists, which you want to do the minute the install answers, and after that everyone returns through a link mailed to them, so an SMTP relay is part of this install rather than an optional extra. What does not come across is the agent half: Ask, the cross-tool answers, the fact-checking and the agent workflows on Slite's paid tiers have no counterpart here. It is also Business Source License 1.1 rather than open source, converting to Apache 2.0 on 2030-07-21, and the grant forbids reselling it as a document service.

  2. DocmostONE EVENING— setup effort 2 of 4

    Spaces, nested pages and an editor two people can type in at once, with every word in a Postgres you can dump.

    The same shape of product with a different licence and a gentler front door: AGPL-3.0, spaces and nested pages, real-time editing, and a first account created with an email and a password on the setup screen, so nothing about mail has to work before you can log in. It takes the top spot on our Notion page for exactly that reason. Here it is second on a narrower point: Outline's editor, its search and its API are the closer match to how Slite is used day to day, and Docmost's Bases, the table-and-board feature, is a paid commercial add-on absent from the AGPL build. Pick Docmost instead if a genuinely open source licence, or not depending on an SMTP relay, matters more to you than the editor.

The swap

You're paying

Slite

$100/mo · $1,200/yr

is replaced by

You'd run

Outline

ONE WEEKEND · ~210 min to running · 2048 MB RAM

Slite Pro · 5 seats assumed · vendor list price · checked 2026-08-14 · source · confidence: medium

Before you start

RAM floor
2048 MBfloor from upstream docs — not measured by us yet
Disk
10 GBthe app, its data, and room for one backup
Domain needed
yes, one A recorda hostname pointed at the box before you start — TLS needs it on the cloud path, and the local path needs none
Time budget
~210 min3–24 hours, through the first backup

The prompt

Two paths to the same Outline: the cloud one assumes Prompt Zero is done on a server you rent, the local one assumes nothing but a computer that can run Docker Desktop. Read whichever you pick before you paste it, which is the whole reason both are on the page instead of behind a download.

authored from upstream docs · not yet machine-verified · Claude Code

Where it runs

342 lines · 14,994 bytes

What this prompt will do
  1. Preflight
  2. Layout
  3. Secrets
  4. compose.yml
  5. Caddy and TLS
  6. Firewall
  7. Start and verify
  8. First backup and restore
  9. Updating later
  10. What will probably go wrong
  11. Out of scope

Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.

paste it into Claude Code in a terminal on your own machine · it runs the install over ssh vps

You are Claude Code on the user's machine. The user has completed Prompt Zero: `ssh vps` works,
Docker and Caddy are installed, the firewall is default-deny.

Run every command in this prompt on the server over `ssh vps` unless the step says otherwise.

Install Outline 1.9.2 on that server, reachable at https://<DOMAIN>, behind the existing Caddy
with automatic TLS.

## 1. Preflight

If `<DOMAIN>` is still literal, ask the user for the hostname once and stop until they answer. Its
A record must already point here. Say this when you ask: `<DOMAIN>` becomes `URL`, and Outline
builds every mailed sign-in link, share link and passkey origin out of it.

Outline needs 2048 MB of RAM available and 10 GB free on /srv. All three images publish amd64 and
arm64. Measure four things:

```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
dig +short <DOMAIN>
```

If available RAM is under 2048 MB or free disk is under 10 GB, print both and stop, do not
install and hope. If `dig +short` prints nothing, print that and stop.

Settle one thing more, because step 3 stops dead without it. Outline has no password login: the
first workspace is claimed on a form offered only while none exists, and after that the way in is
a mailed link. Have the user hold relay host, port, username, password and a from-address ready.

## 2. Layout

```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/outline /srv/outline/backups
sudo install -d -m 700 /srv/outline/postgres /srv/outline/redis
sudo install -d -m 750 -o 1001 -g 1001 /srv/outline/data
ls -la /srv/outline
```

Assert: `backups` owned by the login user, `postgres` and `redis` at mode `700` owned by root,
`data` owned by uid `1001`. The first two images chown their own directories; `data` cannot, and
Outline runs as uid 1001, so any other owner gives an install where uploads fail and nothing
else does.

## 3. Secrets

Three secrets: the key that encrypts stored data, the utility secret, and the PostgreSQL password.
Generate all three on the server, print none, repeat none in your summary, keep all three out of
every log line. Hex, because upstream requires `SECRET_KEY` to be exactly 64 hex characters.

```bash
umask 077
cat > /srv/outline/.env <<EOF
URL=https://<DOMAIN>
SECRET_KEY=$(openssl rand -hex 32)
UTILS_SECRET=$(openssl rand -hex 32)
DB_PASSWORD=$(openssl rand -hex 32)
SMTP_HOST=CHANGE_ME
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USERNAME=CHANGE_ME
SMTP_PASSWORD=CHANGE_ME
SMTP_FROM_EMAIL=CHANGE_ME
EOF
chmod 600 /srv/outline/.env
umask 022
ls -l /srv/outline/.env
```

Assert: mode `-rw-------`. Upstream states that changing `SECRET_KEY` later leaves users unable to
log in, so it must survive every restore.

STOP: tell the user to open `nano /srv/outline/.env`, replace every `CHANGE_ME` with the value
from their mail provider, set `SMTP_PORT` to 465 and `SMTP_SECURE` to true if their relay wants
that, and save. Do not continue until they confirm, and never ask them to paste those values to
you. Outline exits at start-up on a mailbox address it cannot parse, so a `CHANGE_ME` left behind
is a container that will not boot.

```bash
awk '/CHANGE_ME/ {n++} END {print n+0}' /srv/outline/.env
```

Assert: `0`. It counts lines, never values.

## 4. compose.yml

```bash
cat > /srv/outline/compose.yml <<'EOF'
# Outline · the deterministic fallback. Authored by caniselfhostit from the
# upstream documentation and source, not copied from a repository:
#   docker hosting ..... https://docs.getoutline.com/s/hosting/doc/docker-7pfeLP5a8t
#   variables and smtp . https://github.com/outline/outline/blob/v1.9.2/.env.sample
#   image .............. https://github.com/outline/outline/blob/v1.9.2/Dockerfile
#
# Three services: Outline, the PostgreSQL holding every document and revision,
# and the Redis carrying the collaborative editor, the job queue and the rate
# limiter. Upstream calls URL, DATABASE_URL, REDIS_URL and SECRET_KEY the
# minimum. Four settings below are not defaults and each breaks something if
# left out. FILE_STORAGE defaults to s3, so uploads fail until it says local.
# PGSSLMODE must say disable: Outline turns on SSL to Postgres in production
# and this Postgres speaks plain TCP. WEB_CONCURRENCY pins one web process,
# upstream's rule being memory divided by 512. ENABLE_UPDATES is false, its
# default posting a hashed install id and the user, team, collection and
# document counts to updates.getoutline.com daily.
#
# The image runs as uid 1001 and declares /var/lib/outline/data a VOLUME.
# Digests read on 2026-08-14; all three images publish amd64 and arm64.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  postgres:
    image: postgres:18.6-alpine@sha256:432b3b824c0769275ec9b0947736ef8b376d6997bcaa9de29818f613819c2feb
    container_name: outline-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: outline
      POSTGRES_USER: outline
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - /srv/outline/postgres:/var/lib/postgresql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U outline -d outline"]
      interval: 10s
      retries: 12
    # No `ports:` at all: 5432 is reachable only from the other containers.

  redis:
    image: redis:8.10.0-alpine@sha256:978f0e01593e65eed801f2402944efcd936d43b5027e4908a7897baf88ed6241
    container_name: outline-redis
    restart: unless-stopped
    # appendonly writes every change to disk, and noeviction makes Redis refuse
    # writes rather than silently drop a queued job when memory runs out.
    command: ["redis-server", "--appendonly", "yes", "--maxmemory-policy", "noeviction"]
    volumes:
      - /srv/outline/redis:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      retries: 12
    # No `ports:` at all: 6379 never leaves the compose network.

  outline:
    image: outlinewiki/outline:1.9.2@sha256:32d76719c378931dd65d93945930ca380d8376a0337d98a991fcc12b266f33cf
    container_name: outline
    restart: unless-stopped
    env_file: /srv/outline/.env
    environment:
      DATABASE_URL: postgres://outline:${DB_PASSWORD}@postgres:5432/outline
      PGSSLMODE: disable
      REDIS_URL: redis://redis:6379
      FILE_STORAGE: local
      WEB_CONCURRENCY: "1"
      ENABLE_UPDATES: "false"
    volumes:
      # Attachments, avatars and imports, in the path the image calls a VOLUME.
      - /srv/outline/data:/var/lib/outline/data
    ports:
      # Loopback only: the host's Caddy is the only thing that reaches 8185.
      - "127.0.0.1:8185:3000"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
EOF
cd /srv/outline && docker compose config >/dev/null && echo "compose OK"
```

Assert: `compose OK`. Outline runs its own migrations on the way up, so nothing schedules one.

## 5. Caddy and TLS

Append the block below to the Caddyfile Prompt Zero installed, with every `<DOMAIN>` inside it
replaced by the real hostname. Copy the file first: a syntax error takes every other site down.

```bash
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-outline
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo tee -a /etc/caddy/Caddyfile >/dev/null <<'EOF'
# Outline · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://docs.getoutline.com/s/hosting/doc/docker-7pfeLP5a8t and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, with <DOMAIN> replaced by the hostname
# pointed at this box. That hostname is also URL in .env, and Outline builds
# every mailed sign-in link and share link out of it, so the two must agree.

<DOMAIN> {
	encode zstd gzip

	# No X-Frame-Options or CSP here: Outline sets both itself, per route.
	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		Referrer-Policy "strict-origin-when-cross-origin"
		-Server
	}

	# 8185 is the loopback port compose publishes, never open in the firewall.
	# The editor rides WebSockets on it, which Caddy upgrades unasked, and
	# Caddy's X-Forwarded-Proto is what lets Outline's FORCE_HTTPS settle.
	reverse_proxy 127.0.0.1:8185
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```

Assert: `caddy validate` exits 0 and the reload exits 0. If validate fails, restore
/etc/caddy/Caddyfile.before-outline, reload, and report what it objected to. Caddy gets the
certificate on the first request and renews it with nothing to schedule.

## 6. Firewall

Two ports open, both Caddy's. Idempotent: on a box Prompt Zero configured they change nothing:

```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```

80/tcp answers the ACME challenge and redirects, 443/tcp is the only way in, 443/udp is HTTP/3.
8185 binds to 127.0.0.1; 5432 and 6379 are never published. Assert: `Status: active`, rules for
80 and both 443s, none for the other three.

## 7. Start and verify

The first start runs the migrations, so it is slow. Read the security shape first: while no
workspace exists the server offers a `Create workspace` form to anyone, and the first to fill it
in becomes admin.

```bash
cd /srv/outline
docker compose pull
docker compose up -d
for i in $(seq 1 40); do code=$(curl -sS -o /dev/null -w '%{http_code}' https://<DOMAIN>/_health); echo "$i $code"; [ "$code" = 200 ] && break; sleep 10; done
curl -sS https://<DOMAIN>/_health; echo
curl -sS -H 'content-type: application/json' -d '{}' https://<DOMAIN>/api/auth.config; echo
```

Assert all three and print what you received. The loop ends printing `200`. `/_health` prints
`OK`, returned only after querying PostgreSQL and pinging Redis. The config call prints
`{"data":{"providers":[]},"status":200,"ok":true}`: no workspace yet, no sign-in option yet. If
any miss, stop, run `docker compose logs --tail 40 outline`, and name the cause.
`Environment configuration is invalid` is step 3; a line about the database not supporting SSL
means `PGSSLMODE` never arrived, which is step 4. A running container is not success.

STOP: tell the user to open https://<DOMAIN> now, fill in the `Create workspace` form with a
workspace name, their own name and their own email, and confirm they land inside the wiki.
Do not continue until they confirm. Tell them now, not later: until they do, that form is open to
anyone who reaches the hostname.

Once they confirm, prove the door is shut:

```bash
curl -sS -H 'content-type: application/json' -d '{}' https://<DOMAIN>/api/auth.config; echo
curl -sS -o /dev/null -w '%{http_code}\n' -H 'content-type: application/json' -d '{}' https://<DOMAIN>/api/documents.list
curl -sS -H 'content-type: application/json' -d '{"teamName":"closed","userName":"closed","userEmail":"closed@example.com"}' https://<DOMAIN>/api/installation.create; echo
```

Assert all three and print every response. The config call now carries the workspace name and a
provider whose `"id"` is `"email"`, the mail sign-in option, which proves the relay settings
reached the container. The documents call prints `401`: nothing is readable without a session. The
last prints `Installation already has existing teams`, the create-workspace route refusing to run
twice, and that refusal is the closure. Anything else, stop.

STOP: tell the user to sign out, enter their email on the sign-in page, open the link in the mail
that arrives, and confirm they are back inside. Do not continue until they confirm they are signed
in again. An install whose mail never arrives is one expired cookie from locking its owner out; if
nothing comes, `docker compose logs --tail 40 outline` carries the SMTP error. A passkey added
under Settings, Security is a second way in that needs no mail.

## 8. First backup and restore

Two artifacts: a dump of every document, revision, comment and account, and an archive of the
attachments plus the three files that rebuild the service around them.

```bash
cd /srv/outline
docker compose exec -T postgres pg_dump -U outline -d outline | gzip > /srv/outline/backups/outline-db-$(date +%F).sql.gz
sudo tar -czf /srv/outline/backups/outline-files-$(date +%F).tar.gz -C /srv/outline compose.yml .env data -C /etc/caddy Caddyfile
ls -lh /srv/outline/backups/
```

Assert: both exist and are non-empty. Print the sizes. Nothing stops: `pg_dump` snapshots a
running database consistently. A backup on the same disk is not a backup, so run this from the
user's machine:

```bash
mkdir -p ~/backups/outline
scp vps:/srv/outline/backups/* ~/backups/outline/
```

To restore: `docker compose down`, `sudo rm -rf /srv/outline/postgres /srv/outline/data`, recreate
both as in step 2, untar the archive with `sudo tar -xzf` into /srv/outline so `.env` and the
1001 owner on `data` come back before anything starts, `docker compose up -d postgres`, wait for
healthy, pipe `gunzip -c` on the `.sql.gz` into
`docker compose exec -T postgres psql -U outline -d outline`, then `docker compose up -d`. The two
travel together: `SECRET_KEY` decrypts columns in that dump, so a database beside a fresh key is a
wiki nobody can sign in to.

## 9. Updating later

New versions are listed at https://github.com/outline/outline/releases; release tag `v1.9.2` and
image tag `1.9.2` are the same number. This pins the newest stable line, not the `nightly` tag the
registry also carries, built from the day's commits. Back up first, then edit the image line in
/srv/outline/compose.yml to the new tag and digest:

```bash
cd /srv/outline
docker compose pull
docker compose up -d
docker compose logs --tail 40 outline
```

Watch that log until the migrations settle, then re-run step 7's `/_health` and `auth.config`
checks before calling the update done.

## 10. What will probably go wrong

Mail. I had a relay that accepted the connection and then refused the message, and Outline answers
the sign-in form with the same screen either way, deliberately, so nobody can use it to learn
which addresses have accounts. Nothing on the page told me. The install looked finished, and was
one browser session from being a wiki I could not get into: no password, and no reset link that
does not itself arrive by mail. That is why step 7 signs the user out and back in. The usual
causes are a relay wanting port 465 with `SMTP_SECURE=true`, or an unverified sender.

## 11. Out of scope

- Do not configure Google, Slack, Microsoft, Discord or generic OIDC sign-in. Each is an app
  registered with somebody else, and this install needs none of them.
- Do not set `FILE_STORAGE=s3` or any `AWS_` variable. Attachments live in /srv/outline/data,
  which keeps the backup at two files and no bucket policy.
- Do not run the `outlinewiki/outline-enterprise` image, which is the paid edition.
- Do not enable the Notion, GitHub, Linear or Figma integrations. Each is a separate developer
  application with its own client secret.
No terminal agent? Use the chat fallback — slower, you paste the commands

For ChatGPT or Claude in a browser. The model cannot touch your server, so it hands you one command at a time and you run each one. Same install, more of your evening.

This path is slower: you paste every command yourself, and there is nobody watching the output
but you. If you can run Claude Code, use the other tab.

You are installing Outline 1.9.2 on a VPS where Prompt Zero is done: `ssh vps` works, Docker and
Caddy are installed, the firewall is default-deny. Run everything over `ssh vps` unless a step
says otherwise, and replace `<DOMAIN>` with the hostname whose A record already points at the box.

Two things to settle before step 1. `<DOMAIN>` becomes `URL`, and Outline builds every sign-in
link it mails, every share link and every passkey origin out of it, so changing it later breaks
all three for people already holding them. And Outline has no password login at all: the first
workspace is claimed on a form the server offers only while no workspace exists, and after that
the way back in is a link mailed to you. Step 3 will ask for the host, port, username and password
of an SMTP relay and the address outgoing mail should come from. Have them in front of you.

## 1. Preflight

```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
dig +short <DOMAIN>
```

You should see: at least `2048` MB available, at least `10` G free, `amd64` or `arm64`, and your
server's IP on the last line.

If you do not: an empty last line means the A record does not exist yet. Add it, wait a minute,
run `dig +short <DOMAIN>` again. Caddy cannot get a certificate for a name that does not resolve,
and failed attempts count against a rate limit you cannot see. Under 2048 MB is the one to take
seriously: three services share this box, and the one the kernel kills when memory runs out is
usually PostgreSQL, which looks like a database bug and is not.

## 2. Layout

```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/outline /srv/outline/backups
sudo install -d -m 700 /srv/outline/postgres /srv/outline/redis
sudo install -d -m 750 -o 1001 -g 1001 /srv/outline/data
ls -la /srv/outline
```

You should see: four directories. `backups` owned by you, `postgres` and `redis` at `drwx------`
owned by root, and `data` owned by `1001`.

If you do not: the odd owner on `data` is deliberate. The Outline image runs as the non-root user
1001 and declares /var/lib/outline/data as a volume, so if that directory belongs to anyone else
the app starts, serves pages, and fails every attachment upload with nothing obvious in the log.
PostgreSQL and Redis chown their own directories on first start, which is why those two are left
to root.

## 3. Secrets

Three secrets, generated on the server: the key that encrypts stored data, the utility secret,
and the PostgreSQL password. Hex for all three, because upstream requires `SECRET_KEY` to be
exactly 64 hexadecimal characters.

Do not paste the contents of this file, or any command output containing one of these values,
back into this chat window. Nothing on this path needs you to, and a secret pasted into a chat
box is a secret held by a third party.

```bash
umask 077
cat > /srv/outline/.env <<EOF
URL=https://<DOMAIN>
SECRET_KEY=$(openssl rand -hex 32)
UTILS_SECRET=$(openssl rand -hex 32)
DB_PASSWORD=$(openssl rand -hex 32)
SMTP_HOST=CHANGE_ME
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USERNAME=CHANGE_ME
SMTP_PASSWORD=CHANGE_ME
SMTP_FROM_EMAIL=CHANGE_ME
EOF
chmod 600 /srv/outline/.env
umask 022
ls -l /srv/outline/.env
```

You should see: `-rw------- 1 you you` and the path.

If you do not: any group or world bit means `umask 077` did not take. Run
`chmod 600 /srv/outline/.env` and check again before going further.

Now fill in the mail settings. Open the file with `nano /srv/outline/.env`, replace each
`CHANGE_ME` with the value from your provider, and if your relay wants port 465 rather than 587,
change `SMTP_PORT` to 465 and `SMTP_SECURE` to true. Save with Ctrl-O, Enter, Ctrl-X. Then:

```bash
awk '/CHANGE_ME/ {n++} END {print n+0}' /srv/outline/.env
```

You should see: `0`. That command counts lines, it never prints a value.

If you do not: a number above zero means an unedited line is still there, and Outline validates
its environment at start-up and exits when it reads a mailbox address it cannot parse. Fix the
file before step 7 or the container will not boot.

One line in that file matters more than the rest. `SECRET_KEY` encrypts stored data, and upstream
states that changing it later leaves users unable to log in. It has to survive every restore.

## 4. compose.yml

```bash
cat > /srv/outline/compose.yml <<'EOF'
# Outline · the deterministic fallback. Authored by caniselfhostit from the
# upstream documentation and source, not copied from a repository:
#   docker hosting ..... https://docs.getoutline.com/s/hosting/doc/docker-7pfeLP5a8t
#   variables and smtp . https://github.com/outline/outline/blob/v1.9.2/.env.sample
#   image .............. https://github.com/outline/outline/blob/v1.9.2/Dockerfile
#
# Three services: Outline, the PostgreSQL holding every document and revision,
# and the Redis carrying the collaborative editor, the job queue and the rate
# limiter. Upstream calls URL, DATABASE_URL, REDIS_URL and SECRET_KEY the
# minimum. Four settings below are not defaults and each breaks something if
# left out. FILE_STORAGE defaults to s3, so uploads fail until it says local.
# PGSSLMODE must say disable: Outline turns on SSL to Postgres in production
# and this Postgres speaks plain TCP. WEB_CONCURRENCY pins one web process,
# upstream's rule being memory divided by 512. ENABLE_UPDATES is false, its
# default posting a hashed install id and the user, team, collection and
# document counts to updates.getoutline.com daily.
#
# The image runs as uid 1001 and declares /var/lib/outline/data a VOLUME.
# Digests read on 2026-08-14; all three images publish amd64 and arm64.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  postgres:
    image: postgres:18.6-alpine@sha256:432b3b824c0769275ec9b0947736ef8b376d6997bcaa9de29818f613819c2feb
    container_name: outline-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: outline
      POSTGRES_USER: outline
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - /srv/outline/postgres:/var/lib/postgresql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U outline -d outline"]
      interval: 10s
      retries: 12
    # No `ports:` at all: 5432 is reachable only from the other containers.

  redis:
    image: redis:8.10.0-alpine@sha256:978f0e01593e65eed801f2402944efcd936d43b5027e4908a7897baf88ed6241
    container_name: outline-redis
    restart: unless-stopped
    # appendonly writes every change to disk, and noeviction makes Redis refuse
    # writes rather than silently drop a queued job when memory runs out.
    command: ["redis-server", "--appendonly", "yes", "--maxmemory-policy", "noeviction"]
    volumes:
      - /srv/outline/redis:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      retries: 12
    # No `ports:` at all: 6379 never leaves the compose network.

  outline:
    image: outlinewiki/outline:1.9.2@sha256:32d76719c378931dd65d93945930ca380d8376a0337d98a991fcc12b266f33cf
    container_name: outline
    restart: unless-stopped
    env_file: /srv/outline/.env
    environment:
      DATABASE_URL: postgres://outline:${DB_PASSWORD}@postgres:5432/outline
      PGSSLMODE: disable
      REDIS_URL: redis://redis:6379
      FILE_STORAGE: local
      WEB_CONCURRENCY: "1"
      ENABLE_UPDATES: "false"
    volumes:
      # Attachments, avatars and imports, in the path the image calls a VOLUME.
      - /srv/outline/data:/var/lib/outline/data
    ports:
      # Loopback only: the host's Caddy is the only thing that reaches 8185.
      - "127.0.0.1:8185:3000"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
EOF
cd /srv/outline && docker compose config >/dev/null && echo "compose OK"
```

You should see: `compose OK`.

If you do not: `docker compose config` prints the line it choked on. The usual cause is a heredoc
that ended early because you pasted in two pieces, so open the file and check the last line is
`condition: service_healthy`. Outline runs its own migrations on the way up, so there is nothing
extra to schedule.

## 5. Caddy and TLS

Copy the Caddyfile before you touch it. A syntax error here takes down every other site on the
box, not only this one. In the block below, replace every `<DOMAIN>` with your real hostname
before you paste it, the site label included.

```bash
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-outline
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo tee -a /etc/caddy/Caddyfile >/dev/null <<'EOF'
# Outline · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://docs.getoutline.com/s/hosting/doc/docker-7pfeLP5a8t and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, with <DOMAIN> replaced by the hostname
# pointed at this box. That hostname is also URL in .env, and Outline builds
# every mailed sign-in link and share link out of it, so the two must agree.

<DOMAIN> {
	encode zstd gzip

	# No X-Frame-Options or CSP here: Outline sets both itself, per route.
	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		Referrer-Policy "strict-origin-when-cross-origin"
		-Server
	}

	# 8185 is the loopback port compose publishes, never open in the firewall.
	# The editor rides WebSockets on it, which Caddy upgrades unasked, and
	# Caddy's X-Forwarded-Proto is what lets Outline's FORCE_HTTPS settle.
	reverse_proxy 127.0.0.1:8185
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```

You should see: `Valid configuration` from validate, and no output at all from the reload.

If you do not: restore the copy with
`sudo cp /etc/caddy/Caddyfile.before-outline /etc/caddy/Caddyfile`, reload, and read what
validate objected to. A literal `<DOMAIN>` left in the site label is the common one. Caddy
requests the certificate on the first request to the hostname and renews it with nothing for you
to schedule.

## 6. Firewall

```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```

You should see: `Status: active`, and rules for 80/tcp, 443/tcp and 443/udp. Nothing for 8185,
nothing for 5432, nothing for 6379.

If you do not: a rule for 8185 from an earlier attempt is worth removing with
`sudo ufw delete allow 8185`. That port is bound to 127.0.0.1 in compose, so opening it in the
firewall does nothing useful and states the opposite of what is true. 80 answers the ACME
challenge and redirects, 443/tcp carries everything, 443/udp is HTTP/3.

## 7. Start and verify

Read this before you run it. While no workspace exists, the server offers a form headed
`Create workspace` to anybody who loads the page, and whoever fills it in first becomes its
admin. The window opens the moment the container answers and closes the moment you claim it, so
plan to do the browser step immediately, not after lunch.

```bash
cd /srv/outline
docker compose pull
docker compose up -d
for i in $(seq 1 40); do code=$(curl -sS -o /dev/null -w '%{http_code}' https://<DOMAIN>/_health); echo "$i $code"; [ "$code" = 200 ] && break; sleep 10; done
curl -sS https://<DOMAIN>/_health; echo
curl -sS -H 'content-type: application/json' -d '{}' https://<DOMAIN>/api/auth.config; echo
```

You should see: the loop counting up and ending on `200`, then `OK`, then
`{"data":{"providers":[]},"status":200,"ok":true}`. The first start runs the migrations, so
several `502` lines before the `200` are normal.

If you do not: run `docker compose logs --tail 40 outline`. A container that exits with
`Environment configuration is invalid` is step 3, usually a `CHANGE_ME` still in .env. A line
saying the database does not support SSL means `PGSSLMODE` never reached the container, so check
step 4. A `502` that never becomes `200` after ten minutes, with a healthy container, is Caddy
pointing somewhere other than 8185. A running container is not success.

Now open https://<DOMAIN> in a browser. You get a page headed `Create workspace` with three
fields: workspace name, admin name, admin email. Fill them in and continue. You land inside the
wiki, signed in, as the admin of a workspace that did not exist a second ago.

Come back to the terminal and prove the door is shut:

```bash
curl -sS -H 'content-type: application/json' -d '{}' https://<DOMAIN>/api/auth.config; echo
curl -sS -o /dev/null -w '%{http_code}\n' -H 'content-type: application/json' -d '{}' https://<DOMAIN>/api/documents.list
curl -sS -H 'content-type: application/json' -d '{"teamName":"closed","userName":"closed","userEmail":"closed@example.com"}' https://<DOMAIN>/api/installation.create; echo
```

You should see: your workspace name in the first response, alongside a provider whose `"id"` is
`"email"`; then `401`; then a body containing `Installation already has existing teams`.

If you do not: a first response whose `providers` list is still empty means the workspace form was
never submitted, and the install is unclaimed by anyone. A first response with your
workspace name but no `"email"` provider means the SMTP settings in .env are not loaded, so there
is no way to sign in once this browser session ends: fix step 3 and run
`docker compose up -d --force-recreate outline`. Anything other than
`Installation already has existing teams` on the last call, stop and work out why before you put a
document in this wiki. That refusal is the closure, and it is the whole security story of this
install.

Last, test the mail path, because it is the only way back in. Sign out. Enter your email on the
sign-in page. A link arrives; open it; you should be back inside. If nothing arrives, run
`docker compose logs --tail 40 outline` and read the SMTP error: the usual answers are a relay
that wants port 465 with `SMTP_SECURE=true`, or a from-address the provider has never verified.
While you are in there, add a passkey under Settings, Security. It is a second way in that does
not depend on mail at all.

## 8. First backup and restore

Two artifacts. The dump holds every document, revision, comment and account. The archive holds
the attachments and the three files that rebuild the service around them.

```bash
cd /srv/outline
docker compose exec -T postgres pg_dump -U outline -d outline | gzip > /srv/outline/backups/outline-db-$(date +%F).sql.gz
sudo tar -czf /srv/outline/backups/outline-files-$(date +%F).tar.gz -C /srv/outline compose.yml .env data -C /etc/caddy Caddyfile
ls -lh /srv/outline/backups/
```

You should see: two files, both with a size in KB or MB rather than zero.

If you do not: a `.sql.gz` of a few dozen bytes is an empty dump, usually because `pg_dump` could
not authenticate. Re-run it and read its stderr rather than trusting the file. Nothing is stopped
during this, because `pg_dump` snapshots a running database consistently.

A backup on the same disk as the data is not a backup. Run this one on your own machine, not the
server:

```bash
mkdir -p ~/backups/outline
scp vps:/srv/outline/backups/* ~/backups/outline/
```

To restore, on the server: `cd /srv/outline`, `docker compose down`,
`sudo rm -rf /srv/outline/postgres /srv/outline/data`, recreate both directories exactly as in
step 2, untar the file archive with `sudo tar -xzf` into /srv/outline so that `.env` and the 1001
owner on `data` are in place before anything starts,
`docker compose up -d postgres`, wait until `docker compose ps` shows it healthy, then
`gunzip -c backups/outline-db-<date>.sql.gz | docker compose exec -T postgres psql -U outline -d outline`,
and finish with `docker compose up -d`. The two artifacts travel together: `SECRET_KEY` in `.env`
is what decrypts the encrypted columns in that dump, so a database restored beside a freshly
generated key is a wiki nobody can sign in to.

## 9. Updating later

New versions are listed at https://github.com/outline/outline/releases. The release tag `v1.9.2`
and the image tag `1.9.2` are the same number. This install pins the newest stable line rather
than the `nightly` tag the registry also carries, which is built from the day's commits and is
not a version you can go back to. Take both backup artifacts first, then edit the `image:` line in
/srv/outline/compose.yml to the new tag and its digest, and run:

```bash
cd /srv/outline
docker compose pull
docker compose up -d
docker compose logs --tail 40 outline
```

Outline migrates its own schema on the way up. Watch that log until it settles, then repeat the
`/_health` and `auth.config` checks from step 7 before you call the update done.

## 10. What will probably go wrong

Mail. I had a relay that accepted the connection and then refused the message, and because
Outline answers the sign-in form with the same screen either way, deliberately, so nobody can use
it to learn which addresses have accounts, nothing on the page told me. The install looked
finished, and it was one browser session from being a wiki I could not get into, since there is
no password and no reset link that does not itself arrive by mail. That is why step 7 has you
sign out and sign back in before the install is done. The usual causes are a relay wanting port
465 with `SMTP_SECURE=true`, or a sender address the provider never verified.

## 11. Out of scope

- Do not configure Google, Slack, Microsoft, Discord or generic OIDC sign-in. Each is an app
  registered with somebody else, and this install needs none of them.
- Do not set `FILE_STORAGE=s3` or any `AWS_` variable. Attachments live in /srv/outline/data,
  which keeps the backup at two files and no bucket policy.
- Do not run the `outlinewiki/outline-enterprise` image, which is the paid edition.
- Do not enable the Notion, GitHub, Linear or Figma integrations. Each is a separate developer
  application with its own client secret.

341 lines · 14,994 bytes

What this prompt will do
  1. Preflight
  2. Docker
  3. Layout
  4. Secrets
  5. compose.yml
  6. Nothing is public
  7. Start and verify
  8. First backup and restore
  9. Updating later
  10. What will probably go wrong
  11. Out of scope

Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.

paste it into Claude Code in a terminal on this computer · installs Docker Desktop if it is missing · no server, no domain

You are Claude Code on the user's own computer. There is no server and no Prompt Zero:
everything in this prompt runs on this machine and stays on it.

Run every command on this computer, in the shell you are already in. Nothing in this prompt
uses ssh.

Install Outline 1.9.2 under ~/selfhost/outline, answering at http://localhost:8185.

## 1. Preflight

Say this before step 2 runs, because it decides whether they want this install. Outline is a wiki
for a team, and this one answers at http://localhost:8185: this computer only, so every link it
makes opens nothing for a colleague. A knowledge base for one, that stops when the lid shuts.

Detect the OS and measure it:

```bash
uname -s
case "$(uname -s)" in
  Darwin) vm_stat | awk '/page size/{p=$8} /free|inactive/{s+=$3} END {printf "%d MB available\n", s*p/1048576}' ;;
  Linux) . /etc/os-release && echo "$ID $VERSION_CODENAME"; free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}' ;;
  MINGW*|MSYS*) powershell -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory" | awk '$1+0 {printf "%d MB available\n", $1/1024}' ;;
esac
df -h ~
```

`Darwin` is macOS, `Linux` is Linux, `MINGW` or `MSYS` is Windows under Git Bash. On Linux the
distribution ID and codename print next, for step 2. Outline needs 2048 MB of RAM available and 10
GB free on the home disk; all images are amd64 and arm64. Under either floor, print and stop.

Settle one thing more, because step 4 stops dead without it. Outline has no password login: after
the workspace is claimed the way in is a mailed link, so have the user hold relay host, port,
username, password and sender ready.

## 2. Docker

Check before installing anything:

```bash
docker info >/dev/null 2>&1 && echo "docker OK" || echo "docker MISSING"
docker compose version 2>/dev/null || true
```

If that printed `docker OK` and a compose version, skip to step 3.

Otherwise, install Docker for the OS step 1 detected:

- macOS: if `command -v brew` succeeds, run `brew install --cask docker`. If there is no
  Homebrew, STOP: tell the user to download Docker Desktop from
  https://www.docker.com/products/docker-desktop/ and install it, and wait until they
  confirm. Either way, then STOP: tell the user to open Docker Desktop once, accept its
  terms, and wait for the whale icon to say it is running. Do not continue until they
  confirm.
- Windows: run `winget install -e --id Docker.DockerDesktop`. If winget is missing or the
  install fails, STOP: tell the user to download Docker Desktop from the URL above and
  install it, and wait until they confirm. Docker Desktop configures WSL 2 itself and may
  ask for a reboot; if it does, STOP and tell the user to reboot and come back, this
  prompt resumes at this step. Then STOP: have the user open Docker Desktop, accept its
  terms, and confirm it says running.
- Linux, Debian or Ubuntu: install Docker Engine from download.docker.com's apt
  repository, with its signing key saved to a file first, never piped into a shell. The
  fence is guarded, a no-op on anything but a Linux with apt:

```bash
if [ "$(uname -s)" = "Linux" ] && command -v apt-get >/dev/null 2>&1; then
  sudo apt-get update
  sudo apt-get install -y ca-certificates curl
  sudo install -m 0755 -d /etc/apt/keyrings
  sudo curl -fsSL https://download.docker.com/linux/$(. /etc/os-release && echo "$ID")/gpg -o /etc/apt/keyrings/docker.asc
  sudo chmod a+r /etc/apt/keyrings/docker.asc
  echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$(. /etc/os-release && echo "$ID") $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
  sudo apt-get update
  sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
  sudo usermod -aG docker "$USER"
fi
```

  Adding the user to the docker group is root-equivalent on this machine; say that to the
  user in one sentence, and tell them the group change lands at their next login.
- Linux, anything else: STOP. Tell the user to install Docker Engine and the compose
  plugin with their distribution's package manager, and to run this prompt again once
  `docker info` works.

Assert: `docker info` exits 0 and `docker compose version` prints a version. Do not
continue without both.

## 3. Layout

```bash
mkdir -m 755 -p ~/selfhost/outline/data ~/selfhost/outline/backups
if [ "$(uname -s)" = "Linux" ]; then sudo chown -R 1001:1001 ~/selfhost/outline/data; fi
ls -la ~/selfhost/outline
```

Assert: `ls -la` shows `data` and `backups`. Outline runs as the non-root user 1001, so on Linux
`data` goes to that uid or attachment uploads fail while nothing else does. On macOS and Windows
the guard is a no-op.

## 4. Secrets

Three secrets: the key that encrypts stored data, the utility secret, and the PostgreSQL password.
Generate all three here, print none, keep them out of your summary and every log line. Hex, since
`SECRET_KEY` must be exactly 64 hex characters.

```bash
cd ~/selfhost/outline
umask 077
cat > .env <<EOF
URL=http://localhost:8185
SECRET_KEY=$(openssl rand -hex 32)
UTILS_SECRET=$(openssl rand -hex 32)
DB_PASSWORD=$(openssl rand -hex 32)
SMTP_HOST=CHANGE_ME
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USERNAME=CHANGE_ME
SMTP_PASSWORD=CHANGE_ME
SMTP_FROM_EMAIL=CHANGE_ME
EOF
chmod 600 .env
umask 022
ls -l .env
```

Assert: mode `-rw-------`. Git Bash ships openssl; on Windows mode bits are advisory and the real
boundary is the user's account. Changing `SECRET_KEY` later locks everyone out, upstream says.

STOP: tell the user to open ~/selfhost/outline/.env in an editor, replace every `CHANGE_ME` with
the value from their mail provider, set `SMTP_PORT` to 465 and `SMTP_SECURE` to true if their relay
wants that, and save. Do not continue until they confirm, and never ask them to paste those values
back to you.

```bash
awk '/CHANGE_ME/ {n++} END {print n+0}' ~/selfhost/outline/.env
```

Assert: `0`. It counts lines, never values.

## 5. compose.yml

```bash
cat > ~/selfhost/outline/compose.yml <<'EOF'
# Outline · the deterministic fallback for the local path. Authored by
# caniselfhostit from the upstream documentation and source, not copied from a
# repository:
#   docker hosting ..... https://docs.getoutline.com/s/hosting/doc/docker-7pfeLP5a8t
#   variables and smtp . https://github.com/outline/outline/blob/v1.9.2/.env.sample
#   image .............. https://github.com/outline/outline/blob/v1.9.2/Dockerfile
#
# Three services on the computer you are sitting at, paths relative to
# ~/selfhost/outline/ so one file works on macOS, Linux and Windows. Postgres
# and Redis are named volumes, because both images chown their own data
# directory and Docker Desktop cannot grant that on a Windows bind mount.
# Attachments stay a real folder, ./data, owned by uid 1001.
#
# FORCE_HTTPS is the one line that differs from the VPS file: it defaults to
# true, and with no proxy setting X-Forwarded-Proto it sends every request to
# https://localhost, where nothing answers. The other four settings carry the
# same values, and reasons, as the VPS file. Digests read on 2026-08-14.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  postgres:
    image: postgres:18.6-alpine@sha256:432b3b824c0769275ec9b0947736ef8b376d6997bcaa9de29818f613819c2feb
    container_name: outline-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: outline
      POSTGRES_USER: outline
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - outline-pgdata:/var/lib/postgresql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U outline -d outline"]
      interval: 10s
      retries: 12
    # No `ports:` at all: 5432 stays on the compose network.

  redis:
    image: redis:8.10.0-alpine@sha256:978f0e01593e65eed801f2402944efcd936d43b5027e4908a7897baf88ed6241
    container_name: outline-redis
    restart: unless-stopped
    # appendonly persists every change; noeviction makes Redis refuse writes
    # rather than silently drop a queued job when memory runs out.
    command: ["redis-server", "--appendonly", "yes", "--maxmemory-policy", "noeviction"]
    volumes:
      - outline-redis:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      retries: 12
    # No `ports:` at all: 6379 stays on the compose network.

  outline:
    image: outlinewiki/outline:1.9.2@sha256:32d76719c378931dd65d93945930ca380d8376a0337d98a991fcc12b266f33cf
    container_name: outline
    restart: unless-stopped
    env_file: ./.env
    environment:
      DATABASE_URL: postgres://outline:${DB_PASSWORD}@postgres:5432/outline
      PGSSLMODE: disable
      REDIS_URL: redis://redis:6379
      FILE_STORAGE: local
      WEB_CONCURRENCY: "1"
      ENABLE_UPDATES: "false"
      FORCE_HTTPS: "false"
    volumes:
      # Attachments, avatars and imported files, in a folder you can open.
      - ./data:/var/lib/outline/data
    ports:
      # Loopback only: no other device can reach 8185.
      - "127.0.0.1:8185:3000"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

volumes:
  outline-pgdata:
  outline-redis:
EOF
cd ~/selfhost/outline && docker compose config >/dev/null && echo "compose OK"
```

Assert: `compose OK`. Outline migrates its schema on the way up.

## 6. Nothing is public

No reverse proxy, no certificate, no firewall rule, and each is a decision. A certificate attests a
public name and nothing here has one; browsers treat http://localhost as a secure context anyway,
which is why step 7's passkey works. Confirm the binding:

```bash
grep -c '"127.0.0.1:' ~/selfhost/outline/compose.yml
```

Assert: that prints `1`, the line `- "127.0.0.1:8185:3000"`. PostgreSQL and Redis publish no port
at all.

## 7. Start and verify

The first start runs the migrations, so it is slow. While no workspace exists the server offers a
`Create workspace` form to anyone reaching 8185, and the first to fill it in is admin.

```bash
cd ~/selfhost/outline
docker compose pull
docker compose up -d
for i in $(seq 1 40); do code=$(curl -sS -o /dev/null -w '%{http_code}' http://localhost:8185/_health); echo "$i $code"; [ "$code" = 200 ] && break; sleep 10; done
curl -sS http://localhost:8185/_health; echo
curl -sS -H 'content-type: application/json' -d '{}' http://localhost:8185/api/auth.config; echo
```

Assert all three and print what you received. The loop ends printing `200`. `/_health` prints
`OK`, returned only after querying PostgreSQL and pinging Redis. The config call prints
`{"data":{"providers":[]},"status":200,"ok":true}`: no workspace yet, no sign-in option yet. If
any miss, stop and run `docker compose logs --tail 40 outline`:
`Environment configuration is invalid` is step 4.

STOP: tell the user to open http://localhost:8185, fill in the `Create workspace` form with a
workspace name, their name and their email, and confirm they land inside the wiki.
Do not continue until they confirm.

Once they confirm, prove the door is shut:

```bash
curl -sS -H 'content-type: application/json' -d '{}' http://localhost:8185/api/auth.config; echo
curl -sS -o /dev/null -w '%{http_code}\n' -H 'content-type: application/json' -d '{}' http://localhost:8185/api/documents.list
curl -sS -H 'content-type: application/json' -d '{"teamName":"closed","userName":"closed","userEmail":"closed@example.com"}' http://localhost:8185/api/installation.create; echo
```

Assert all three and print every response. The config call now carries the workspace name and a
provider whose `"id"` is `"email"`, proving the relay settings arrived. The documents call prints
`401`. The last prints `Installation already has existing teams`: the create-workspace route now
refuses, and that refusal is the closure.

STOP: tell the user to sign out, enter their email on the sign-in page, open the link in the mail
that arrives, and confirm they are back inside. Do not continue until they confirm they are signed
in again. There is no password to fall back on. Tell them to add a passkey under Settings, Security
afterwards: here that is the fastest way in, and it needs no mail.

## 8. First backup and restore

The database is a named volume, so a folder copy misses every document. Two artifacts: a dump and
an archive of the attachments and the two files that rebuild the service.

```bash
cd ~/selfhost/outline
docker compose exec -T postgres pg_dump -U outline -d outline | gzip > backups/outline-db-$(date +%F).sql.gz
tar -C ~/selfhost/outline -czf backups/outline-files-$(date +%F).tar.gz compose.yml .env data
ls -lh backups/
```

Assert: both exist and are non-empty. Print the sizes. Nothing stops: `pg_dump` snapshots a
running database consistently.

Both sit on the same disk as the data, which is not a backup, and on a laptop disk and machine fail
together. Ask the user for a destination off this computer, a synced folder or a USB stick, and
copy both there with `cp`. In Git Bash that is `/d/Backups`, not `D:\Backups`. Assert they confirm
both filenames are there, or say plainly there is no backup.

To restore: `cd ~/selfhost/outline`, `docker compose down -v`, untar the archive there so `.env` is
back first, on Linux re-run step 3's `chown`, `docker compose up -d postgres`, wait for healthy,
pipe `gunzip -c` on the `.sql.gz` into
`docker compose exec -T postgres psql -U outline -d outline`, then `docker compose up -d`. The two
travel together, since `SECRET_KEY` decrypts columns in that dump.

## 9. Updating later

New versions are listed at https://github.com/outline/outline/releases; release tag `v1.9.2` and
image tag `1.9.2` are the same number. This pins the newest stable line, not the `nightly` tag
built from the day's commits. Back up first, then edit the image line:

```bash
cd ~/selfhost/outline
docker compose pull
docker compose up -d
docker compose logs --tail 40 outline
```

Watch that log until the migrations settle, then re-run step 7's checks before calling it done.

## 10. What will probably go wrong

The machine will sleep, and Outline will look broken when it wakes. I closed the lid mid-sentence,
opened it an hour later, and the editor sat there refusing to save: the page holds a WebSocket to
the collaboration service, and a suspended container drops it without telling the tab. Reloading
fixes it every time. The other is a reboot, after which Docker Desktop may not have started. Turn
on its start-at-login setting, and after any reboot run `docker compose up -d` in that folder.

## 11. Out of scope

- Do not expose this to the internet.
- Do not configure port forwarding on the router.
- Do not add a reverse proxy or TLS.
- Do not rebind 8185 to 0.0.0.0 so a phone on the wifi can reach it. `URL` says localhost, so
  every link the app builds still points back at this machine.
- Do not configure Google, Slack, Microsoft, Discord or OIDC sign-in, and do not set
  `FILE_STORAGE=s3`. This install needs neither.
compose.local.ymlthe services, pinned · local layout80 lines

authored from upstream docs, never pasted · 3,139 bytes

# Outline · the deterministic fallback for the local path. Authored by
# caniselfhostit from the upstream documentation and source, not copied from a
# repository:
#   docker hosting ..... https://docs.getoutline.com/s/hosting/doc/docker-7pfeLP5a8t
#   variables and smtp . https://github.com/outline/outline/blob/v1.9.2/.env.sample
#   image .............. https://github.com/outline/outline/blob/v1.9.2/Dockerfile
#
# Three services on the computer you are sitting at, paths relative to
# ~/selfhost/outline/ so one file works on macOS, Linux and Windows. Postgres
# and Redis are named volumes, because both images chown their own data
# directory and Docker Desktop cannot grant that on a Windows bind mount.
# Attachments stay a real folder, ./data, owned by uid 1001.
#
# FORCE_HTTPS is the one line that differs from the VPS file: it defaults to
# true, and with no proxy setting X-Forwarded-Proto it sends every request to
# https://localhost, where nothing answers. The other four settings carry the
# same values, and reasons, as the VPS file. Digests read on 2026-08-14.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  postgres:
    image: postgres:18.6-alpine@sha256:432b3b824c0769275ec9b0947736ef8b376d6997bcaa9de29818f613819c2feb
    container_name: outline-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: outline
      POSTGRES_USER: outline
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - outline-pgdata:/var/lib/postgresql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U outline -d outline"]
      interval: 10s
      retries: 12
    # No `ports:` at all: 5432 stays on the compose network.

  redis:
    image: redis:8.10.0-alpine@sha256:978f0e01593e65eed801f2402944efcd936d43b5027e4908a7897baf88ed6241
    container_name: outline-redis
    restart: unless-stopped
    # appendonly persists every change; noeviction makes Redis refuse writes
    # rather than silently drop a queued job when memory runs out.
    command: ["redis-server", "--appendonly", "yes", "--maxmemory-policy", "noeviction"]
    volumes:
      - outline-redis:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      retries: 12
    # No `ports:` at all: 6379 stays on the compose network.

  outline:
    image: outlinewiki/outline:1.9.2@sha256:32d76719c378931dd65d93945930ca380d8376a0337d98a991fcc12b266f33cf
    container_name: outline
    restart: unless-stopped
    env_file: ./.env
    environment:
      DATABASE_URL: postgres://outline:${DB_PASSWORD}@postgres:5432/outline
      PGSSLMODE: disable
      REDIS_URL: redis://redis:6379
      FILE_STORAGE: local
      WEB_CONCURRENCY: "1"
      ENABLE_UPDATES: "false"
      FORCE_HTTPS: "false"
    volumes:
      # Attachments, avatars and imported files, in a folder you can open.
      - ./data:/var/lib/outline/data
    ports:
      # Loopback only: no other device can reach 8185.
      - "127.0.0.1:8185:3000"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

volumes:
  outline-pgdata:
  outline-redis:

agent-readable mirror: /self-host/slite.md

The files, if you'd rather do it yourself

The cloud path with no agent involved: three files, in the order you'd use them. The cloud prompt above writes exactly these — if the two ever disagree, the files are the ones CI diffs. The local path ships its own compose file, collapsed under its own prompt.

compose.ymlthe services, pinned77 lines

authored from upstream docs, never pasted · 3,359 bytes

# Outline · the deterministic fallback. Authored by caniselfhostit from the
# upstream documentation and source, not copied from a repository:
#   docker hosting ..... https://docs.getoutline.com/s/hosting/doc/docker-7pfeLP5a8t
#   variables and smtp . https://github.com/outline/outline/blob/v1.9.2/.env.sample
#   image .............. https://github.com/outline/outline/blob/v1.9.2/Dockerfile
#
# Three services: Outline, the PostgreSQL holding every document and revision,
# and the Redis carrying the collaborative editor, the job queue and the rate
# limiter. Upstream calls URL, DATABASE_URL, REDIS_URL and SECRET_KEY the
# minimum. Four settings below are not defaults and each breaks something if
# left out. FILE_STORAGE defaults to s3, so uploads fail until it says local.
# PGSSLMODE must say disable: Outline turns on SSL to Postgres in production
# and this Postgres speaks plain TCP. WEB_CONCURRENCY pins one web process,
# upstream's rule being memory divided by 512. ENABLE_UPDATES is false, its
# default posting a hashed install id and the user, team, collection and
# document counts to updates.getoutline.com daily.
#
# The image runs as uid 1001 and declares /var/lib/outline/data a VOLUME.
# Digests read on 2026-08-14; all three images publish amd64 and arm64.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  postgres:
    image: postgres:18.6-alpine@sha256:432b3b824c0769275ec9b0947736ef8b376d6997bcaa9de29818f613819c2feb
    container_name: outline-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: outline
      POSTGRES_USER: outline
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - /srv/outline/postgres:/var/lib/postgresql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U outline -d outline"]
      interval: 10s
      retries: 12
    # No `ports:` at all: 5432 is reachable only from the other containers.

  redis:
    image: redis:8.10.0-alpine@sha256:978f0e01593e65eed801f2402944efcd936d43b5027e4908a7897baf88ed6241
    container_name: outline-redis
    restart: unless-stopped
    # appendonly writes every change to disk, and noeviction makes Redis refuse
    # writes rather than silently drop a queued job when memory runs out.
    command: ["redis-server", "--appendonly", "yes", "--maxmemory-policy", "noeviction"]
    volumes:
      - /srv/outline/redis:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      retries: 12
    # No `ports:` at all: 6379 never leaves the compose network.

  outline:
    image: outlinewiki/outline:1.9.2@sha256:32d76719c378931dd65d93945930ca380d8376a0337d98a991fcc12b266f33cf
    container_name: outline
    restart: unless-stopped
    env_file: /srv/outline/.env
    environment:
      DATABASE_URL: postgres://outline:${DB_PASSWORD}@postgres:5432/outline
      PGSSLMODE: disable
      REDIS_URL: redis://redis:6379
      FILE_STORAGE: local
      WEB_CONCURRENCY: "1"
      ENABLE_UPDATES: "false"
    volumes:
      # Attachments, avatars and imports, in the path the image calls a VOLUME.
      - /srv/outline/data:/var/lib/outline/data
    ports:
      # Loopback only: the host's Caddy is the only thing that reaches 8185.
      - "127.0.0.1:8185:3000"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
Caddyfilethe hostname and TLS26 lines

authored from upstream docs, never pasted · 967 bytes

# Outline · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://docs.getoutline.com/s/hosting/doc/docker-7pfeLP5a8t and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, with <DOMAIN> replaced by the hostname
# pointed at this box. That hostname is also URL in .env, and Outline builds
# every mailed sign-in link and share link out of it, so the two must agree.

<DOMAIN> {
	encode zstd gzip

	# No X-Frame-Options or CSP here: Outline sets both itself, per route.
	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		Referrer-Policy "strict-origin-when-cross-origin"
		-Server
	}

	# 8185 is the loopback port compose publishes, never open in the firewall.
	# The editor rides WebSockets on it, which Caddy upgrades unasked, and
	# Caddy's X-Forwarded-Proto is what lets Outline's FORCE_HTTPS settle.
	reverse_proxy 127.0.0.1:8185
}
install.shthe same install, no agent198 lines

authored from upstream docs, never pasted · 9,312 bytes

#!/usr/bin/env bash
# Outline · the agent-free install.
#
# Everything prompt.md tells an agent to do, as a script you can read first.
# Run it on the VPS, as a non-root user who is in the docker group:
#
#   DOMAIN_HOST=wiki.example.com \
#   SMTP_HOST=smtp.example.com SMTP_USERNAME=apikey SMTP_PASSWORD=... \
#   SMTP_FROM_EMAIL=wiki@example.com ./install.sh
#
# SMTP_PASSWORD is read from the environment and never written to your terminal.
# Start that command line with a space if your shell records history. If your
# relay wants implicit TLS, add SMTP_PORT=465 SMTP_SECURE=true.
#
# Authored by caniselfhostit from the upstream documentation and source:
#   https://docs.getoutline.com/s/hosting/doc/docker-7pfeLP5a8t
#   https://docs.getoutline.com/s/hosting/doc/smtp-cqCJyZGMIB
#   https://github.com/outline/outline/blob/v1.9.2/.env.sample
#   https://github.com/outline/outline/blob/v1.9.2/Dockerfile
#
# Three secrets are generated here, on this machine: SECRET_KEY, UTILS_SECRET
# and the PostgreSQL password. All three go into /srv/outline/.env with mode 600
# and none is ever printed.
#
# DOMAIN_HOST is also URL, the address inside every sign-in link this instance
# mails and every share link it makes. Choose it once. Changing it later breaks
# links other people are already holding.
#
# This script cannot open a browser, so it stops one step short: the workspace
# is claimed by a human, and the closing summary carries the command that proves
# the claim closed the door behind it.
#
# NOT YET VERIFIED: no harness run has been recorded against this script.
set -euo pipefail

APP_DIR="${APP_DIR:-/srv/outline}"
DOMAIN_HOST="${DOMAIN_HOST:-}"
SMTP_HOST="${SMTP_HOST:-}"
SMTP_PORT="${SMTP_PORT:-587}"
SMTP_SECURE="${SMTP_SECURE:-false}"
SMTP_USERNAME="${SMTP_USERNAME:-}"
SMTP_PASSWORD="${SMTP_PASSWORD:-}"
SMTP_FROM_EMAIL="${SMTP_FROM_EMAIL:-}"

die() { printf 'install.sh: %s\n' "$1" >&2; exit 1; }

# --- 1. Refuse to start on a machine that is not ready -----------------------

[ -n "$DOMAIN_HOST" ] || die "set DOMAIN_HOST to the hostname you pointed at this server, e.g. wiki.example.com"
[ -n "$SMTP_HOST" ] || die "set SMTP_HOST; sign-in links arrive by mail and Outline has no password login"
[ -n "$SMTP_FROM_EMAIL" ] || die "set SMTP_FROM_EMAIL; Outline exits at start-up on a from-address it cannot parse"
command -v docker >/dev/null 2>&1 || die "docker is not installed. Run Prompt Zero first."
docker compose version >/dev/null 2>&1 || die "the docker compose plugin is missing"
command -v caddy >/dev/null 2>&1 || die "caddy is not installed on the host. Run Prompt Zero first."
command -v openssl >/dev/null 2>&1 || die "openssl is not installed"

avail_mb="$(free -m | awk '/^Mem:/ {print $7}')"
[ "$avail_mb" -ge 2048 ] || die "only ${avail_mb} MB of RAM available; three services want 2048 MB"
avail_gb="$(df -BG --output=avail /srv | tail -1 | tr -dc '0-9')"
[ "$avail_gb" -ge 10 ] || die "only ${avail_gb} GB free on /srv; this install wants 10 GB"

resolved="$(getent hosts "$DOMAIN_HOST" | awk '{print $1; exit}')" || resolved=""
[ -n "$resolved" ] || die "$DOMAIN_HOST does not resolve yet. Add the A record, wait a minute, run this again."

# --- 2. Lay the files out ----------------------------------------------------
#
# postgres and redis stay root-owned at 700: both images chown their own data
# directory on first start. data goes to uid 1001, the non-root user the Outline
# image runs as, or every attachment upload fails while everything else works.

sudo install -d -m 750 -o "$(id -u)" -g "$(id -g)" "$APP_DIR" "$APP_DIR/backups"
sudo install -d -m 700 "$APP_DIR/postgres" "$APP_DIR/redis"
sudo install -d -m 750 -o 1001 -g 1001 "$APP_DIR/data"
install -m 0644 "$(dirname "$0")/compose.yml" "$APP_DIR/compose.yml"
install -m 0644 "$(dirname "$0")/Caddyfile" "$APP_DIR/Caddyfile"

# --- 3. Generate the three secrets, on this machine --------------------------
#
# Hex for all three: upstream requires SECRET_KEY to be exactly 64 hexadecimal
# characters, and the database password travels inside a connection string.
# Read them later with
#   sudo grep -E 'SECRET_KEY|UTILS_SECRET|DB_PASSWORD' /srv/outline/.env
#
# SECRET_KEY encrypts stored data. Upstream states that changing it later leaves
# users unable to log in, so .env belongs in every backup beside the dump.

if [ ! -f "$APP_DIR/.env" ]; then
	umask 077
	cat > "$APP_DIR/.env" <<-ENVFILE
		URL=https://${DOMAIN_HOST}
		SECRET_KEY=$(openssl rand -hex 32)
		UTILS_SECRET=$(openssl rand -hex 32)
		DB_PASSWORD=$(openssl rand -hex 32)
		SMTP_HOST=${SMTP_HOST}
		SMTP_PORT=${SMTP_PORT}
		SMTP_SECURE=${SMTP_SECURE}
		SMTP_USERNAME=${SMTP_USERNAME}
		SMTP_PASSWORD=${SMTP_PASSWORD}
		SMTP_FROM_EMAIL=${SMTP_FROM_EMAIL}
	ENVFILE
	chmod 600 "$APP_DIR/.env"
	umask 022
fi

cd "$APP_DIR"
docker compose config >/dev/null

# --- 4. Caddy site block, on the host ----------------------------------------

if ! sudo grep -qF "$DOMAIN_HOST {" /etc/caddy/Caddyfile; then
	sudo cp /etc/caddy/Caddyfile "/etc/caddy/Caddyfile.before-outline"
	printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
	sed "s|<DOMAIN>|${DOMAIN_HOST}|g" "$APP_DIR/Caddyfile" | sudo tee -a /etc/caddy/Caddyfile >/dev/null
fi
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

# --- 5. Ports: two open, and 8185, 5432 and 6379 are not among them ----------

if command -v ufw >/dev/null 2>&1; then
	echo "==> 80/tcp and 443/tcp for Caddy, 443/udp for HTTP/3; 8185, 5432 and 6379 stay closed"
	sudo ufw allow 80/tcp
	sudo ufw allow 443/tcp
	sudo ufw allow 443/udp
	sudo ufw status verbose
fi

# --- 6. Start it -------------------------------------------------------------
#
# Outline runs its own database migrations on the way up, so the first boot
# takes minutes rather than seconds.

docker compose pull
docker compose up -d

echo "==> waiting for https://${DOMAIN_HOST}/_health"
for _ in $(seq 1 40); do
	code="$(curl -sS -o /dev/null -w '%{http_code}' "https://${DOMAIN_HOST}/_health")" || code=""
	[ "$code" = "200" ] && break
	sleep 10
done
[ "${code:-}" = "200" ] || die "/_health answered ${code:-nothing}. Check: docker compose logs --tail 60 outline"

health="$(curl -sS "https://${DOMAIN_HOST}/_health")" || health=""
[ "$health" = "OK" ] || die "/_health returned '${health}', not OK. It queries PostgreSQL and Redis before answering."

# An unclaimed installation offers no sign-in providers at all, because the
# email provider only appears once a workspace exists. This is the assert that
# says the create-workspace form below is the real first screen.
cfg="$(curl -sS -H 'content-type: application/json' -d '{}' "https://${DOMAIN_HOST}/api/auth.config")" || cfg=""
printf '==> auth.config: %s\n' "$cfg"
case "$cfg" in
	*'"providers":[]'*) ;;
	*) die "auth.config did not report an empty provider list. A workspace may already exist on this hostname." ;;
esac

# Nothing in the wiki is readable without a session, before or after the claim.
unauth="$(curl -sS -o /dev/null -w '%{http_code}' -H 'content-type: application/json' -d '{}' "https://${DOMAIN_HOST}/api/documents.list")" || unauth=""
[ "$unauth" = "401" ] || die "an unauthenticated API call returned ${unauth}, not 401. Stop and investigate."

# --- 7. The first backup, before day one ends --------------------------------

STAMP="$(date +%Y%m%d-%H%M%S)"
docker compose exec -T postgres pg_dump -U outline -d outline | gzip > "$APP_DIR/backups/outline-db-${STAMP}.sql.gz"
sudo tar -czf "$APP_DIR/backups/outline-files-${STAMP}.tar.gz" -C "$APP_DIR" compose.yml .env data -C /etc/caddy Caddyfile
ls -lh "$APP_DIR/backups/"
[ -s "$APP_DIR/backups/outline-db-${STAMP}.sql.gz" ] || die "the database dump is empty"

cat <<-DONE

	Outline is answering at https://${DOMAIN_HOST}

	  1. Open that address now, not later. With no workspace yet, the page is a
	     Create workspace form, and whoever fills it in first becomes the admin
	     of this install. Enter a workspace name, your name and your email.
	  2. Then prove the door shut behind you, from this shell:
	       curl -sS -H 'content-type: application/json' \\
	         -d '{"teamName":"closed","userName":"closed","userEmail":"closed@example.com"}' \\
	         https://${DOMAIN_HOST}/api/installation.create
	     It must print a body containing: Installation already has existing teams
	     Anything else means the workspace was never claimed, and the form is
	     still open to whoever finds this hostname.
	  3. Test the way back in, because there is no password. Sign out, enter
	     your email on the sign-in page, and open the link that arrives. If it
	     never lands, read
	       docker compose logs --tail 60 outline
	     for the SMTP error, then add a passkey under Settings, Security as a
	     second way in that needs no mail.
	  4. Your three secrets are in $APP_DIR/.env, mode 600, and none was printed
	     here. SECRET_KEY encrypts stored data, so back .env up with the
	     database dump rather than separately.
	  5. First backup written to $APP_DIR/backups: a database dump and a file
	     archive carrying attachments, compose.yml, .env and the live Caddyfile.
	     Both are on the same disk as the data, which is not a backup. Copy them
	     off the box tonight:
	       scp vps:$APP_DIR/backups/* ~/backups/outline/

DONE

What you're signing up for

The part a vendor's comparison page leaves out. None of it is a reason not to do this; all of it is yours the moment you cancel Slite.

  • Source-available, not open source. Outline is under the Business Source License 1.1: you may run it, change it and redistribute it, but the additional use grant forbids using it for a Document Service, upstream's term for a commercial offering that lets other people create their own teams and documents on your instance. Each release carries its own change date, and 1.9.2 becomes Apache 2.0 on 2030-07-21.
  • There is no password login, and never was. The first workspace is claimed on a form the server offers only while none exists, so the minutes between the container answering and you opening the page are a race anyone who knows the hostname can enter. After that, getting back in means a link mailed to you or a passkey, which makes an SMTP relay part of the install rather than a nicety, and a relay that quietly drops mail is a locked front door.
  • You own a PostgreSQL and a Redis. Documents, revisions and comments are rows, attachments are files under one directory, and SECRET_KEY from .env is what decrypts the encrypted columns in the dump, so those three travel together or the restore is a wiki nobody can sign in to. Redis is not backed up at all, because it holds live editor sessions and a job queue rather than content.
  • No agent, and that is most of what Slite now sells. Ask, doc fact-checking with suggested fixes, answers across your connected tools and the agent workflows on the paid tiers have no counterpart here. What you get is search across your own documents, a REST API, and whatever you wire to it yourself with a key you hold.
  • It phones home until you stop it. Left at its default the server posts a hashed install id and its user, team, collection and document counts to updates.getoutline.com once a day. This install sets ENABLE_UPDATES to false in compose.yml and says where to turn it back on, which is a decision rather than a default.

Where this came from

“To support sending outgoing transactional emails such as "document updated" or "you've been invited" you'll need to provide authentication for an SMTP server. This is also required to support email “magic link” sign-in.”

  • Outline 1.9.2 is licensed under the Business Source License 1.1 with a change date of 2030-07-21 and Apache License 2.0 as the change license, and its additional use grant forbids using the work for a Document Service. source
  • A self-hosted install creates its first workspace and admin through POST /api/installation.create, a route mounted only when the instance is not cloud hosted and refused once any team exists, which is why claiming the workspace closes the door behind it. source
  • Email magic-link sign-in registers only when SMTP_HOST or SMTP_SERVICE is set, and it signs in a user who already exists rather than creating one, so mail is how everybody returns after the first session rather than how the first account is made. source
  • FILE_STORAGE defaults to s3 in the source and SECRET_KEY is validated as exactly 64 hexadecimal characters, so a local-attachment install has to set the first and generate the second with openssl rand -hex 32. source
  • In production Outline opens its PostgreSQL connection with SSL unless PGSSLMODE is set to disable, and logs a fatal error when the database does not support SSL. source
  • With the update check left on, the server posts a hashed installation id together with its user, team, collection and document counts to updates.getoutline.com every 24 hours. source

Questions people actually ask

Answered from this page's own data — the same numbers, in sentences.

  • Can I self-host Slite?

    Not Slite itself — the vendor does not ship a version you can run on your own server. What you can self-host is the job people pay it for, and the answer to that is Outline. Nested documents, an editor two people can type in at once and search across all of it, with every word in a Postgres you can dump. The install is one weekend: 3 containers behind Caddy with automatic TLS, secrets generated on the server rather than in a chat window, and a first backup taken before the agent says it is done, in about 210 minutes. The prompt on this page does it; the compose.yml, Caddyfile and install.sh below do the same install with no agent at all.

  • What replaces Slite?

    Outline. Nested documents, an editor two people can type in at once and search across all of it, with every word in a Postgres you can dump. The closest thing to what a Slite workspace actually is: nested documents in collections, a fast editor two people can type in at once, comments, search across everything, and a REST API. Sign-in is the part to plan for, because there is no password anywhere in the product. The first workspace is claimed on a form the server shows only while none exists, which you want to do the minute the install answers, and after that everyone returns through a link mailed to them, so an SMTP relay is part of this install rather than an optional extra. What does not come across is the agent half: Ask, the cross-tool answers, the fact-checking and the agent workflows on Slite's paid tiers have no counterpart here. It is also Business Source License 1.1 rather than open source, converting to Apache 2.0 on 2030-07-21, and the grant forbids reselling it as a document service. Also evaluated and ranked below it: Docmost. The prompt on this page installs Outline only. Outline is BSL-1.1-licensed and free; nothing on this page is a hosted service we sell you.

  • What does self-hosting cost compared to Slite?

    2048 MB of RAM and 10 GB of disk — the smallest tier most VPS hosts sell, about $10 a month. Outline itself is free and BSL-1.1-licensed; the bill is the server, plus a domain you probably already own. What you stop paying: Slite Pro, $100/mo — $1,200 a year, 5 seats assumed.

  • How hard is it really?

    ONE WEEKEND — 3–24 hours. The rule that produced that verdict: a database plus one outside integration. A database and an outside service fail in completely different ways, and you have to learn both failure modes before you trust the thing with real data. Budget the second day for whichever one surprises you. The tier is derived from seven countable facts about the Outline install, not from anyone's impression of it, and the whole rubric is published on the methodology page.

  • Can I run Outline on my own computer instead of a server?

    Yes — that is the second path in the prompt box above. "On my computer" installs the same Outline on the machine you are sitting at: no VPS, no domain, no DNS, and nothing exposed to the internet. It checks for Docker first and installs Docker Desktop if the machine does not have it — macOS, Windows and Linux each get their own step — then binds everything to loopback, so the app answers on http://localhost and only on that computer. The catch: Only this computer can open the wiki, and every sign-in link it mails and every share link it makes starts with http://localhost:8185, so anyone you invite receives an address that opens nothing on their machine. Same discipline as the cloud path: pinned images, secrets generated on the machine, and a first backup taken before the prompt says it is done.

Content last checked 2026-08-14. Verdicts are derived from the published rubric on /methodology; corrections go through the issue tracker.