forgejo-tickets/docs/deployment.md

127 lines
4.1 KiB
Markdown

# Deployment
## Local Development
1. Copy the example environment file and edit it:
```bash
cp .env.example .env
```
2. Start PostgreSQL (e.g. via Docker):
```bash
docker run -d --name postgres \
-e POSTGRES_USER=user -e POSTGRES_PASSWORD=password -e POSTGRES_DB=forgejo_tickets \
-p 5432:5432 postgres:16-alpine
```
3. Run the application:
```bash
make run
```
This runs `go run ./cmd/server`, which auto-migrates the database on startup.
4. For CSS development, run the Tailwind watcher in a separate terminal:
```bash
make tailwind-watch
```
The public UI is at `http://localhost:8080` and the admin panel at `http://localhost:8081`.
## Makefile Targets
| Target | Description |
|--------|-------------|
| `build` | Compiles Tailwind CSS then builds the Go binary (`forgejo-tickets`) |
| `run` | Runs the server directly with `go run` (no prior build needed) |
| `test` | Runs all Go tests (`go test ./...`) |
| `tailwind` | Compiles Tailwind CSS from `web/static/css/input.css` to `web/static/css/output.css` (minified) |
| `tailwind-watch` | Runs Tailwind in watch mode for development |
| `docker` | Builds the Docker image tagged with the git SHA and `latest` |
| `docker-push` | Builds and pushes the Docker image to the registry |
| `clean` | Removes the compiled binary and generated CSS |
## Docker
The Dockerfile uses a 3-stage build:
1. **Node/Tailwind stage** (`node:22-alpine`) — Compiles Tailwind CSS.
2. **Go build stage** (`golang:1.23-alpine`) — Downloads dependencies and compiles the Go binary with `CGO_ENABLED=0`.
3. **Runtime stage** (`alpine:3.20`) — Minimal image with just the binary, templates, and static assets. Exposes ports 8080 and 8081.
Build and run:
```bash
make docker
docker run --env-file .env -p 8080:8080 -p 8081:8081 registry.ts.mattnite.net/forgejo-tickets:latest
```
## Nomad
The production deployment uses Nomad with the job spec at `infra/tickets/tickets.hcl`.
### Job Structure
- **Job**: `tickets` (service type, datacenter `dc1`)
- **Group**: `tickets` (count 1)
- Two ports on the Tailscale host network: `http` (public) and `admin`
- **Task**: `server` (Docker driver)
### Environment Injection
Secrets are stored as Nomad variables at the path `nomad/jobs/tickets` and injected via a template block:
- `DATABASE_URL`, `SESSION_SECRET`
- `FORGEJO_URL`, `FORGEJO_API_TOKEN`
- `POSTMARK_SERVER_TOKEN`
- `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`
- `MICROSOFT_CLIENT_ID`, `MICROSOFT_CLIENT_SECRET`
- `APPLE_CLIENT_ID`, `APPLE_TEAM_ID`, `APPLE_KEY_ID`
Non-secret configuration is set directly in the `env` block:
- `PUBLIC_ADDR` and `ADMIN_ADDR` use Nomad's dynamic port variables
- `BASE_URL` is the public-facing URL
- `POSTMARK_FROM_EMAIL` is the sender address
- `TAILSCALE_ALLOWED_USERS` controls admin access
### Traefik Routing
Two Nomad services are registered with Traefik tags:
| Service | Host | Purpose |
|---------|------|---------|
| `tickets` | `tickets.ts.mattnite.net` | Public UI (internal Traefik, TLS via Let's Encrypt) |
| `tickets-admin` | `tickets-admin.ts.mattnite.net` | Admin panel (internal Traefik, TLS via Let's Encrypt) |
Both use the `websecure` entrypoint with automatic TLS certificate resolution.
### Health Checks
The public service registers an HTTP health check at `GET /health` with a 10-second interval and 30-second timeout.
### Logging
Docker logging is configured to ship to Loki at `loki.ts.mattnite.net` with job name and allocation ID as labels.
### Resources
- CPU: 200 MHz
- Memory: 256 MB
## Database
The application uses PostgreSQL with GORM as the ORM. On startup, `database.RunMigrations()` calls `models.AutoMigrate()` which:
1. Creates PostgreSQL enum types (`ticket_status`, `token_type`) if they don't exist
2. Auto-migrates all model tables (User, OAuthAccount, Session, Repo, Ticket, TicketComment, EmailToken)
3. Creates composite and partial unique indexes
No separate migration tool is needed — the schema is managed entirely through GORM auto-migration.
See [Configuration](./configuration.md) for all environment variables and [Forgejo Integration](./forgejo-integration.md) for webhook URL setup.