Install¶
Two commands get from a machine with Docker on it to an agent that answers:
docker compose up -d against one downloaded file, and a bootstrap inside the
container it started. This page is those two commands, the source build for
anyone changing the code, and what to do when something does not come up.
Every step is idempotent — re-run any of them whenever you are not sure it took.
One command¶
scripts/quickstart.sh needs Docker and nothing else - the Compose plugin at
2.24 or later, which it checks. It downloads docker-compose.yml at the latest
release into ./agenticos, writes a .env beside it (mode 0600) with a
generated SECRET_KEY, VAULT_MASTER_KEY and sandbox token, asks four
questions, pulls the published images and brings the stack up - console
included - creates an organization with an owner and a published agent, and
optionally mirrors the MCP registry. Run from inside a clone, it builds the same
images from the tree instead. A docker-compose.yml that belongs to another
project is left alone: the install goes into ./agenticos beside it.
It takes --check to only report what is missing, --dry-run to print every
command it would run without running one, and --yes with --provider,
--api-key, --email, --password and --org for an unattended install.
Everything below is what it does, in case you would rather do it yourself - and there is no step it takes that you cannot.
Requirements¶
| To | You need |
|---|---|
| Run it | Docker with the Compose plugin, 2.24 or later - Docker Desktop, OrbStack, or Engine with docker-compose-plugin. https://docs.docker.com/get-docker/ |
| Change it | The above, plus GNU Make, uv and bun - make install checks for all three |
On Windows, use WSL2
The Makefile and the shell helpers assume bash. WSL2 or Git Bash. Once you are inside one, everything below is identical.
Run it from the published images¶
The product is two images, ghcr.io/vstorm-co/agenticos-backend and
ghcr.io/vstorm-co/agenticos-frontend, published by
every release for amd64 and
arm64. docker-compose.yml at the root of the repository pulls them and starts
everything around them, and it works on its own:
mkdir agenticos && cd agenticos
curl -fsSLO https://raw.githubusercontent.com/vstorm-co/agenticos/main/docker-compose.yml
docker compose up -d
That pulls the images and starts Postgres (with pgvector), Redis, the Prefect server and runner, the API and the console, runs the migrations, and answers on http://localhost:3000. The first pull is about 2 GB.
flowchart LR
F["frontend<br/>:3000"] --> A["api<br/>:8000"]
A --> PG[("postgres<br/>pgvector")]
A --> RD[("redis")]
A --> SD["sandboxd<br/><i>holds the Docker socket</i>"]
A --> PF["prefect server"]
PF --> WK["prefect runner"]
WK --> PG
M["migrate<br/><i>runs once, exits</i>"] --> PG
There is no .env to write first
Every variable in docker-compose.yml carries a default, deliberately. Write
a .env beside it when there is something to change - all of it optional:
AGENTICOS_VERSION |
Which release to run. latest when unset; a version such as 0.0.380 to pin one, edge for whatever main last published |
PUBLIC_API_URL, PUBLIC_WS_URL, PUBLIC_SITE_URL |
What the browser is told to call, when the host is reached by a name other than localhost. The backend's FRONTEND_URL and CORS_ORIGINS are the same fact from its side |
OAUTH_PROVIDERS, CHAT_MAX_UPLOAD_SIZE_MB |
The sign-in buttons the console offers, and what the composer refuses before uploading |
SECRET_KEY, VAULT_MASTER_KEY |
Optional on a laptop, where the defaults are a constant from the repository and a vault sealed under it. scripts/quickstart.sh generates both; by hand, openssl rand -hex 32 each - and back the vault key up with the database, because a dump restored beside a different key is unreadable |
Anything from backend/.env.example |
A provider key, SMTP, a Logfire token - the containers read the same file |
The images read that .env, and backend/.env when there is one, so a clone
keeps its settings where the rest of this documentation says to look.
The sandbox service - the one that gives an agent a container to run code in -
is behind the sandbox profile, because it holds the Docker socket and refuses
to start without a token of its own:
echo "SANDBOXD_TOKEN=$(head -c 32 /dev/urandom | base64)" >> .env
docker compose --profile sandbox up -d
The token is generated once and then left alone. Regenerating it orphans every
workspace the service is holding. (scripts/quickstart.sh does both of these
for you.)
Or build it from a clone¶
A clone has docker-compose.override.yml beside the base file, and Compose
merges the two on its own - so the same docker compose up that pulls images in
an empty directory builds them from the tree here, bind-mounts the source, and
reloads the API on every edit. That is what make dev runs, with the sandbox
profile on and a SANDBOXD_TOKEN generated into backend/.env first (it never
regenerates one that is there).
When you do want to change something — a provider key on the host, a different
database name — edit backend/.env. make install creates it from
backend/.env.example when there is none, and never overwrites it afterwards, so
the file holding your keys survives every re-run.
The first build takes a few minutes: the backend image carries LibreOffice and Tesseract for document parsing, and the console is a Next.js production build. Afterwards Docker's layer cache makes it about a minute, and the bind mounts mean an edit needs no rebuild at all.
flowchart LR
F["frontend<br/>:3000"] --> A["api<br/>:8000"]
A --> PG[("postgres<br/>pgvector, :5432")]
A --> RD[("redis<br/>:6379")]
A --> SD["sandboxd<br/><i>holds the Docker socket</i>"]
A --> PF["prefect server<br/>:4200"]
PF --> WK["prefect runner"]
WK --> PG
Migrations run as the migrate service every time the stack starts, and are a
no-op when the database is already at head - which is why make dev is also the
command to re-run after any code or config change.
The console, in a clone¶
Not started by make dev, and not an oversight
In a clone the console sits behind the console compose profile, so that
working on the API does not rebuild a frontend image, and so that running
bun dev on your host is not fighting a container for port 3000. Outside a
clone there is no profile: docker compose up starts it with everything else.
Create an organization, an owner, a model and an agent¶
make platform-bootstrap BOOTSTRAP_API_KEY=sk-... # in a clone
docker compose exec -T -e BOOTSTRAP_API_KEY=sk-... app \
agenticos cmd bootstrap # anywhere else
This is the one that turns an empty database into something you can use.
An empty AgenticOS is a chicken-and-egg problem — an agent needs a model, a model needs a key, a key needs an organization — and this walks that chain once:
| It creates | |
|---|---|
| An organization | Acme, or --org |
| An owner | admin@example.com / admin123, or --email / --password |
| A vault entry | Your provider key, sealed for that organization |
| A model profile | gpt-4.1, claude-sonnet-4-6, gemini-2.5-pro or openai/gpt-4.1, whichever provider the key is for |
| A context file | AGENTS.md, explaining what this platform is |
| An agent | @getting-started, published if there is a key |
Now open http://localhost:3000, sign in as admin@example.com / admin123, and
go to Agents → Getting Started → Test.
You have a working agent. Not a stub: it can search the web and fetch a page,
run Python, draw a chart, keep files and a shell, plan a piece of work, remember
something across conversations, search what was said earlier, read the shipped
skills and AGENTS.md, and hand independent work to sub-agents it invents on
the spot. All of it on the one key you just pasted — the sandbox runs in the
run's own store and search defaults to DuckDuckGo, so there is no second
credential and no service to stand up.
Open it in the Builder to see how: every one of those is a capability that was switched on, and the agent's instructions are a text box you can edit.
No provider key yet?
Leave BOOTSTRAP_API_KEY out. Everything is still created and the demo agent
is saved as a draft rather than published — an agent with no model cannot
answer, and publishing one that fails on its first message is worse than not
publishing it.
Add a key under Settings → AI providers, then publish.
make seed is a different thing
It creates admin@example.com as a deployment superadmin and nothing else:
no organization, no model, no agent. make platform-bootstrap creates that
user too, so on a fresh install you want bootstrap.
make dev prints a suggestion to run seed. It is the older path, and still
valid if all you want is an admin login.
Check it¶
doctor asks the questions a first message would ask. Is the database reachable
and at head? Does the vault decrypt? Is there a model profile with a key behind
it? Does every registered sandbox connection answer with a runtime?
Each line names the part that is missing, rather than telling you something failed.
Recap¶
mkdir agenticos && cd agenticos
curl -fsSLO https://raw.githubusercontent.com/vstorm-co/agenticos/main/docker-compose.yml
docker compose up -d # everything, from the published images
docker compose exec -T -e BOOTSTRAP_API_KEY=sk-... app \
agenticos cmd bootstrap # an org, an owner, a model, an agent
Then http://localhost:3000, admin@example.com / admin123. To change the
code instead: git clone, make dev, make dev-frontend, make platform-bootstrap.
When it does not come up¶
| What you see | Why |
|---|---|
Ingestion 500s with extension "vector" is not available |
Stock Postgres instead of pgvector/pgvector:pg16. See below |
uv run reports Python 3.13 or 3.14 |
backend/.venv resolved past the pin. Delete it and re-run uv sync |
| The frontend loads but every request fails | The API is still starting - it waits for the migrate service - or the browser was told the wrong host: PUBLIC_API_URL and PUBLIC_WS_URL have to be reachable from where the browser is. docker compose logs migrate app |
docker compose up fails with unauthorized on ghcr.io/vstorm-co/... |
The package is private, or a stale docker login to GHCR is in the way. The images pull anonymously; docker logout ghcr.io and try again, and if it still refuses the package's visibility is the problem, not your machine |
The app service is Up and unhealthy, and every request hangs |
A wedged event loop. The worker takes itself down after 15s and something replaces it, in all three stacks — so if it is still hanging a minute later, EVENT_LOOP_WEDGED_AFTER is set to 0 somewhere, which is what a debugger needs and what nothing else should. docker inspect shows 137 with OOMKilled=false, and the log line above it says which |
The sandboxd service exits immediately |
No SANDBOXD_TOKEN in .env or backend/.env. make sandbox-token in a clone, or write one, then up -d again |
Files says This host's files could not be read and names workspace_root |
A sandbox service started before it had one. Recreate it — docker compose --profile sandbox up -d sandboxd — and docker rm the leftover sandboxd-* containers: a persisted container is reattached with the mounts it was created with, so an old session keeps writing where nothing can read it |
Stopped: another AgenticOS stack named 'agenticos' runs on this machine |
Compose names a project after its directory, so a clone at ~/agenticos and an install at ./agenticos are one project to Docker, and starting the second would take over the first's containers and database - under a freshly generated VAULT_MASTER_KEY that cannot read what the first sealed. The installer refuses instead; stop the other stack (docker compose down keeps its volumes) or install under another name with --dir |
| A port is already taken (3000, 5432, 6379, 8000, 4200) | Something else is on it. make dev-down, stop the other process, start again |
| Anything stranger | make docker-clean wipes containers, networks and volumes — all local data — then make dev from scratch |
The database must be pgvector¶
Not stock Postgres
If document ingestion 500s on a fresh environment, check the image before you check anything else.
The retrieval store issues CREATE EXTENSION IF NOT EXISTS vector the first time
a collection is written to. Stock Postgres answers
extension "vector" is not available — a 500 before any row is committed.
Every compose file in this repository pins pgvector/pgvector:pg16.
Day to day¶
make dev # start or restart (idempotent); in a clone, from source
make dev-down # stop everything
make dev-logs # tail logs
make dev-rebuild # force-rebuild the backend image after a pyproject change
make dev-frontend # start the console container (behind the `console` profile in a clone)
Outside a clone the same four are docker compose up -d, down, logs -f, and
docker compose pull && docker compose up -d to move to a newer release.
And where everything is:
| Frontend | http://localhost:3000 |
| API | http://localhost:8000 |
| OpenAPI docs | http://localhost:8000/docs |
| Django-style admin | http://localhost:8000/admin |
| Prefect UI | http://localhost:4200 |
| Postgres | localhost:5432 (postgres / postgres) - published by the clone's override file only |
| Redis | localhost:6379 - the same |
The sandbox service is not published, on purpose
It holds the Docker socket, which is an unauthenticated API for root on the host. It is reachable only from inside the compose network, and the API proxies whatever a browser needs to see of it.
Running the backend on your host¶
Useful for breakpoints and IDE debugging. The services stay in Docker; the API does not.
make install # .env + uv sync + bun install + pre-commit
docker compose up -d db redis
make db-upgrade # apply migrations
make run # uvicorn --reload
make install is the whole setup path: backend/.env from the example if there
is none, uv sync for the backend, bun install --frozen-lockfile for
frontend/node_modules, and the pre-commit hooks.
None of the three is optional, and each was missing at some point:
backend/.envis what everything running on your host reads —db-check,db-upgrade,runand pytest, all throughapp.core.config. Without one,POSTGRES_PASSWORDis empty andalembic checkis refused withfe_sendauth: no password supplied.frontend/node_modulesholds eslint, prettier, tsc, vitest and next, so the frontend half is owed even if you only ever touch Python.make checkruns all five.
Both are per-checkout and shared between no two worktrees, so this is owed on every clone rather than once per laptop.
Python is pinned to 3.12
backend/.python-version pins it, matching requires-python,
backend/Dockerfile and every CI job. If uv run python -V reports anything
else, delete backend/.venv and re-run uv sync — a newer interpreter has
reachable APIs that the one which ships does not.
Environments¶
Three. Every one runs the two published images, at the AGENTICOS_VERSION its
env file names; the laptop is the one that builds them from the tree instead.
| Target | Compose files | Use |
|---|---|---|
docker compose up |
docker-compose.yml |
The product, from the published images. Console included, migrations run on start, every variable defaulted |
make dev |
docker-compose.ymldocker-compose.override.yml |
Local, in a clone. The override builds from source, bind-mounts it, reloads, and publishes Postgres and Redis to the host |
make dev-server |
docker-compose-dev.ymldocker-compose-dev.frontend.yml |
A deployed dev environment. Pulls edge, no bind mounts, no database port, verbose logging |
make prod |
docker-compose-prod.ymldocker-compose-prod.frontend.yml |
Production. Pulls a pinned release; resource limits, internal data network, tuned Postgres |
Each has matching -down, -logs and -frontend siblings. make stage is kept
as an alias for make dev-server, which is what it used to be.
Both deployed environments want a reverse proxy in front of them, and there are
two ways to give them one. By default the stack publishes both ports on the
loopback and a proxy on the host reaches them - nginx/nginx.conf is that
template, and it resolves backend:8000 and frontend:3000 as network aliases.
make prod PROXY=traefik instead adds two overlay files that put the containers
on an existing Traefik's network with the labels it discovers them by.
Deploy walks through both.
The proxy reaches them by those aliases, so production publishes both ports on
127.0.0.1 and nothing off the host can reach either directly. That is a
security boundary rather than tidiness: with
RATE_LIMIT_TRUST_FORWARDED_FOR
on, whatever can reach past the proxy chooses the address its requests are
counted against. Set BIND_HOST=0.0.0.0 for a proxy that runs somewhere else.
What supervises the API differs in all three, and each recovers a worker that
died: the local stack runs its own reload supervisor, the dev stack is a single
process whose exit Docker restarts, and production runs four workers under
uvicorn's Multiprocess. A worker that is wedged rather than dead is handled
the same way everywhere — the worker kills itself. See
Configuration.
PUBLIC_* are what the browser is told, and they are read at start
PUBLIC_API_URL, PUBLIC_WS_URL and PUBLIC_SITE_URL are the addresses the
console hands the browser - the chat WebSocket and the sign-in redirect reach
the API directly, so they have to be names a browser can resolve, never a
container name. The dev-server and production frontend files refuse to start
without them.
The console reads them when the container starts, so the published image is the same for every deployment and a change is a restart. Getting one wrong is still the classic failure: server-side rendering keeps working over the compose network while every call from the browser goes to the wrong host.
Next¶
-
From a key to a published, metered agent.
-
What a spec, a version and an exposure actually are.
For every setting there is, see Configuration. For getting this onto a real host, see Deploy.