← Home# Self-hosted deployment
TradePlan runs as a standard Next.js app on **your own server** — no Vercel or other platform lock-in.
## What you need
| Component | Self-hosted default | Optional cloud |
|-----------|---------------------|----------------|
| App | Node 20+ (`npm run build` + `npm run start`) or Docker | — |
| Database | SQLite file `file:./data/tradeplan.db` | [Turso](https://turso.tech) remote libSQL |
| Auth | [Clerk](https://clerk.com) (Google, email, etc.) | — |
| Images | `STORAGE_DRIVER=local` → `var/uploads/` | `STORAGE_DRIVER=r2` + `R2_*` → Cloudflare R2 |
| TLS | Caddy / nginx reverse proxy | — |
## 1. Environment
Copy `web/.env.example` to `web/.env.production` and fill in:
```bash
TRADEPLAN_ENV=production
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_...
CLERK_SECRET_KEY=sk_live_...
# SQLite on the server (simplest)
TURSO_DATABASE_URL=file:./data/tradeplan.db
# Local image uploads on disk (explicit; ignores R2 env if set)
STORAGE_DRIVER=local
```
For Turso instead of a local file, use `libsql://…` plus `TURSO_AUTH_TOKEN`. For R2, set `STORAGE_DRIVER=r2` and all `R2_*` variables. If `STORAGE_DRIVER` is unset, the app uses R2 when fully configured, otherwise local disk only in non-production (legacy).
## 2. Database migrations
From `web/` with production env loaded:
```bash
export $(grep -v '^#' .env.production | xargs)
mkdir -p data
npm run db:migrate
```
For a `file:` database, `TURSO_AUTH_TOKEN` is not required.
## 3. Run with Node
```bash
cd web
npm ci
npm run start:prod # migrate + build + start (uses .env.production)
```
Or step by step: `npm run db:migrate` → `npm run build` → `npm run start` (listens on `:3000`).
Use a process manager (systemd, PM2) so the app restarts on failure. Example systemd unit path: `docs/systemd/tradeplan.service.example`.
## 4. Run with Docker
```bash
cd web
cp .env.example .env.production
# edit .env.production — set Clerk keys, etc.
# migrate once on the host (uses same env + file DB path)
export $(grep -v '^#' .env.production | xargs)
mkdir -p data
npm run db:migrate
docker compose up -d --build
```
Volumes persist `data/` (SQLite) and `var/uploads/` (images).
Inside Docker, use `TURSO_DATABASE_URL=file:/app/data/tradeplan.db` (see `docker-compose.yml`).
## 5. Reverse proxy & HTTPS
Bind the app to localhost and terminate TLS at the proxy.
- **Caddy:** see `docs/Caddyfile.example`
- **nginx:** proxy `https://your-domain` → `http://127.0.0.1:3000`
Add your public `https://` URL in the **Clerk dashboard** under **Domains**.
## 6. Clerk authentication
1. Create a Clerk app and copy production API keys into `.env.production`.
2. Enable **Google** (or other providers) under **Social connections**.
3. Add your production domain in Clerk.
See `docs/clerk-auth.md` for full steps.
## 7. Updates
```bash
git pull
cd web
npm ci
npm run db:migrate
npm run build
# restart the process manager or docker compose
```
## Checklist
- [ ] `npm run check-env:prod` passes
- [ ] Clerk production domain matches your HTTPS URL
- [ ] Migrations applied (`npm run db:migrate`)
- [ ] Reverse proxy serves HTTPS
- [ ] Back up `data/tradeplan.db` and `var/uploads/` regularly