# 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.