Files
trip-plan/README.md
T
grabowski 1d86c68665 Fix bind-mount data dir permissions via entrypoint
Docker creates ./data root-owned on the host, but the app runs as the unprivileged node user, causing SQLITE_CANTOPEN on first deploy. New entrypoint starts as root, chowns DATA_DIR, then drops privileges with setpriv. Compose keeps the SQLite database in ./data next to the compose file. Also trims scripts/ and playwright artifacts from the image.
2026-07-18 23:54:50 +07:00

121 lines
5.2 KiB
Markdown

# Trip Plan
A self-hosted, multi-user web tool for collaboratively planning trips. Multiple people log in to the same instance, create trips with a name and date range, and fill in a day-by-day calendar together — flights (with multi-leg connections), hotels, border crossings, drives, rental cars, and activities. Each trip is drawn on an interactive map with the route between stops, per-leg distances, cost splitting between members, and an overall summary. It runs as a single Docker container with a SQLite database.
## Features
- **Accounts without passwords** — Mullvad-style: one click generates a random account number (shown once, stored only as a hash). No email, no password, no personal data.
- **Sharing via join codes** — every trip has a short code; anyone with an account enters it to become a co-editor. The owner can regenerate the code to stop further joins.
- **Day-by-day calendar** — a real calendar grid; click a day to add any number of entries (activity, hotel, travel, flight, rental car, immigration, note).
- **Multi-leg flights** — type `CNX-BKK-DXB-FRA` and it expands into segments with per-leg flight number and times; every airport becomes a stop on the map (bundled offline IATA airport database).
- **Rental cars** — brand/model/type, booking ref, pickup/dropoff with dates and times, and included km — compared against the trip's estimated driven km.
- **Locations** — geocoded via OpenStreetMap (Nominatim, English results); lat/lng stored per entry.
- **Map** — Leaflet + OpenStreetMap; stops plotted in order, air legs dashed / ground legs solid, per-leg great-circle km.
- **Costs & splitting** — price per entry with "split equally", "everyone pays their own", or "payer's expense"; per-person share/paid/net table and minimal settle-up transfers, in the trip's currency.
- **Summary** — days/nights, entry counts, total km, **≈ km driven** (rental-car figure) vs km flown, and locations visited.
## Deployment (Docker + external Caddy for TLS)
The app serves plain HTTP on port 3000 and is designed to sit behind a reverse
proxy that terminates TLS — e.g. an external [Caddy](https://caddyserver.com/).
### 1. Run the container
Using the prebuilt image from the registry:
```yaml
# docker-compose.yml
services:
tripplan:
image: git.b4l.co.th/grabowski/trip-plan:latest
restart: unless-stopped
environment:
SESSION_SECRET: "<long random string — e.g. openssl rand -hex 32>"
volumes:
- ./data:/app/data # database lives next to this compose file
# No public port mapping needed when Caddy shares a Docker network with
# the app (recommended). For a host-level Caddy, map localhost only:
ports:
- "127.0.0.1:3000:3000"
```
The entrypoint chowns `./data` to the app's unprivileged `node` user on
startup, so a root-created bind-mount directory works out of the box. (Images
older than this note need a one-time `sudo chown -R 1000:1000 ./data`.)
```bash
docker compose up -d
```
(Or build locally instead of pulling: clone this repo and use `build: .` — the
checked-in `docker-compose.yml` does exactly that.)
### 2. Point Caddy at it
If Caddy runs on the same host (host-level Caddy, app bound to `127.0.0.1:3000`):
```caddyfile
trips.example.com {
reverse_proxy 127.0.0.1:3000
}
```
If Caddy runs as a container, attach both services to a shared Docker network
and proxy by service name instead:
```caddyfile
trips.example.com {
reverse_proxy tripplan:3000
}
```
That's all — Caddy obtains and renews the certificate automatically; the app
needs no TLS configuration of its own. Session cookies are `SameSite=Lax` and
`httpOnly`, and work as-is behind the proxy.
### 3. Updating
```bash
docker compose pull && docker compose up -d
```
Schema migrations run automatically at startup; existing data in `./data` is
preserved (back up that directory to back up all trips).
## Quick start (local, no proxy)
```bash
docker compose up -d # builds the image from source
# open http://localhost:3000 and create the first account
```
> Before exposing the app beyond localhost, set your own random `SESSION_SECRET`
> in `docker-compose.yml`.
## Local development
```bash
npm install
npm start # http://localhost:3000
npm test # API tests (node:test + supertest)
```
`npm run dev` starts the server with `--watch` for auto-reload.
## Environment variables
| Variable | Default | Description |
|---|---|---|
| `PORT` | `3000` | HTTP port the server listens on. |
| `DATA_DIR` | `./data` (`/app/data` in Docker) | Directory holding the SQLite database file. |
| `SESSION_SECRET` | dev value (warns) | Secret used to sign session cookies. **Set this in production.** |
## Data
All state lives in a single SQLite file under `DATA_DIR`. In Docker this is `/app/data`, bind-mounted from `./data` next to the compose file — back up that directory to back up all trips. No external database or services are required (geocoding calls OpenStreetMap's Nominatim, which needs no API key; airport lookups use a bundled offline dataset).
## Documentation
- [docs/PROJECT_OVERVIEW.md](docs/PROJECT_OVERVIEW.md) — architecture, data model, and design decisions.
- [docs/API.md](docs/API.md) — the REST API contract.