Running Your Stack
On this page
Once your stack is up, day-to-day work is a handful of commands plus a few addresses worth bookmarking. This page covers both. Every command here is run from inside your variant folder, the one holding docker-compose.yml and .env.
This page is written for the local-semantius-idp variant, and applies to local-entra-idp as well. Where a task happens in your identity provider rather than in semantius-idp, the section says where. On Dokploy there are no scripts to run, so use the Dokploy page instead. A variant is one ready-made setup of the stack; Choose or Build Your Variant lists them all and which pages apply to each.
Every command exists twice: a .sh version for macOS, Linux, and Git Bash, and a .cmd version for the Windows command prompt. The examples below show the .sh form. On Windows, swap the extension.
The Everyday Commands
| Command | What it does | Your data |
|---|---|---|
./status.sh | Shows what is running and whether each part is healthy | Untouched |
./stop.sh | Stops everything, leaves it ready to resume | Kept |
./start.sh | Starts it again after a stop | Kept |
./up.sh | Applies changes you made to .env or the other settings files | Kept |
./create.sh | Rebuilds everything from scratch, including a brand new database | Deleted |
./destroy.sh | Removes the containers and the database entirely | Deleted |
The two that delete your data both ask for confirmation first when there is a database to lose.
On Windows, start is a built-in command, so type .\start.cmd rather than start.cmd or you will open a new window instead of starting your stack.
Changed a Setting? Use up, Not create
This is the one distinction worth learning, because getting it backward costs you your data.
./up.sh keeps your database. Use it whenever you have edited .env or one of the settings files next to it. It applies the change to the running system and leaves everything you have built alone.
./create.sh wipes your database and starts over. Use it on a first install, or when you deliberately want a clean slate.
The reason they are separate commands rather than one command with a flag is that the database only sets itself up once, on the very first start. Restarting or even fully recreating the containers keeps the existing database exactly as it was. So if you change something that only applies at database creation time and then wonder why nothing happened, that is the rule you are seeing, not a bug.
Adding People
With semantius-idp, sign in as the administrator and go to http://localhost:3000/idp/admin. That is where accounts are created and managed.
With your own identity provider, people are managed there: in Entra, you assign them to the Semantius API, as the Entra page describes. Either way, what each person can see is granted in the Semantius app, under Administration, then Users.
The rest of this section is about semantius-idp’s email.
By default the stack does not send email. That is deliberate: a local install with no mail service configured would otherwise offer password reset links that never arrive. The practical consequence is that an administrator creates accounts and sets passwords directly, and there is no self-service password reset.
To turn email on for a real deployment, add a Resend API key to .env as RESEND_API_KEY and run ./up.sh. Password resets, email verification, and notifications switch on together. Resend is the only email service the sign-in service supports; there are no settings for your own SMTP server.
Reaching the API From a Script
Browsers sign in interactively, but scripts and scheduled jobs cannot. For those, semantius-idp offers personal API keys, through the API gateway built into it:
- Sign in and open
http://localhost:3000/idp/account/api-keys. - Create a key and copy it. You only see it once.
- Send it with every request, as the
x-api-keyheader, to the/gateway/rest/address:
curl -H "x-api-key: YOUR_KEY_HERE" \
"http://localhost:3000/gateway/rest/customers?limit=5"
That address is the same API as /rest/, with the key exchange handled for you. The key belongs to your user account, so it can see exactly what you can see and nothing more. Treat it like a password: anyone holding it can act as you.
With your own identity provider, such as Entra, a script gets an access token from that provider and sends it as a bearer token to /rest/. If you want personal API keys there as well, put an API gateway of your choice, such as Kong, Tyk or AWS API Gateway, in front of /rest/.
Browsing the API
http://localhost:3000/api-docs/ lists every endpoint your model generates, with request and response shapes and a Test Request button on each one. With semantius-idp, when you are signed in to the app in the same browser, those test requests are authenticated automatically, so you can explore the whole API without pasting a credential anywhere. With your own identity provider, paste an access token into the docs page instead.
The documentation is generated from your live model. Change the model and the docs change with it.
Connecting the semantius CLI
The semantius CLI, the one coding agents use, works against a self-hosted stack the same way it works against a hosted one. Point it at your front door address, localhost:3000 or whatever domain you deployed to:
semantius use localhost:3000It configures itself from there: every stack publishes a small description of itself at /.well-known/semantius.json that tells the CLI where to sign in and where the API lives. Nothing has to be passed by hand.
This is not the pg_semantius CLI, which connects straight to the database for administration. You do not need that one to use a self-hosted stack.
You can read that document yourself to check a deployment is answering correctly:
curl http://localhost:3000/.well-known/semantius.jsonConnecting a Database Client
The stack publishes two database ports on purpose, for the programs that talk SQL rather than HTTP.
Port 6432 is the pooled connection for your own applications. It handles many short-lived connections far better than connecting to the database directly. It logs in as semantius_authenticator, with the SEMANTIUS_AUTHENTICATOR_PASSWORD value from your .env.
Port 5434 is the direct connection, for administration and for migrations with the pg_semantius CLI, and for anything that needs a session of its own. Connect as postgres with the POSTGRES_PASSWORD value from your .env. The database is named semantius.
On a public server, restrict both to the machines that need them, as Going Live shows.
If you just want to look at some data and do not want to install a client at all, semantius-idp’s administrators have a read-only SQL console at http://localhost:3000/idp/admin/database. Every query there runs read-only and is recorded in the audit log, which also means an administrator who can open it can read everything stored in the database. Worth knowing before you hand out administrator accounts.
Backing Up
Your data lives in one PostgreSQL database, so a backup is a plain pg_dump. Run this from the variant folder while the stack is up:
docker exec semantius-postgres pg_dump -U postgres -Fc -d semantius > semantius.dump
That writes a single file you can copy anywhere. It runs as the postgres superuser on purpose: an ordinary account would only back up the rows it is allowed to see.
./create.sh and ./destroy.sh delete the database, and there is no undo. The data sits in a Docker volume called semantius_pgdata, and those two commands remove it. Take a dump before running either one on a stack you care about.
For a server, put the command on a daily schedule. Keep the copies somewhere other than that server, encrypt them if they leave your network, and decide how many days to keep. A backup you have never restored is a hope rather than a backup, so try a restore on a test machine now and then. Restoring follows the rules in pg_semantius: Upgrade and Backup: restore into an empty database, as the postgres user, with no extra flags.
Choosing a Version
By default the stack runs the latest published version of every piece. It downloads that version each time you run ./create.sh or ./up.sh. A plain restart, a reboot or ./start.sh does not download anything, and keeps running what is already there. That is convenient while you are trying things out, and the wrong default for a server you depend on.
To pin a database version for one run, pass the tag:
./up.sh 0.4.0-pg18To make it permanent, set SEMANTIUS_DB_VERSION in .env. There are matching SEMANTIUS_APP_VERSION and SEMANTIUS_IDP_VERSION settings for the web app and the sign-in service. Pin all three before you put a stack into real use, so an update happens when you choose rather than on the next ./up.sh.
The published versions are listed on each image’s package page: database, web app and sign-in service. Database tags always end in -pg18, for example 0.4.0-pg18.
The other pieces have no setting in .env yet. The API server, the connection pooler, the API docs viewer and the key fetcher run their latest version, and the front door runs Caddy 2-alpine. Each ./up.sh or ./create.sh downloads their newest release. Once a combination works for you, pin at least the API server and the docs viewer to fixed tags too, by setting their image: in a docker-compose.override.yml.
Updating to a New Version
Updating the web app and the sign-in service is one step: change SEMANTIUS_APP_VERSION or SEMANTIUS_IDP_VERSION in .env and run ./up.sh. Your data is kept.
Updating the database takes one more step. The database sets itself up only once, when it is first created. A newer SEMANTIUS_DB_VERSION therefore brings newer extension files, but it does not change the schema of a database that already exists. Take a backup first, then change the tag and run ./up.sh, then apply the update inside the database:
docker exec semantius-postgres psql -U postgres -d semantius \
-c "ALTER EXTENSION pg_semantius UPDATE;" \
-c "SELECT semantius.migrate();"
Both statements are safe to run again, because migrate() records what it has already applied and skips it. To see what is installed and whether anything is still pending, run SELECT * FROM semantius.status(); the same way. pg_semantius: Upgrade and Backup explains both in more detail, including which old versions have no upgrade path.
Updating the Stack Files
The compose file, the front door configuration and the scripts also change over time. You pick up a newer set by pulling the repository again, from the folder you cloned:
git pullThen run ./up.sh from your variant folder. It recreates the containers from the new files and keeps your database.
Your .env and a docker-compose.override.yml survive this, because neither is part of the repository. A change made directly to a file in the variant folder does not: git pull stops on the conflict. Keep your changes in those two files, or in a variant of your own, as Choose or Build Your Variant explains.
When Sign-In Suddenly Stops Working
The sign-in system periodically replaces the keys it uses to sign your login tokens, and the API reads those keys once, when it starts. If people are signed in fine but every API call is rejected, the API is most likely holding the previous set.
./jwks-refresh.shThat fetches the current keys and restarts the API, which takes about a second. Your database stays up and nobody is signed out. Most providers rotate slowly enough that running this when you hit the problem is fine. If yours rotates often, put it on a daily schedule.
Next Steps
- Going Live: run the stack on a real server, behind your own domain and with HTTPS.
- Settings Reference: every value in
.env, grouped by what it does. - Dokploy: deploy through Dokploy without editing config files.
- Microsoft Entra ID: sign people in with their Microsoft work accounts instead of the bundled provider.