Skip to main content

Running ConnectedCampus on Windows without Docker

For a Windows machine where Docker Desktop itself is the problem — commonly an 8GB laptop, where Docker Desktop's WSL2 VM reservation defaults to up to 50% of host RAM regardless of what's actually running inside it. This walks through running the full dev stack with zero Docker containers, step by step, either against Postgres installed directly on this machine or — usually less setup — against a team's already-deployed shared database over WireGuard (step 5 covers both).

Why this goes through WSL2, not "native Windows"​

This repo's Postgres needs two extensions compiled against it from source: pgvector and Apache AGE (shared_preload_libraries = 'age') — no prebuilt binary ships either for Windows, and Apache AGE in particular has no realistic native-Windows build path (its build system expects a Unix toolchain). WSL2 is a real Linux kernel, not emulation, so it's the one place on a Windows machine this can be built and run exactly the way infra/postgres/Dockerfile already does it for Docker — same apt packages, same make && make install steps, just on the host filesystem instead of inside a container. There is no Docker anywhere in this path: not for Postgres, not for the Bun apps. WSL2 is the OS underneath, the same way it always is for a Docker Desktop setup — the difference here is nothing runs containerized on top of it.

If you've never used WSL2 before: it's a lightweight Linux environment that runs alongside Windows (not a VM you manage yourself, not dual-boot) — you keep using Windows normally, and open a Linux terminal when you need one. Editing files from VS Code's Remote - WSL extension works exactly like editing local files.

1. Install WSL2 + Ubuntu​

From PowerShell as Administrator (Windows side):

wsl --install

This installs WSL2 and Ubuntu (the default distro) in one step on a current Windows 10/11 build. Restart when prompted. On first launch of the "Ubuntu" app from the Start menu, it finishes setup and asks you to create a Linux username/password — this is separate from your Windows login, and only exists inside WSL2.

If wsl --install reports WSL is already installed but no distro is present, run wsl --install -d Ubuntu instead.

Everything from here on runs inside the Ubuntu (WSL2) terminal, not PowerShell/cmd.

2. Work inside the Linux filesystem, not /mnt/c​

Clone the repo under your WSL2 home directory (e.g. ~/ConnectedCampus), not under /mnt/c/Users/.... Files under /mnt/c are on the Windows filesystem accessed through a translation layer — bun install and Postgres both do a lot of small file I/O, and that layer makes it dramatically slower. Working entirely inside ~ avoids this; VS Code's Remote-WSL extension opens ~/ConnectedCampus directly with no translation either.

cd ~

3. Install Bun and git​

Ubuntu ships git already; if not: sudo apt-get update && sudo apt-get install -y git.

curl -fsSL https://bun.sh/install | bash
exec $SHELL # reload so `bun` is on PATH
bun --version # should be 1.4+ — the repo pins bun@1.4.2 in package.json

4. Clone and install​

git clone <repo-url> ConnectedCampus
cd ConnectedCampus
bun install
cp .env.example .env

Set BETTER_AUTH_SECRET in .env to any random string (openssl rand -hex 32) — auth will not work without it. Also set INTERNAL_SERVICE_TOKEN (same command) — apps/agents and apps/credentials fail closed (503 on everything but /health) without it. See Getting Started for what every other variable does; nothing about them changes on this path.

5. Set up Postgres — two options​

If your team already has ConnectedCampus deployed on a shared VPS, use Option A — it's less work (no compiling extensions from source) and puts you on the same real data as everyone else. Use Option B only if you're working offline, or there's no shared instance yet.

Postgres already exists on the VPS, reachable only through a WireGuard tunnel — never on the public internet (see Remote Postgres over WireGuard for the full reasoning). Nothing to compile or install except the WireGuard client itself.

Ask whoever administers the VPS for two things, sent through a secure channel (not chat/email in plaintext — both are credentials):

  1. A WireGuard peer config — they run ./infra/wireguard/add-peer.sh <your-name> on the VPS and send you the resulting .conf file.
  2. The real DATABASE_URL and RUNTIME_DATABASE_URL values for the shared database.

Install and bring the tunnel up, inside WSL2 — not the Windows WireGuard app. The Bun processes you're about to run live inside WSL2's own network namespace, which is separate from Windows': a tunnel brought up only through the Windows-side app would not be reachable from them.

sudo apt-get install -y wireguard-tools
mkdir -p ~/.wireguard
# save the .conf file you were given as ~/.wireguard/<your-name>.conf, then:
chmod 600 ~/.wireguard/<your-name>.conf
sudo wg-quick up ~/.wireguard/<your-name>.conf

Verify it's actually connected, not just configured:

sudo wg show # a recent "latest handshake" line confirms it's live
ping -c 2 10.8.0.1 # the VPS's tunnel address
pg_isready -h 10.8.0.1 -p 55432 # the shared Postgres, reachable only through this tunnel

Then in .env (after cp .env.example .env), replace DATABASE_URL and RUNTIME_DATABASE_URL with the real values you were given — same shape as below, with a real password instead of <password>:

DATABASE_URL=postgres://campus:<password>@10.8.0.1:55432/campus_os
RUNTIME_DATABASE_URL=postgres://campus_app:<password>@10.8.0.1:55432/campus_os

Skip step 6 below (Migrate and seed) — the schema already exists on the shared database, and running migrations isn't something every developer needs to do day-to-day. Go straight to step 7.

This is the same live data every other developer and the deployed app itself use — not a personal sandbox. Anything your local API writes while testing (creating a student, editing attendance, etc.) is a real write everyone else will see. Bring the tunnel down when you're done for the session: sudo wg-quick down ~/.wireguard/<your-name>.conf.

Option B — install Postgres natively on this machine​

./infra/postgres-native/setup.sh

This is the script referenced in Getting Started — it adds the PGDG apt repo, installs Postgres 18, builds pgvector and apache/age from source, configures shared_preload_libraries = 'age', and creates the campus role + campus_os database. It listens on port 55432 (matching docker-compose.yml's mapping), so .env.example's DATABASE_URL / RUNTIME_DATABASE_URL need zero changes. Takes a few minutes the first time (compiling two Postgres extensions from source); safe to re-run if it fails partway through.

It ends by verifying both extensions are actually loaded:

select extname from pg_extension where extname in ('vector', 'age') order by extname;

Confirm you see both age and vector in the output before continuing.

6. Migrate and seed​

Option A only skips this step — the shared database is already migrated and seeded. If you set up Postgres natively (Option B), run this once:

bun run db:generate # only if you changed the Drizzle schema — skip on a fresh clone
bun run db:migrate # applies migrations, RLS policies, then creates campus_app
bun run db:seed # seeds the 20 KIOT module_definitions rows

7. Run it​

bun run dev

Turborepo runs every app in parallel — apps/web at http://localhost:3000. WSL2 forwards localhost automatically on current Windows builds, so this works the same from a Windows browser as it would from inside WSL2 itself — no port-forwarding setup needed.

If you only need one service (lightest possible setup — Postgres + one Bun process):

bun --filter '@campus/api' dev
curl http://localhost:4000/health

There is still no tenant/admin user at this point — either run the import below, or create one by hand; see Getting Started for both options.

Even without Docker Desktop's reservation, WSL2 itself can grow to use most of the host's RAM under load. Cap it explicitly — create/edit %UserProfile%\.wslconfig on the Windows side (a Windows-side file, not something you create from inside WSL2):

[wsl2]
memory=6GB
processors=4

Then from PowerShell: wsl --shutdown, and reopen your Ubuntu terminal for it to take effect.

Optional: importing the real legacy institution data​

Skip this if you're on Option A — the shared database already has the real data imported; importing again would be redundant (and the import scripts assume they're the only writer).

For Option B (a fresh, empty local Postgres): Getting Started's import pipeline loads a legacy MySQL backup — and that step still uses a throwaway Docker MySQL container (infra/import/docker-compose.mysql.yml), just to load mysqldump output reliably. If you're committed to zero Docker on this machine, skip the import entirely and start with an empty institution instead — everything in steps 1–7 above works fully without it. If you do want the real data, that one step needs Docker installed for its duration; nothing else in this guide does.

Troubleshooting​

  • bun install or Postgres build fails with a permissions error inside /mnt/c — you're working outside the Linux filesystem; re-clone under ~ (step 2).
  • Git shows every file as modified with no real changes — usually CRLF line endings from a Windows-side clone leaking in. Run git config --global core.autocrlf input inside WSL2 before cloning.
  • localhost:3000 doesn't load from a Windows browser — confirm you're on WSL2 (wsl -l -v from PowerShell should show VERSION 2, not 1); WSL1 doesn't forward localhost the same way.
  • Port 55432 (or 5432) already in use (Option B only) — something else is already listening; check with sudo ss -tlnp | grep 5432 and stop or reconfigure it before re-running infra/postgres-native/setup.sh.
  • .wslconfig changes don't seem to apply — they only take effect after wsl --shutdown fully stops the WSL2 VM (not just closing the terminal window) and it's restarted.
  • (Option A) pg_isready -h 10.8.0.1 -p 55432 times out — the tunnel isn't actually up. Check sudo wg show for a recent handshake; if there's none, re-run sudo wg-quick up ~/.wireguard/<your-name>.conf and check its output for errors (a common one: the .conf file's permissions aren't 600, or it was saved with Windows CRLF line endings — re-save it with Unix line endings if wg-quick complains about parsing it).
  • (Option A) Connected, but the app can't reach the database from inside WSL2 — confirm the tunnel was brought up with wg-quick inside the WSL2 terminal, not the Windows WireGuard app; see the note in step 5's Option A about the two having separate network namespaces.

Verifying everything works​

bun run typecheck # across every package
bun run test # needs Postgres up — rls.test.ts hits the real database

bun run test will fail if Postgres isn't reachable or the extensions aren't loaded — same check as step 5's verification (either option), exercised for real. If you're on Option A, this runs against the real shared database — the RLS test itself only reads/asserts, but be aware any other test that writes data would be a real write there too.