Running SkillNet
In order, top to bottom. Nothing to skip, and only one thing to decide (step 2).
If you are an AI agent and were asked to “start the project”, this file is the whole answer. Everything else is detail you do not need yet.
Step 0 — What you need
Docker with Compose v2. docker compose version should print 2.x.
Nothing else: Python, Node and PostgreSQL all run inside the containers.
Step 1 — Clone and copy the config
git clone https://github.com/ANFAIA/SkillNet.git
cd SkillNet
cp .env.example .env
Step 2 — Decide what powers it
This is the only real decision, and everything else follows from it. Pick a row, put those
values in your .env, and move on.
| I want to use… | Put in .env |
What it costs you |
|---|---|---|
| An API key (recommended) | LLM_API_KEY=sk-… — nothing else |
Fast: seconds per screen, around 0.01 USD per generated course. The defaults gpt-4o-mini and text-embedding-3-small already match the database schema and both run off this one key. Any litellm provider works instead — set LLM_MODEL=anthropic/claude-sonnet-4-20250514, deepseek/deepseek-chat, groq/llama-3.1-8b-instant… |
| A local model | Nothing — use the overlay in step 3 | Free, private, offline. But slow: measured ~185 s to generate one lesson screen on CPU. Needs ~8 GB of RAM and ~5 GB of disk. Good for trying it without an account; not comfortable for real use. |
| Nothing at all | LLM_MODEL=fixture/local and EMBEDDING_MODEL=fixture/local |
Free and instant, but only screens with a recorded response render. Enough to click through the interface; not enough to author a course. |
Whichever row you picked, two values are always required:
| Variable | How to fill it |
|---|---|
SECRET_KEY |
python -c "import secrets; print(secrets.token_urlsafe(32))" |
POSTGRES_PASSWORD |
Same generator. Letters, digits, - and _ only |
The password restriction is not a style preference. It is interpolated into a connection URL
without escaping, so a @, :, / or # — exactly what a password manager produces —
splits the URL, and the API then fails to reach the database with an error that never
mentions the password.
.env.example already ships working demo credentials (admin@skillnet.dev / admin123), so
there is nothing else to set. Dynamic courses (v2) need no flag — the seed data already
includes a validated dynamic course, and any new course can opt in per-course.
Step 3 — Start it
docker compose up -d --build
Or, if you picked the local model in step 2:
docker compose -f docker-compose.yml -f docker-compose.ollama.yml up -d --build
A cold build takes a couple of minutes. The ollama overlay also downloads the models (a few
GB) before the API comes up, so give the first start time. See
docker-compose.ollama.yml for what it does and which model ids
are valid.
Step 4 — Load the demo data
This step is for exploring the demo. A real deployment skips it: you create your own content in the app — upload a document or describe a topic and let it generate a course. Run this only if you want the ready-made example to click around.
docker compose exec api uv run python -m src.seed_learning_demo
Startup creates the organization and the admin user, but no courses, no documents and no employees. Without this seed you log in to an empty dashboard — which is exactly right for a fresh install, and just an empty database if you meant to try the demo.
This seed is the public, self-branded SkillNet demo, on the meta theme of how we learn: four short, Brilliant-style courses (“Cómo aprende tu cerebro”, “Sesgos cognitivos”, “La ciencia de los hábitos”, “Memoria y olvido”), all generated and validated at seed time, plus three demo learners with different declared learning styles. The showcase course carries a podcast and an infographic per node so the in-lesson media components appear; the other three carry a course-level podcast. It is idempotent and re-runnable (it reuses an already-validated course of the same title), and it prints every account and per-course result. Generation is LLM-backed, so a full run is slow — that is expected.
The previous demo (a Spanish bakery-café) has been retired and removed from the codebase.
seed_learning_demois the public default; when it runs it also cleans up any leftover bakery-café data from the default org on dev databases that still carry it.
There is also a much smaller v1 seed, src.seed_demo (1 employee and 16 skills), which
predates dynamic courses and exists to compare the old static path.
Individual workspace mode
By default a deployment runs in organization mode (a company/team/class, the flow above).
The other mode is individual: one person who installs SkillNet for themselves and both
administers and learns — no employees, talent, assignments or org reports. See
docs/design/audience-modes.md.
The mode is a stable per-deployment setting, chosen one of two ways:
-
First-boot wizard (UI). If you leave
ADMIN_EMAIL/ADMIN_PASSWORDunset, the first time you open the app it shows a/setupscreen: pick the mode (Organization / Just me), create the owner, and you are signed in. The wizard closes for good once an owner exists. -
Headless (
.env). Set the owner and mode before the first boot (the mode is read only when the organization row is first created):WORKSPACE_MODE=individual # in your .env, alongside ADMIN_EMAIL / ADMIN_PASSWORD
Step 5 — Open it
| Role | Password | |
|---|---|---|
| Admin | admin@skillnet.dev |
admin123 |
| Learner — metaphors + audio (sees the in-lesson podcast) | ana@skillnet.dev |
aprender2026 |
| Learner — definitions-first + visual (sees the in-lesson infographic) | bruno@skillnet.dev |
aprender2026 |
| Learner — no profile, to walk the onboarding wizard | carla@skillnet.dev |
aprender2026 |
Log in as the admin to author courses, or as an employee to take them.
Did it work?
curl http://localhost:3000/api/v1/health
database must say connected and embeddings.status must say ok.
If embeddings.status is mismatch, the response also states exactly what to change. Worth
checking, because a wrong embedding dimension is the one misconfiguration that otherwise
fails silently: documents look ingested but nothing can retrieve them, and the tutor answers
from weaker sources without saying so.
Developing the frontend (hot reload)
The web container on :3000 is the production build — an nginx image baked at
docker compose build. Rebuilding it for every CSS tweak is the slow way and is not how
you develop the UI. For frontend work, run the API and database in Docker and the frontend
with Vite on the host, which hot-reloads on save:
# 1. API + DB in Docker (the dev overlay publishes the API on 127.0.0.1:8000)
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d db api
# 2. Frontend on the host — the one thing that needs Node (≥20) + pnpm locally
pnpm --dir apps/skillnet-web install # first time only
pnpm --dir apps/skillnet-web dev # Vite dev server
Then open http://localhost:5173 (Vite’s port), not 3000. Vite proxies /api to
http://127.0.0.1:8000, so it talks to the dockerized API; point it elsewhere with
SKILLNET_API_PROXY. Edit anything under apps/skillnet-web/src and the change appears
instantly — no docker compose build web.
Rebuild the web container only to check the real production bundle:
docker compose build web && docker compose up -d web → served on :3000.
When something is wrong
| Symptom | Cause |
|---|---|
docker compose up complains about a missing variable |
SECRET_KEY or POSTGRES_PASSWORD is empty in .env |
| API cannot reach the database, error does not mention the password | The password contains @, :, /, # or ?. See step 2 |
| Dashboard is empty after logging in | Step 4 was skipped |
embeddings.status: mismatch in /health |
EMBEDDING_DIMENSIONS does not match the column. The message says what to do |
Courses exist but open blank, using fixture/local |
No recording for that prompt. Expected; use an API key or the local model |
Something in .env seems to be ignored |
Probably is. Only variables listed in docker-compose.yml reach the container — there is no env_file. Add it to the environment: block of api |
Logs: docker compose logs -f api.
Ports
A default docker compose up -d publishes only 3000. The API and the database are
reachable only from inside the compose network, because nginx is where the security headers
and the upload limit live.
Everything optional binds to 127.0.0.1: the development overlay’s 8000 and 5432, plus
api-fixtures (8001), a2a (5000) and ollama (11434). Do not change those to 0.0.0.0 on
a shared network — Docker publishes ports with DNAT rules that bypass the host firewall.
web on 3000 is the deliberate exception; it is the front door. If you serve it beyond
localhost over plain HTTP, note that COOKIE_SECURE defaults to false, so session cookies
travel unencrypted. Put it behind TLS and set COOKIE_SECURE=true.
Stopping it
docker compose down # stop, keep the data
docker compose down -v # stop, destroy the database and uploads
Next: README.md for what SkillNet is and how it works,
AGENTS.md for conventions and boundaries when changing the code, and
docs/design/docker-deployment.md for why the deployment
is shaped this way.