Skip to main content

Install sol2flow with Docker Compose

A self-hosted sol2flow is one container (the web app, the API and the background jobs) plus a PostgreSQL database (16, 17 or 18; 18 recommended). Uploaded files go to a volume or to S3-compatible storage. Emails go out over SMTP.

Requirements​

  • A Linux server (x86-64 or ARM64) with Docker and the Docker Compose plugin.
  • 1 CPU and 1–2 GB of memory for a small team; more for many users or large imports.
  • A domain name pointing at the server, and a reverse proxy for HTTPS (Reverse proxy and TLS).
  • An SMTP account to send email (Email).
  • Disk space for the database and for uploads, plus room for backups.
  • PostgreSQL 16, 17 or 18 (18 recommended). The compose.yml below brings 18; already on 16 or 17? See Upgrade PostgreSQL.

The image is ghcr.io/sol2flow/sol2flow. Tags: latest, a release such as 1.2.3, or a minor line such as 1.2. Pin a version in production so upgrades happen when you choose.

1. Prepare a directory​

sudo mkdir -p /srv/sol2flow && cd /srv/sol2flow

Create compose.yml:

name: sol2flow
services:
app:
image: ghcr.io/sol2flow/sol2flow:${SOL2FLOW_VERSION:-latest}
restart: unless-stopped
env_file: .env
environment:
DATABASE_URL: postgresql://sol2flow:${POSTGRES_PASSWORD:-sol2flow}@db:5432/sol2flow?schema=public
ports: ["127.0.0.1:3000:3000"] # only the reverse proxy on this host talks to the app
volumes: [uploads:/data/uploads, logs:/data/logs] # logs: only used with LOG_FILE
depends_on: { db: { condition: service_healthy } }
db:
image: postgres:18-alpine
restart: unless-stopped
environment: { POSTGRES_USER: sol2flow, POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:-sol2flow}", POSTGRES_DB: sol2flow }
volumes: [pgdata:/var/lib/postgresql] # from PostgreSQL 18 not …/data (data: …/18/docker)
healthcheck: { test: ["CMD-SHELL", "pg_isready -U sol2flow"], interval: 5s, retries: 20 }
volumes: { pgdata: {}, uploads: {}, logs: {} }

The app runs as the user and group 1001, not as root, and owns /data/uploads and /data/logs in the image. Named volumes like the ones above take that over on their own. To keep the files in a directory of the host instead (a bind mount such as /srv/sol2flow/uploads:/data/uploads), give it to that user first:

sudo mkdir -p /srv/sol2flow/uploads /srv/sol2flow/logs
sudo chown -R 1001:1001 /srv/sol2flow/uploads /srv/sol2flow/logs

2. Configure​

Create .env next to it. The minimum for production:

SOL2FLOW_VERSION=latest
APP_URL=https://flow.example.com
AUTH_SECRET=<openssl rand -base64 32>
APP_ENCRYPTION_KEY=<openssl rand -base64 32>
POSTGRES_PASSWORD=<openssl rand -hex 16>

MAIL_DRIVER=smtp
MAIL_FROM="sol2flow <[email protected]>"
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_PASSWORD=<your SMTP password>

BOOTSTRAP_ADMIN_EMAIL=[email protected]
BOOTSTRAP_ADMIN_USERNAME=admin
BOOTSTRAP_ADMIN_PASSWORD=<a long password>

Replace every <…> with a real value. Keep .env readable only by root (chmod 600 .env).

Keep APP_ENCRYPTION_KEY safe

APP_ENCRYPTION_KEY encrypts secrets stored in the database (identity-provider secrets, two-factor secrets, webhook secrets). Store a copy outside the server and outside your database backups. Losing it, or changing it, makes those secrets unreadable — see Backups and upgrades.

Every variable is described in Configuration.

3. Start​

docker compose pull
docker compose up -d
docker compose logs -f app

On every start the app applies pending database migrations, then starts the server on port 3000. Two endpoints tell you it's up:

  • /api/health answers when the process runs.
  • /api/ready answers when the database is reachable too.
curl -fsS http://127.0.0.1:3000/api/ready

4. Put HTTPS in front​

Point your reverse proxy at 127.0.0.1:3000 and serve it on your domain: Reverse proxy and TLS. APP_URL must be exactly the address people type (scheme, host, and port if not the default).

5. The first administrator​

With BOOTSTRAP_ADMIN_PASSWORD set, the first start creates the account from BOOTSTRAP_ADMIN_* as the instance's Owner — the top staff role. The username is admin unless you set another; the email address is optional. It only happens while the instance has no users, so you can remove BOOTSTRAP_ADMIN_PASSWORD from .env afterwards. Its email address counts as verified, as does the address of every account the create-admin script below creates.

A new instance starts empty: this account is its only one, and there is no demo data. The built-in board templates are there from the start.

Without it, create the Owner from the command line:

docker compose exec app node scripts/create-admin.mjs <username> <email> <password>

The script also works later:

  • While the instance has no Owner, it creates (or promotes) an Owner; otherwise an Admin.
  • Pick another staff role with --role owner|admin|support|auditor, for example docker compose exec app node scripts/create-admin.mjs lea [email protected] '…' --role support.
  • An existing account keeps its password and gets the role. The script never demotes an existing Owner — change roles in Admin → Users.

What each role may do: Staff roles.

6. After the first sign-in​

  1. Admin → General: the instance name, uploads limits, notification emails. See Instance settings.
  2. Admin → Authentication: password sign-in, self sign-up, single sign-on. See Sign-in.
  3. Send yourself a password reset to check that email works.
  4. Set up backups before real data arrives.

Separate worker (optional)​

Background work (emails, cleanups, exports, imports, webhooks) runs inside the app container. On a busy instance you can move it to a container of its own:

  1. Set ROLE=web on the container(s) that serve users: they stop running background jobs.
  2. Add one more service with the same image and the same .env, with ROLE=worker and no published port. It runs the jobs.