197 lines
5.4 KiB
Markdown
197 lines
5.4 KiB
Markdown
# Watch Party (Frontend + Backend)
|
||
|
||
Watch Party is a small full-stack app:
|
||
|
||
- **Frontend:** Vite + React + TypeScript, served by **Nginx**
|
||
- **Backend:** Go (Gin) API + PostgreSQL
|
||
- **Orchestration:** One **docker-compose** that keeps FE/BE split on the same internal network
|
||
- **Routing:** Your Debian Nginx is the public entrypoint and reverse-proxies to the Mac mini (where this stack runs)
|
||
|
||
```
|
||
router → Debian nginx (TLS) → Mac mini :3000 → [web nginx ↔ api ↔ db]
|
||
```
|
||
|
||
---
|
||
|
||
## Repository Layout
|
||
|
||
```
|
||
.
|
||
├── docker-compose.yml # single compose for web + api + db + migrate
|
||
├── .env # stack configuration (see template below)
|
||
├── backend/ # Go API + migrations
|
||
│ ├── cmd/
|
||
│ │ ├── migrate/ # one-off migration binary
|
||
│ │ └── server/ # API server (Gin)
|
||
│ ├── db/migration/ # SQL migrations (0001_init.sql, etc.)
|
||
│ ├── Dockerfile # builds server + migrate
|
||
│ └── go.mod / go.sum
|
||
└── frontend/ # Vite + React + TS
|
||
├── Dockerfile # Vite build → Nginx runtime (envsubst for proxy)
|
||
├── ops/ # container runtime bits
|
||
│ ├── entrypoint.sh
|
||
│ ├── nginx.conf # (optional/manual)
|
||
│ └── nginx.conf.template # uses BACKEND_ORIGIN
|
||
├── src/, public/, vite.config.ts, package*.json, tsconfig*.json
|
||
```
|
||
|
||
---
|
||
|
||
## Services (Compose)
|
||
|
||
- **web**: Nginx serving the built SPA, and proxying `/api/*` to `api:8082` (Docker DNS)
|
||
- **api**: Go server (Gin), listens on `:8082`, health at `/healthz`
|
||
- **migrate**: one-off job that runs the `migrate` binary, applies SQL from `backend/db/migration`
|
||
- **db**: PostgreSQL 16 (internal only)
|
||
|
||
Only **web** is published to your LAN (port `3000` by default). `api` and `db` are internal to the compose network.
|
||
|
||
---
|
||
|
||
## .env (template)
|
||
|
||
```dotenv
|
||
##################
|
||
## Frontend env ##
|
||
##################
|
||
WEB_PORT=3000
|
||
PUBLIC_BASE_PATH=/watch-party/
|
||
BACKEND_ORIGIN=http://api:8082
|
||
|
||
#################
|
||
## Backend env ##
|
||
#################
|
||
# Postgres (container)
|
||
POSTGRES_DB=watchparty
|
||
POSTGRES_USER=admin
|
||
POSTGRES_PASSWORD=change-me
|
||
TZ=Asia/Tokyo
|
||
COMPOSE_PLATFORM=linux/arm64/v8
|
||
|
||
# Backend server
|
||
ADDR=:8082
|
||
GIN_MODE=release
|
||
PGHOST=db
|
||
POSTGRES_PORT=5432
|
||
PGSSLMODE=disable
|
||
```
|
||
|
||
> **Why mixed names?** The current Go code builds its DSN from `PGHOST`, `POSTGRES_PORT`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`, and `PGSSLMODE`.
|
||
|
||
---
|
||
|
||
## Run (Production-like)
|
||
|
||
```bash
|
||
docker compose build
|
||
docker compose up -d
|
||
```
|
||
|
||
Open: `http://<mac-mini-LAN-IP>:3000/watch-party/`
|
||
|
||
Health checks:
|
||
- Web (nginx): `curl http://<mac-mini-LAN-IP>:3000/`
|
||
- API through web proxy: `curl http://<mac-mini-LAN-IP>:3000/api/healthz`
|
||
- API direct (inside network only): `curl http://api:8082/healthz` (from another container)
|
||
|
||
Logs:
|
||
```bash
|
||
docker compose logs -f migrate # should start, apply SQL, and exit 0
|
||
docker compose logs -f api
|
||
docker compose logs -f web
|
||
docker compose logs -f db
|
||
```
|
||
|
||
---
|
||
|
||
## Debian Nginx (public reverse proxy)
|
||
|
||
Terminate TLS on Debian and forward to the Mac mini:
|
||
|
||
```nginx
|
||
# Serve the SPA under /watch-party/
|
||
location /watch-party/ {
|
||
proxy_pass http://<mac-mini-LAN-IP>:3000/watch-party/;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Forwarded-For $remote_addr;
|
||
}
|
||
|
||
# Proxy API via the web container (keeps same-origin from browser POV)
|
||
location /api/ {
|
||
proxy_pass http://<mac-mini-LAN-IP>:3000/api/;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Forwarded-For $remote_addr;
|
||
}
|
||
```
|
||
|
||
Security headers & HSTS on Debian are recommended.
|
||
|
||
---
|
||
|
||
## Local Development (hot reload)
|
||
|
||
You can dev each side outside Docker:
|
||
|
||
- **Frontend**
|
||
```bash
|
||
cd frontend
|
||
npm ci
|
||
npm run dev # http://localhost:5173
|
||
```
|
||
Configure your FE to call the API (e.g., via a local `.env` or vite config).
|
||
|
||
- **Backend**
|
||
```bash
|
||
cd backend
|
||
go run ./cmd/server
|
||
```
|
||
Make sure Postgres is reachable (either from the compose `db` or your local).
|
||
|
||
For a dev-only compose with Vite HMR, create an override (not included here).
|
||
|
||
---
|
||
|
||
## Notes & Conventions
|
||
|
||
- **Only `web` is exposed** to the LAN. `api` and `db` are `expose`d internally.
|
||
- The frontend **does not** use `host.docker.internal`; it proxies to `api:8082` by Docker DNS.
|
||
- Backend healthcheck probes `http://localhost:8082/healthz` and the server binds `ADDR=:8082`.
|
||
- Migrations live in `backend/db/migration/` and are applied by the **migrate** container at startup.
|
||
- Vite base path is controlled by `PUBLIC_BASE_PATH` (e.g., `/watch-party/`).
|
||
|
||
---
|
||
|
||
## Quick Test Endpoints
|
||
|
||
```bash
|
||
# Latest health:
|
||
curl http://<mac-mini-LAN-IP>:3000/api/healthz
|
||
|
||
# Current show (if table seeded and current_ep=true):
|
||
curl http://<mac-mini-LAN-IP>:3000/api/current
|
||
```
|
||
|
||
---
|
||
|
||
## Troubleshooting
|
||
|
||
- **Migrate “hangs” / API starts instead**
|
||
Ensure the backend image uses `CMD` (not `ENTRYPOINT`) and compose sets:
|
||
```yaml
|
||
migrate:
|
||
command: ["/app/migrate"]
|
||
api:
|
||
command: ["/app/server"]
|
||
```
|
||
- **404 for deep links** under `/watch-party/`
|
||
Confirm SPA fallback is active in `frontend/ops/nginx.conf.template` and Vite `base` matches `PUBLIC_BASE_PATH`.
|
||
- **DB not reachable**
|
||
Check `.env` values match the mixed DSN env names. `PGHOST=db`, `POSTGRES_PORT=5432`, etc.
|
||
- **CORS issues**
|
||
There shouldn’t be any—browser hits `web`, which proxies to `api`, so it’s same-origin.
|
||
|
||
---
|
||
|
||
## License
|
||
|
||
MIT |