Run PlacePrep offline for in-person exams with Docker and a local Appwrite: LAN setup, pre-exam sync, auto-submit and post-exam sync.
Self-Hosting Guide
PlacePrep can be run entirely on your own infrastructure — on a single machine, a college server, or a LAN setup for offline exams. This guide covers everything you need to deploy it yourself.
For a quick reference of every important command, see the Technical Reference page.
Requirements
- Docker Desktop (Windows/Mac) or Docker Engine (Linux) — v24+
- Bun v1.x — for running scripts and development mode
- 10 GB+ free disk space — for Docker images and Appwrite data
- Appwrite Cloud account (free tier works) — used as the production database
- Ports 80 and 3000 free on the exam machine
- Node.js is not required — Bun handles everything
Architecture
The self-hosted exam mode runs two separate environments:
| Environment | Used for | Database |
|---|---|---|
| Cloud (Appwrite Cloud) | Normal usage, production data | Cloud Appwrite |
| Local (Docker on your machine) | Offline exams on LAN | Local Appwrite |
The two environments use the same schema and same user IDs, so data syncs cleanly between them.
Sync directions:
| Direction | When | What moves |
|---|---|---|
| Cloud → Local (Pre-exam) | Before the exam | Users, exams, questions |
| Local → Cloud (Post-exam) | After the exam | Attempts, answers, new users |
Production Appwrite Setup
If you don't have a cloud project yet:
- Create a free account at appwrite.io
- Create a new Project
- Create a Database — note the Database ID
- Run the setup script to auto-create all collections:
powershell
bun run setup:appwrite - Create an API key (Settings → API Keys → all scopes) — copy it
- Fill in your
.env:
envNEXT_PUBLIC_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1 NEXT_PUBLIC_APPWRITE_PROJECT_ID=your_project_id NEXT_PUBLIC_APPWRITE_DATABASE_ID=your_database_id APPWRITE_API_KEY=your_api_key
Note: Usehttps://cloud.appwrite.io/v1(not a regional endpoint likefra.cloud.appwrite.io).
Local / Offline Exam Setup
Step 1 — Start local Appwrite
powershelldocker compose -f docker/exam-mode/docker-compose.yml up -d
Open http://localhost/console and:
- Create a local Appwrite account (any email + password — this is just for the local console)
- Create a Project → copy the Project ID
- Settings → API Keys → create a key with all scopes → copy it
Tip: Keep the console tab open — you'll need to verify the database got created in Step 3.
Step 2 — Configure .env
Add the following to your.env file:
envEXAM_MODE=true APPWRITE_LOCAL_ENDPOINT=http://localhost/v1 APPWRITE_LOCAL_PROJECT_ID=<from step 1 above> APPWRITE_LOCAL_DATABASE_ID=exam-db APPWRITE_LOCAL_API_KEY=<from step 1 above> EXAM_PASSWORD_PREFIX=exam2026
AboutEXAM_PASSWORD_PREFIX: Despite the name, this is not a prefix — the value you set here is the password, and nothing is appended to it. All students share it during the exam, so pick something short and easy to type (e.g.exam2026). Change it each academic year. If the variable is unset, it defaults toexam_2026_.
Step 3 — Create local database schema
Run the setup script pointing at your local instance:
This auto-creates thepowershellbun run setup:appwrite:local
exam-db database, all 16 collections with their attributes and indexes, and the tnp_documents storage bucket. It is safe to re-run — anything that already exists is skipped.
Verify it worked: Go to http://localhost/console → your project → Databases → you should see exam-db with 16 collections.
Step 4 — Build the application
powershellbun run build
This compiles the Next.js app with exam mode settings baked in.
Step 5 — Start the dev server (exam day)
Use the single command that starts Docker and the Next.js dev server bound to all interfaces:
powershellbun run exam:start
This is equivalent to:
docker compose -f docker/exam-mode/docker-compose.yml up -d(starts local Appwrite)next dev --hostname 0.0.0.0 --port 3000(students can reach the app over LAN)
Students connect at: http://<your-machine-ip>:3000
Admin dashboard: http://<your-machine-ip>:3000/exam-dashboard
Appwrite console: http://localhost/console
Finding your IP (Windows): Runipconfigin PowerShell and look for theIPv4 Addressunder your active Wi-Fi or Ethernet adapter (e.g.192.168.1.42).
Step 6 — Stop the server
powershellbun run exam:stop
This kills the Next.js process and stops the Docker containers. Student data in Docker volumes is preserved until you explicitly wipe it.
Before the Exam
Pre-exam checklist
- ✅ Connect the exam machine to the same Wi-Fi / LAN students will use
- ✅ Start the server:
bun run exam:start - ✅ Open
/exam-dashboard→ Pre-Exam Sync (Cloud → Local)- First time only: the Bootstrap Sync dialog asks for your
APPWRITE_LOCAL_API_KEY, since the local database has no accounts yet - This pulls all registered students, exams, and questions from cloud to local
- First time only: the Bootstrap Sync dialog asks for your
- ✅ Verify the exam appears in the dashboard's exam filter, and that it is published
- ✅ Note the shared Student Password shown in the Exam Mode Status card and tell students what it is
- ✅ Test by logging in as one student on another device
Walk-in students (not pre-registered)
Students who didn't register before the exam can simply sign up on the local app — their account is created in the local database and pushed to cloud during the post-exam sync.
You can also create the account directly:
Returns the student'sPOST /api/exam-mode/register-local { "name": "Student Name", "email": "student@example.com", "registration_number": "12345678", "branch": "CSE", "college_name": "Example College" }
userId and examPassword immediately. The student can log in right away.
There is no button for this on the dashboard yet — it is an API-only endpoint.
Walk-in students created locally are automatically synced to cloud when you run the Post-Exam Sync — no manual steps needed.
During the Exam
Admin dashboard features
| Feature | Description |
|---|---|
| Exam Mode Status | Shows whether exam mode is on, the local endpoint, project and database IDs, the shared student password, and allowed IPs |
| Data Sync | Pre-Exam Sync (Cloud → Local) and Post-Exam Sync (Local → Cloud), with a per-collection report |
| Exam Filter | Choose which live exam to monitor |
| Active Students | Live view of who is currently taking the exam |
| Student Progress | Per-student progress within the exam |
| Force Submit | Instantly submit a student's exam |
| Restart Exam | Clear a student's answers and restart their timer |
| Completed Students | Students who have submitted, with the option to delete an attempt |
| Auto-refresh (15s) | Refreshes the lists and runs the auto-submit sweep |
Auto-submit behavior
The system has two layers of auto-submit protection:
- Student-side timer: each student's timer counts down to zero, then auto-submits. The effective time is whichever is shorter: their remaining duration, or the time left until the exam's end time.
- Dashboard sweep: while the exam dashboard is open with Auto-refresh (15s) enabled, it fires a sweep every 15 seconds that force-submits any in-progress attempt whose exam end time has passed — even if the student's browser is closed or frozen.
The sweep is driven by the open dashboard tab, not by a cron job. Keep the dashboard open on the exam machine for the duration of the exam.
After the Exam
Post-exam checklist
- ✅ Wait for all students to submit (or force-submit any remaining)
- ✅ Restore internet connectivity on the exam machine
- ✅ Open
/exam-dashboard→ Sync Local → Cloud- This pushes: all exam attempts, student answers, evaluation queue, and any newly registered students
- ✅ Verify the sync report shows the expected numbers
- ✅ Log in to the normal admin panel (cloud) and evaluate/publish results
- ✅ After evaluation is done, wipe local data:
powershell
docker compose -f docker/exam-mode/docker-compose.yml down -v
What gets synced to cloud (Post-Exam Sync)
| Data | Synced? | Notes |
|---|---|---|
| Exam attempts | ✅ Yes | Only submitted / evaluated — not in_progress |
| Student answers | ✅ Yes | All answers for synced attempts |
| Manual evaluation queue | ✅ Yes | Subjective questions needing manual grading |
| New users (walk-ins / local sign-ups) | ✅ Yes | Created in cloud with exam password |
| Existing cloud users | ➡️ Skipped | Already exist — never overwritten or deleted |
Important: Post-Exam Sync never deletes any existing cloud user or data. It only creates missing users and upserts submission records.
Managing the Stack
powershell# ── Start / Stop ───────────────────────────────────────────────────── # Start everything (Docker + dev server) bun run exam:start # Stop everything (Docker containers + kill Next.js process) bun run exam:stop # Start Docker containers only (background, no Next.js) docker compose -f docker/exam-mode/docker-compose.yml up -d # Stop Docker containers (data is preserved in volumes) docker compose -f docker/exam-mode/docker-compose.yml down # ── Data Management ────────────────────────────────────────────────── # Wipe ALL local data — run ONLY after syncing to cloud docker compose -f docker/exam-mode/docker-compose.yml down -v # ── Logs & Status ──────────────────────────────────────────────────── # View Appwrite logs (live) docker logs appwrite -f # Check which containers are running docker ps # Check container health docker compose -f docker/exam-mode/docker-compose.yml ps # ── Database ───────────────────────────────────────────────────────── # Re-create collections after wiping (run after down -v) bun run setup:appwrite:local # ── Sync ───────────────────────────────────────────────────────────── # Pre-exam sync (Cloud → Local) — via dashboard UI or API # POST http://localhost:3000/api/exam-mode/sync-to-local # Post-exam sync (Local → Cloud) — via dashboard UI or API # POST http://localhost:3000/api/exam-mode/sync-to-cloud
Environment Variables
Required — Cloud
| Variable | Description |
|---|---|
NEXT_PUBLIC_APPWRITE_ENDPOINT | Cloud Appwrite endpoint (https://cloud.appwrite.io/v1) |
NEXT_PUBLIC_APPWRITE_PROJECT_ID | Cloud project ID |
NEXT_PUBLIC_APPWRITE_DATABASE_ID | Cloud database ID |
APPWRITE_API_KEY | Cloud admin API key (server-only) |
Required — Exam Mode
| Variable | Description |
|---|---|
EXAM_MODE | true to enable offline exam mode |
APPWRITE_LOCAL_ENDPOINT | Local Appwrite URL (http://localhost/v1) |
APPWRITE_LOCAL_PROJECT_ID | Local project ID |
APPWRITE_LOCAL_DATABASE_ID | Must be exam-db |
APPWRITE_LOCAL_API_KEY | Local admin API key |
EXAM_PASSWORD_PREFIX | The shared exam password all students use to log in |
Optional
| Variable | Description |
|---|---|
EXAM_ALLOWED_IPS | Comma-separated IPs allowed to access exam APIs (blank = allow all) |
Security Notes
- Change
_APP_OPENSSL_KEY_V1and_APP_EXECUTOR_SECRETindocker/exam-mode/appwrite.envto long random strings before any real use - The local Appwrite console (port 80) should only be accessible to the admin machine — block it from student devices using your router/firewall
EXAM_PASSWORD_PREFIXshould be kept secret and changed after each exam season- After syncing to cloud, run
docker compose down -vto wipe all local student data
Troubleshooting
Server won't stop with Ctrl+C
Use the dedicated stop command instead:
powershellbun run exam:stop
If that doesn't work, manually kill port 3000:
powershell# Find the process using port 3000 netstat -ano | findstr :3000 # Kill it (replace <PID> with the number from the above output) taskkill /F /PID <PID>
Students can't connect to the server
- Make sure the exam machine and student devices are on the same network (same Wi-Fi or LAN)
- Find your machine's IP with
ipconfig(look forIPv4 Address) - Check Windows Firewall isn't blocking port 3000:
powershell
# Allow port 3000 through firewall netsh advfirewall firewall add rule name="PlacePrep Exam" dir=in action=allow protocol=TCP localport=3000 - Make sure
bun run exam:startis running (not just Docker)
Pre-exam sync fails with authentication error
The local database is empty so there are no user accounts yet. Use bootstrap mode: the dashboard shows a Bootstrap Sync dialog asking for a local API key. Enter yourAPPWRITE_LOCAL_API_KEY value from .env and run the sync — this bypasses normal auth for the first sync.
"exam-db" database not found
The local database schema hasn't been created yet. Run:
Make surepowershellbun run setup:appwrite:local
APPWRITE_LOCAL_PROJECT_ID and APPWRITE_LOCAL_API_KEY are correct in .env first.
Docker containers keep restarting
Check logs for errors:
powershelldocker logs appwrite -f docker logs appwrite-mariadb -f
Common causes:
- Not enough disk space (
docker system dfto check) - Port 80 already in use by another app — stop IIS, Nginx, or XAMPP first
- Corrupted volume data — try
docker compose down -vand restart
Student signed up locally but can't log in after sync to cloud
Students who signed up during exam mode are synced to cloud with the exam password (yourEXAM_PASSWORD_PREFIX value). They should use that to log in to the cloud app. If they want to set a custom password, they can use the "Forgot Password" flow on the cloud app.
Post-sync shows +0 for users but a new student exists locally
Make sure you're running the Post-Exam Sync (Local → Cloud), not the Pre-Exam Sync. The post-sync is the one that pushes new users and attempts to cloud.
Found a problem with this page? Help us fix it.
Still stuck? Read the FAQ or the troubleshooting guide.