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.ymlbelow 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
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_PASSWORD=<your SMTP password>
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).
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/healthanswers when the process runs./api/readyanswers 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 exampledocker 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
- Admin → General: the instance name, uploads limits, notification emails. See Instance settings.
- Admin → Authentication: password sign-in, self sign-up, single sign-on. See Sign-in.
- Send yourself a password reset to check that email works.
- 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:
- Set
ROLE=webon the container(s) that serve users: they stop running background jobs. - Add one more service with the same image and the same
.env, withROLE=workerand no published port. It runs the jobs.