Quickstart
barakoCMS quickstart
Run the barakoCMS API, the full suite (engine + every module), with Postgres from prebuilt
images. No build step, no .NET toolchain. You edit one .env and start it.
This is the API on its own, and its surface is Swagger. For the API with the console in front of
it, start from barakoBrew: it runs this same image and
puts ghcr.io/baryodev/barako-brew in front of it.
What you get
- CMS API (
ghcr.io/baryodev/barako-cms), core plus every module: Accounting, Analytics (Umami), Email (Resend), Feature flags, Diagnostics, Import, Files, Device trust, External auth, Portability. - Postgres 16 with a persistent volume.
- A nightly backup container running the same script the production stack uses. The folder
carries its own copy in
scripts/backup-cron.sh, so copy the whole folder, not just the compose file.
Every module is already in the image. Each one stays off or on a safe mock until you provide its
keys, so an empty-but-valid .env boots a working CMS you can grow into.
Run it
# from this quickstart/ folder
cp .env.example .env
# edit .env, at minimum: DB_PASSWORD, JWT_KEY, ADMIN_PASSWORD
docker compose up -d
Then:
| URL | |
|---|---|
| CMS API | http://localhost:5005 |
| API health | http://localhost:5005/health |
| Swagger | http://localhost:5005/swagger, once SWAGGER_ENABLED=true |
Sign in as ADMIN_USERNAME / ADMIN_PASSWORD from your .env, from Swagger or with curl:
curl -s -X POST http://localhost:5005/api/auth/login -H 'Content-Type: application/json' \
-d '{"username":"admin","password":"YOUR_ADMIN_PASSWORD"}'
Configuration
Full reference is in .env.example. The essentials:
Required
| Variable | Notes |
|---|---|
DB_PASSWORD |
Postgres password. |
JWT_KEY |
Token signing key. Must be 32+ characters or the API won't start. |
ADMIN_USERNAME / ADMIN_PASSWORD |
The first admin account, seeded on first boot. |
Swagger
Swagger is on automatically in Development and off in Production, which is what this compose
defaults ASPNETCORE_ENVIRONMENT to. The compose passes SWAGGER_ENABLED through as
Swagger__Enabled, so:
SWAGGER_ENABLED=true
turns it on at /swagger without changing the environment.
Turning modules on
Each module block in .env is optional. Fill it in and docker compose up -d again to apply.
Every module in the image is registered unless BarakoCMS__Modules__Enabled says otherwise. Unset,
all of them run and the API logs one warning saying so at boot. Set it to a comma-separated list of
module names (BarakoCMS__Modules__Enabled=Accounting,Files) to run only those, or to an empty
string for core only. A name that matches nothing stops the boot and lists the names available.
Turning a module off leaves its data in the database; GET /api/modules shows each module with
enabled. See MODULES.md in the repository. docker-compose.yml passes a fixed set of variables
to the API and this is not one of them, so it goes under the api service's environment: block
(there is a commented line ready), not in .env.
- Email (Resend): set
RESEND_API_KEY+RESEND_FROMto actually send mail (otherwise emails are logged by a mock).RESEND_WEBHOOK_SECRETenables bounce/complaint tracking. - Analytics (Umami): point
UMAMI_BASEURLat your Umami instance, setUMAMI_ENABLED=trueand a read account (UMAMI_USERNAME/UMAMI_PASSWORD). The analytics endpoints then answer with visitors, top pages, referrers, devices and more, and can register new sites. - Social sign-in (ExternalAuth): set
EXTERNALAUTH_ENABLED=trueand the client id/secret for each provider you want (Google, GitHub, Facebook, LinkedIn). - Device trust:
DEVICETRUST_ENFORCE=truerequires new devices to be approved via an emailed code.
Behind a domain (production)
If you have no proxy already and just want a domain with TLS, use the repository's
docker-compose.prod.yml instead of this one. It runs the same published image with Caddy in
front, refuses to start without real secrets, and is the file the project supports for production.
See docs/deploy-in-production.md.
To keep this compose and put your own proxy in front, the compose exposes the API on localhost. In front of it put a reverse proxy (nginx, Caddy, Traefik) terminating TLS, then set:
ASPNETCORE_ENVIRONMENT=Production
APP_BASE_URL=https://cms.example.com # what the API puts in links it hands out
ALLOWED_HOSTS=cms.example.com # the Host headers the API answers to
ALLOWED_ORIGINS=https://admin.example.com # where the console and your site are served
BARAKO_TAG=3.21.0 # pin a release rather than :latest
APP_BASE_URL and ALLOWED_HOSTS are the pair that keeps a caller from choosing the origin of the
links this API produces. The Host header is written by whoever sent the request, so an RSS feed or
an OAuth redirect_uri built from it points wherever the caller likes. Set APP_BASE_URL and the
links come from configuration instead. Set ALLOWED_HOSTS and anything with another Host gets a
400 before it reaches any code, which makes the header trustworthy again and is the better fix if
you can enumerate your hostnames.
With neither set, the RSS feed answers 503 and the OAuth start endpoints fail, naming the setting. That is deliberate: guessing produces a working-looking link to somebody else's domain.
One trap with ALLOWED_HOSTS: a Kubernetes httpGet probe sends the pod IP as the Host header
unless you add one, so a list of real hostnames makes the probes 400 and the pod never goes ready.
Either add a Host header to the probe or leave ALLOWED_HOSTS at * and rely on APP_BASE_URL.
TLS is terminated by the proxy, so add HSTS there:
add_header Strict-Transport-Security "max-age=7776000" always;
The API sends the same header itself outside Development, but only on requests it can see are
HTTPS, which behind a proxy means only once FORWARDED_HEADERS_ENABLED is on. 90 days and no
includeSubDomains are the defaults, tunable with Hsts__MaxAgeDays and Hsts__IncludeSubDomains.
Turn includeSubDomains on only once every subdomain is on HTTPS: a browser that has seen it keeps
it for the whole max-age whatever you deploy afterwards.
The app ignores X-Forwarded-For until you say which hop to believe, because the header is written
by the caller and honouring it from anywhere lets anyone choose the IP that rate limiting and the
audit log see. Name the proxy and both start working per client:
FORWARDED_HEADERS_ENABLED=true
TRUSTED_PROXY_ADDRESS=10.87.51.2 # the proxy's own address, as the API sees it
Name the proxy's address, not the range it sits in. Anything else inside a trusted range can reach
the API and set X-Forwarded-For itself, and on a Docker host the whole 172.16.0.0/12 bridge
range includes every other container. If the proxy is a container, give it a fixed ipv4_address on
a network with a fixed subnet, the way docker-compose.prod.yml does, so the address survives a
restart. TRUSTED_PROXY_NETWORK takes CIDR notation for the case where the address really cannot be
pinned, and both can be set together.
Turning it on without either is a startup failure rather than a silent "trust everyone".
Postgres settings
Postgres runs with settings sized for a 2 GB machine (shared_buffers 512MB, random_page_cost
1.1, jit off) and loads pg_stat_statements, which the one-shot postgres-extensions service
creates on every start. Each value is a PG_* variable in .env. The values for 4 GB and 8 GB,
and the stock ones to go back to, are in the barakoCMS repository's
docs/deploy-in-production.md, "Postgres on a small server".
Upgrading
Change BARAKO_TAG first, then:
docker compose pull
docker compose run --rm --no-deps api db-assert
docker compose up -d
Migrations do not run on start. The API creates missing tables but never alters an existing one
(outside Development it runs Marten's CreateOnly), so a release that changes a table you already
have fails to start until its SQL from migrations/<version>/ in the barakoCMS repository is
applied. db-assert exits 0 when the database holds everything the new build needs, and non-zero
listing what is outstanding. The barakoCMS repository's docs/deploy-in-production.md,
"Upgrading", has the full procedure. Pin BARAKO_TAG to a specific version so an upgrade is
deliberate.
Data & backup
Postgres data lives in the pgdata volume, and db-backup dumps it nightly into the backups
volume with the same script production runs. It also takes one backup at startup, once the API has
created its tables, so docker compose logs db-backup shows Backup OK within a few minutes of the
first up. If it does not, the stack has no recovery point. For a one-off dump:
docker compose exec postgres pg_dump -U postgres barakocms > backup.sql
Lean core instead of the full suite
Want only some modules? Swap the API image for the core-only build
(ghcr.io/baryodev/barako-cms-decaf) and register just the modules you need in your own host. The
suite here is the batteries-included default.
If barakoCMS is useful to you, a star on the repository helps other people find it.