Install Folio on Portainer
You manage Docker hosts with Portainer. A stack is a Compose file you paste into it.
What you need
- Portainer (Community or Business) connected to a Docker host with 2 GB of RAM or more.
- Optionally a reverse proxy for HTTPS (Traefik, Nginx Proxy Manager, Caddy).
Steps
Step 1: Stacks > Add stack. Name it folio. Choose Web editor.
Step 2: Paste the stack.
Show stack.yml (82 lines) and copy it
stack.yml# Folio as a Portainer stack: Stacks > Add stack > Web editor, paste this, and # set the environment variables listed in the steps. No port other than # FOLIO_PORT is published; put your reverse proxy in front of it for HTTPS. services: app: image: ${FOLIO_IMAGE:-ghcr.io/evergreen-it-dev/folio:latest} restart: unless-stopped ports: - '${FOLIO_BIND:-0.0.0.0}:${FOLIO_PORT:-4870}:4870' environment: NODE_ENV: production PORT: '4870' DATABASE_URL: postgresql://folio:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@postgres:5432/folio REDIS_URL: redis://redis:6379 PUBLIC_URL: ${PUBLIC_URL:?set PUBLIC_URL to the address people will open} ASSET_BACKEND: local # Left empty, the app generates a secret on first start and keeps it on # the data volume (see the command below). 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" fi FOLIO_SECRET="$$(cat "$$secret_file")" export FOLIO_SECRET fi exec npx tsx server/index.ts volumes: - 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 postgres: image: postgres:17-alpine restart: unless-stopped environment: POSTGRES_USER: folio POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD} 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 volumes: folio-data: folio-pg:Step 3: Under Environment variables add PUBLIC_URL and POSTGRES_PASSWORD from the table below. FOLIO_SECRET is optional but recommended: openssl rand -hex 32.
Step 4: Deploy the stack. When app shows healthy, open the address. The first account is the administrator.
Step 5: For HTTPS, point your reverse proxy at the host's port 4870 (or attach the app service to the proxy's network), and make it pass WebSocket connections (the Upgrade and Connection headers). Real-time editing uses them. PUBLIC_URL must be the https:// address.
Settings
Add these as environment variables of the stack.
| Variable | Default | What it does |
|---|---|---|
PUBLIC_URL | required | The address people will open, for example https://wiki.example.com (or http://<server-ip>:4870 to try it). |
POSTGRES_PASSWORD | required | A long random string (openssl rand -hex 24). |
FOLIO_SECRET | optional | openssl rand -hex 32. Empty means the app generates one and keeps it on the data volume. Never change it afterwards. |
FOLIO_PORT | 4870 | Optional. |
FOLIO_BIND | 0.0.0.0 | Optional. Use 127.0.0.1 behind a proxy on the same host. |
FOLIO_IMAGE | ghcr.io/evergreen-it-dev/folio:latest | Pin a release, for example :v0.1.0. |
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
Open the stack > Editor > Update the stack, ticking Re-pull image and redeploy. The database migrates itself on start. Back up first.
Alternatively choose Repository when you create the stack: repository https://github.com/evergreen-it-dev/folio, Compose path deploy/portainer/stack.yml, the same variables. Portainer can then redeploy when the file changes.
Back up
From the host (or Portainer's console for each container). The container names are shown on the stack page.
docker exec <folio-postgres-container> pg_dump -U folio folio > folio-db.sql
docker exec <folio-app-container> tar czf - -C /app/data . > folio-data.tgzLimits and honest notes
- One instance of the app. The disk is local to the container.
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.