Documentation

Install

One command, no database to set up, no setup wizard.

What you need

  • Node 22 or newer. That is the entire list.
  • A few minutes. There is no account to create and nothing to license.

Start a project

>_
pnpm dlx @forinda/fcms-cli init my-site
cd my-site && pnpm install
pnpm start
npx @forinda/fcms-cli init my-site
cd my-site && npm install
npm run start
yarn dlx @forinda/fcms-cli init my-site
cd my-site && yarn
yarn start
bunx @forinda/fcms-cli init my-site
cd my-site && bun install
bun run start

That writes a site you can already run — a spec that validates, a package.json, and a .gitignore that keeps the database out of version control. The next steps it prints come back in whichever manager you used, too.

Postgres runs inside the process — the real thing, compiled to WebAssembly — and keeps its data in .fcms. Nothing to install, nothing listening but the site itself, and a backup is cp -r .fcms.

Prefer to run Postgres as a service, or already have one? Docker Compose and a connection string both work, and the schema is the same either way.

With Docker

Docker brings the database and a version to pin along with it, which is what you want once one process is not enough. It needs Docker Compose and about 1–2 GB of memory.

terminal
mkdir my-site && cd my-site
curl -O https://forinda-cms.netlify.app/install/compose.yaml
curl -o .env https://forinda-cms.netlify.app/install/env.example

Open .env and set three things:

POSTGRES_PASSWORDAnything long and random. There is no default on purpose.
OWNER_EMAILYour sign-in.
OWNER_PASSWORDAt least 12 characters.
terminal
docker compose up -d

Open http://localhost:8080. There is a working site, and your dashboard is at /admin. Sign in and it asks what kind of site this is — bookings, enquiries, or nothing yet — and gives you a real one to start from.

There is no migration step and no setup command. The app migrates its own database and creates the first owner on boot, so up reaches a working site rather than a page telling you to run something else. Booting again changes nothing — migrations keep their ledger in the database, and the owner is created once.

Against a Postgres you already have

The same server, pointed at a database somebody else runs — a VPS, a managed one, the one your host already gave you. Same schema and same migrations as the embedded one, so moving between them is pg_dump and one variable:

terminal
DATABASE_URL=postgres://user:pass@localhost:5432/forinda \
OWNER_EMAIL=you@example.com OWNER_PASSWORD=a-long-enough-password \
npx @forinda/fcms-core

A postgres:// URL is a server; anything else is a directory to keep files in. npx @forinda/fcms-core —help lists everything it reads.

This is also the way past the embedded database’s ceiling: it serves one query at a time, which is invisible for a business taking bookings and a wall under real traffic.

What this path does not bring. No database, no TLS, no backup.sh — those came from compose. Put a reverse proxy in front of it for HTTPS, set SECURE_COOKIES=true and TRUST_PROXY=true behind one, point MEDIA_DIR at a directory that survives a redeploy, and take your own pg_dump backups.

Putting it on the internet

Compose publishes the app on HTTP_PORT and deliberately does not publish Postgres. Put a TLS terminator in front — Caddy, nginx, a load balancer — and then set two more variables:

SECURE_COOKIES=true Marks the session cookie Secure. Turn this on only once HTTPS works, or the cookie never arrives and nobody can sign in.
PUBLIC_URL Your address, e.g. https://example.com. Used for canonical URLs and the sitemap.
TRUST_PROXY=true Only with a proxy you control. It makes the app believe forwarded headers, which is wrong for anything else.

Upgrading

terminal
docker compose pull && docker compose up -d

Migrations are idempotent and resumable, so an interrupted upgrade is re-runnable rather than a state to reason about. Pin a version in .env if you would rather upgrade deliberately.