Install Folio on a Linux server
You have a Linux server, or are ready to rent one, and want the simplest setup for a team. This is also what the DigitalOcean and cloud-init installs use.
What you need
- A Linux server. Ubuntu 22.04 or 24.04 and Debian 12 are what the script assumes. At least 2 GB of RAM, 1 vCPU and 10 GB of disk; 4 GB and 2 vCPU for a team of about twenty.
- Docker Engine with the Compose plugin (docker compose version works), and curl.
- For HTTPS: a domain whose DNS A record points at the server, and ports 80 and 443 open.
Steps
Step 1: Install Docker Engine with the Compose plugin if the server does not have it yet.
bashcurl -fsSL https://get.docker.com | shStep 2: Run the installer. It asks for a domain. An empty answer means plain http on the server's IP, for a trial.
bashcurl -fsSL https://raw.githubusercontent.com/evergreen-it-dev/folio/main/deploy/vps/install.sh | bashStep 3: To skip the question, pass the domain as a variable.
bashFOLIO_DOMAIN=wiki.example.com FOLIO_YES=1 bash -c "$(curl -fsSL https://raw.githubusercontent.com/evergreen-it-dev/folio/main/deploy/vps/install.sh)"Step 4: The script checks Docker, downloads docker-compose.yml into /opt/folio (or ~/folio when you are not root), generates the database password and FOLIO_SECRET, writes them to a .env readable only by its owner, starts the stack from the published image and waits until the app is healthy. Running it again keeps your .env and secrets.
Step 5: Open the address the script prints. The first account you create is the administrator.
Files
The installer downloads these three files into one folder. To install by hand, save them next to each other, copy .env.example to .env, set the values and run docker compose up -d. For HTTPS add --profile https.
Show docker-compose.yml (114 lines) and copy it
# Folio on one server, from the published image: the app, PostgreSQL, Redis,
# and (optional) Caddy for automatic HTTPS. Written for install.sh, which also
# creates the .env file next to this one; works by hand too.
#
# docker compose up -d # plain http on FOLIO_PORT
# docker compose --profile https up -d # automatic HTTPS for FOLIO_DOMAIN
#
# Nothing in here needs editing. Settings live in .env (see .env.example).
name: folio
services:
app:
image: ${FOLIO_IMAGE:-ghcr.io/evergreen-it-dev/folio:latest}
restart: unless-stopped
ports:
# 127.0.0.1 when HTTPS is on (Caddy reaches the app over the compose
# network); 0.0.0.0 for plain http access by address.
- '${FOLIO_BIND:-0.0.0.0}:${FOLIO_PORT:-4870}:4870'
environment:
NODE_ENV: production
PORT: '4870'
DATABASE_URL: postgresql://folio:${POSTGRES_PASSWORD:?run install.sh or set POSTGRES_PASSWORD in .env}@postgres:5432/folio
REDIS_URL: redis://redis:6379
PUBLIC_URL: ${PUBLIC_URL:-http://localhost:4870}
ASSET_BACKEND: local
# Generated by install.sh. If you start without it, the app creates one
# on first start and keeps it on the data volume.
FOLIO_SECRET: ${FOLIO_SECRET:-}
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
GOOGLE_ALLOWED_DOMAINS: ${GOOGLE_ALLOWED_DOMAINS:-}
CURSOR_API_KEY: ${CURSOR_API_KEY:-}
CURSOR_AGENT_MODEL: ${CURSOR_AGENT_MODEL:-auto}
CURSOR_AGENT_WORKSPACE_ROOT: /app/data/assistant/workspaces
CURSOR_AGENT_STATE_DIR: /app/data/assistant/state
entrypoint: ['/sbin/tini', '--', '/bin/sh', '-c']
command:
- |
set -e
if [ -z "$$FOLIO_SECRET" ]; then
secret_file=/app/data/.folio-secret
if [ ! -s "$$secret_file" ]; then
umask 077
node -e "process.stdout.write(require('node:crypto').randomBytes(32).toString('hex'))" > "$$secret_file"
echo "folio: generated a new secret in $$secret_file"
fi
FOLIO_SECRET="$$(cat "$$secret_file")"
export FOLIO_SECRET
fi
exec npx tsx server/index.ts
volumes:
# The content itself: git repositories of the spaces, uploaded files.
- folio-data:/app/data
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
healthcheck:
test:
- CMD
- node
- -e
- "fetch('http://127.0.0.1:4870/api/health').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
interval: 30s
timeout: 5s
retries: 3
start_period: 60s
# Not published outside the compose network.
postgres:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_USER: folio
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?run install.sh or set POSTGRES_PASSWORD in .env}
POSTGRES_DB: folio
volumes:
- folio-pg:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U folio -d folio']
interval: 5s
timeout: 5s
retries: 20
# Transient data only (presence, rate limits): persistence is off on purpose.
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --save '' --appendonly no
# Automatic HTTPS. Started only with `--profile https`; needs ports 80 and
# 443 and a DNS record for FOLIO_DOMAIN pointing at this server.
caddy:
image: caddy:2-alpine
profiles: ['https']
restart: unless-stopped
ports:
- '80:80'
- '443:443'
environment:
FOLIO_DOMAIN: ${FOLIO_DOMAIN:-localhost}
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
depends_on:
- app
volumes:
folio-data:
folio-pg:
caddy-data:
caddy-config:# Used by `docker compose --profile https up -d`.
# Caddy obtains and renews the certificate for FOLIO_DOMAIN by itself and
# passes WebSocket connections (real-time editing) through unchanged.
{$FOLIO_DOMAIN} {
encode zstd gzip
reverse_proxy app:4870
}# Settings for deploy/vps/docker-compose.yml. install.sh writes this file for
# you with generated secrets; copy it by hand only if you skip install.sh.
# The address people type to open Folio. Invitation and share links are built
# from it. Must match the address in the browser, including http or https.
PUBLIC_URL=http://localhost:4870
# Required. Applied when the database is first created; changing it later here
# does not change it inside the database.
POSTGRES_PASSWORD=change-me
# Encrypts saved git tokens and personal AI keys. `openssl rand -hex 32`.
# Never change it afterwards. Empty = generated on first start and kept on the
# data volume.
FOLIO_SECRET=
FOLIO_PORT=4870
# 0.0.0.0 = reachable from other machines; 127.0.0.1 = this machine only.
FOLIO_BIND=0.0.0.0
# Automatic HTTPS: set the domain, FOLIO_BIND=127.0.0.1, an https PUBLIC_URL,
# and start with `docker compose --profile https up -d`.
# FOLIO_DOMAIN=wiki.example.com
# Which image to run. Pin a release (ghcr.io/evergreen-it-dev/folio:v0.1.0)
# to update only when you choose to.
# FOLIO_IMAGE=ghcr.io/evergreen-it-dev/folio:latest
# GOOGLE_CLIENT_ID=
# GOOGLE_CLIENT_SECRET=
# GOOGLE_ALLOWED_DOMAINS=example.com
# CURSOR_API_KEY=Settings
Everything is in .env next to the compose file. After editing, apply it with docker compose up -d. Options of the script itself are environment variables: FOLIO_DIR, FOLIO_PORT, FOLIO_IMAGE, FOLIO_RAW_BASE.
| Variable | Default | What it does |
|---|---|---|
PUBLIC_URL | set by the script | The address people open. Must match the browser, including https. |
POSTGRES_PASSWORD | generated | Applied when the database is first created. |
FOLIO_SECRET | generated | Encrypts saved Git tokens and personal AI keys. Never change it. |
FOLIO_DOMAIN | empty | Domain for the bundled Caddy (automatic HTTPS). |
FOLIO_IMAGE | ghcr.io/evergreen-it-dev/folio:latest | Pin a release (for example :v0.1.0) to update on your terms. |
FOLIO_PORT, FOLIO_BIND | 4870, 0.0.0.0 | The port, and which addresses may reach it. |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET | empty | Turn on "Sign in with Google". Register <your address>/api/auth/google/callback with Google. |
GOOGLE_ALLOWED_DOMAINS | empty | Email domains allowed to sign in with Google. Empty means nobody. |
CURSOR_API_KEY | empty | Shared key for the AI assistant. Without it each person adds their own. |
Update
The script takes a backup, pulls the newest image and restarts. The database migrates itself on start.
cd /opt/folio && ./install.sh updateBy hand: docker compose pull && docker compose up -d.
Back up
This writes backups/folio-db-<date>.sql and backups/folio-data-<date>.tgz: the database and the data volume (the Git repositories and uploads). Both are needed. Copy them off the server.
cd /opt/folio && ./install.sh backupLimits and honest notes
- Without a domain the address is plain http://<ip>:4870. Folio then does not mark its cookies HTTPS-only, so signing in works, but do not expose such an instance to the internet.
- Your own reverse proxy works too: set FOLIO_BIND=127.0.0.1, leave the domain empty, and forward to 127.0.0.1:4870, passing the Upgrade and Connection headers.
- docker compose down stops Folio and keeps the data. Adding --volumes deletes every page and account.
Platform documentation
Check the installation
From a clone of the repository, run the smoke test against a fresh instance. It needs Node 18 or newer. It creates the first account, a space and a page, and opens a WebSocket connection to the page. Delete the instance, or that space, afterwards.
node deploy/smoke.mjs https://wiki.example.comOther ways to install
Compare all platformsNot ready to install?
Try the public demo: https://demo.foliowiki.online. Pick Sam on the sign-in screen. The login is shared and the data resets every 24 hours, so don't enter personal data or API keys.