Turnpike/docs/INSTALLATION.md

197 lines
7.4 KiB
Markdown
Raw Permalink Normal View History

# Turnpike Installation Guide
This guide covers building, deploying, and operating Turnpike. For usage and event workflows, see [USAGE.md](USAGE.md).
## Requirements
- **Go 1.24+** (for building)
- **Node.js 18+** (for building the frontend)
- No external database — Turnpike uses embedded SQLite via a pure-Go driver
## Building from Source
```sh
git clone <repo-url>
cd turnpike
make build
```
This runs `npm ci && npm run build` in `frontend/`, then `CGO_ENABLED=0 go build -o turnpike .`. The result is a single static binary with the frontend assets embedded. No runtime dependencies.
Other make targets: `make dev` (prints two-terminal dev instructions), `make clean` (removes binary and build artifacts).
## Configuration Reference
| Flag | Env var | Default | Description |
|------|---------|---------|-------------|
| `--addr` | — | `0.0.0.0:8180` | Listen address |
| `--db` | — | `turnpike.db` | SQLite database path |
| `--secret` | `TURNPIKE_SECRET` | auto-generated | JWT signing secret |
| `--token-expiry` | — | `24` | JWT lifetime in hours |
| `--base-url` | — | — | Public URL for volunteer token links |
| `--smtp-host` | — | — | SMTP server hostname |
| `--smtp-port` | — | `587` | SMTP port (587 = STARTTLS, 465 = implicit TLS) |
| `--smtp-user` | — | — | SMTP username |
| `--smtp-password` | — | — | SMTP password |
| `--smtp-from` | — | — | Sender email address |
| `--smtp-from-name` | — | — | Sender display name |
| — | `TURNPIKE_ADMIN_USER` | — | Bootstrap admin username (first run only) |
| — | `TURNPIKE_ADMIN_PASSWORD` | — | Bootstrap admin password (first run only) |
SMTP settings can also be configured at runtime through the Settings page (admin only). CLI flags override stored database values.
## Running
```sh
# Minimal startup (creates admin user on first run)
TURNPIKE_ADMIN_USER=admin TURNPIKE_ADMIN_PASSWORD=changeme ./turnpike
# With explicit options
./turnpike \
--addr 0.0.0.0:8180 \
--db /var/lib/turnpike/turnpike.db \
--secret your-jwt-secret \
--base-url https://turnpike.example.com
```
The admin account is only created on first startup when no users exist and both `TURNPIKE_ADMIN_USER` and `TURNPIKE_ADMIN_PASSWORD` are set. After the first user is created, these environment variables have no effect.
### JWT Secret
If `--secret` / `TURNPIKE_SECRET` is not provided, Turnpike auto-generates a secret and stores it in the database. This persists across restarts as long as the database file is preserved. To use an explicit secret (recommended for production), pass it via flag or env var.
Changing or losing the JWT secret invalidates all active sessions — users will need to log in again.
## Systemd
```ini
[Unit]
Description=Turnpike event management
After=network.target
[Service]
ExecStart=/usr/local/bin/turnpike --db /var/lib/turnpike/turnpike.db --base-url https://turnpike.example.com
Environment=TURNPIKE_SECRET=your-secret-here
EnvironmentFile=/etc/turnpike/admin.env
StateDirectory=turnpike
DynamicUser=true
Restart=on-failure
[Install]
WantedBy=multi-user.target
```
The `admin.env` file should contain:
```
TURNPIKE_ADMIN_USER=admin
TURNPIKE_ADMIN_PASSWORD=your-password
```
Note: systemd uses `EnvironmentFile` (singular). The plural `EnvironmentFiles` is silently ignored.
## Docker
A multi-stage `Dockerfile` is included in the repo (node build, Go build, scratch final image):
```sh
docker build -t turnpike .
docker run -p 8180:8180 \
-v turnpike-data:/data \
-e TURNPIKE_ADMIN_USER=admin \
-e TURNPIKE_ADMIN_PASSWORD=changeme \
-e TURNPIKE_SECRET=your-secret \
turnpike --db /data/turnpike.db
```
## NixOS
2026-03-04 23:02:35 -06:00
Turnpike builds with `buildGoModule` + `buildNpmPackage` (pure-Go SQLite, no CGO). The frontend is built separately and copied into the Go build:
```nix
2026-03-04 23:02:35 -06:00
frontendDist = pkgs.buildNpmPackage {
pname = "turnpike-frontend";
src = "${src}/frontend";
npmDepsHash = "sha256-...";
buildPhase = "npm run build";
installPhase = "cp -r dist $out";
};
turnpike = pkgs.buildGoModule {
pname = "turnpike";
2026-03-04 23:02:35 -06:00
src = fetchgit { url = "..."; rev = "v2.0.0"; hash = "sha256-..."; };
vendorHash = "sha256-...";
env.CGO_ENABLED = 0;
2026-03-04 23:02:35 -06:00
preBuild = "cp -r ${frontendDist} frontend/dist";
};
```
2026-03-04 23:02:35 -06:00
A complete NixOS module with `DynamicUser`, `StateDirectory`, and secrets is in the project's `homelab/turnpike.nix`.
## Reverse Proxy
Turnpike serves both the API and frontend on a single port. Behind a reverse proxy, ensure SSE connections for real-time sync are not buffered.
### nginx
```nginx
server {
listen 443 ssl;
server_name turnpike.example.com;
location / {
proxy_pass http://127.0.0.1:8180;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api/sync/stream {
proxy_pass http://127.0.0.1:8180;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
}
}
```
The `/api/sync/stream` endpoint uses Server-Sent Events (SSE). The key settings are `proxy_buffering off` and a long `proxy_read_timeout` to keep the connection alive.
## TLS / HTTPS
Use a TLS termination proxy (nginx, Caddy, Traefik) with Let's Encrypt or your own certificates. Turnpike itself serves plain HTTP.
Set `--base-url https://turnpike.example.com` so that volunteer token links in exported CSVs and emails use the correct URL.
## Backup
Turnpike stores all data in a single SQLite file (the path passed to `--db`).
**Simple backup:** copy the `.db` file while the server is running — SQLite WAL mode ensures a consistent snapshot.
**Continuous replication:** [Litestream](https://litestream.io) streams WAL changes to S3, GCS, or Azure Blob Storage with near-zero overhead.
## Upgrading
1. Pull the latest source
2. Rebuild: `make build`
3. Replace the binary and restart the service
Database migrations run automatically on startup. All migrations are additive (`ALTER TABLE ... ADD COLUMN`) — no data is lost.
## Troubleshooting
**Login fails after restart:** If the JWT secret wasn't persisted (`--secret` or `TURNPIKE_SECRET`), a new secret is generated on restart, invalidating all existing tokens. Users need to log in again. To prevent this, always set an explicit secret in production.
**Admin account not created:** The bootstrap admin is only created on first startup when no users exist. If the service started without `TURNPIKE_ADMIN_USER` / `TURNPIKE_ADMIN_PASSWORD`, users were never created. Delete the database file and restart with the env vars set.
**Soft deletes:** Records are marked with `deleted_at`, not removed from the database. This is required for sync — clients need to see deletions to purge their local IndexedDB. Don't hard-delete records directly from the SQLite database.
**`EnvironmentFile` vs `EnvironmentFiles`:** systemd's directive is `EnvironmentFile` (singular). Using the plural form silently ignores the file — the service starts but environment variables are never set.
**SSE disconnections:** If real-time updates stop working behind a proxy, check that `proxy_buffering off` is set for `/api/sync/stream`. The client reconnects automatically, but buffering proxies can prevent events from reaching the browser.