# Production Docker Compose (/api Path, Single Image) Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Produce a production docker-compose configuration that builds a single immutable image where the backend serves the built frontend and public API traffic is routed via `/api` on the single domain.

**Architecture:** Single container built from the multi-stage Dockerfile; backend serves `hls-player/dist` and listens on `0.0.0.0:2738`, published to `0.0.0.0:${BACKEND_PORT:-2738}` for compatibility across hosts. Build-time `VITE_BACKEND_URL` is set to `https://datamedia.chinhphu.vn/api` so the frontend targets the public `/api` path. Runtime uses `FRONTEND_PUBLIC_URL` for CORS and `BACKEND_PUBLIC_URL` for client configuration.

**Tech Stack:** Docker, Docker Compose, Node.js (Express), Vite, external Caddy reverse proxy.

**Success Criteria:**
- Backend health check returns OK via direct port (`http://<host>:2738/health`).
- Frontend loads from the backend-served `hls-player/dist` via Caddy domain root (`https://datamedia.chinhphu.vn/`).
- `/api/*` requests route through Caddy and hit the backend (e.g., `https://datamedia.chinhphu.vn/api/health`).
- Single container only; no separate frontend service.

**Runtime Environment Source:**
- Primary: `env_file: ./hls-player/.env` for backend/runtime settings.
- Overrides: only non-secret overrides in `docker-compose.yml` if needed.
- Defaults: backend falls back to built-in defaults when envs are omitted.

**Caddy Deployment Note:**
- Caddy runs externally on the host (not in Compose). It terminates TLS for `datamedia.chinhphu.vn` and proxies `/api/*` + `/` to the backend container.

---

### Task 1: Wire build-time VITE_BACKEND_URL into Dockerfile

**Files:**
- Modify: `Dockerfile`

**Step 1: Write the failing test**

```bash
# Define a validation command that should fail before the Dockerfile accepts VITE_BACKEND_URL.
# (This expects the built assets NOT to contain the /api URL yet.)
docker build --build-arg VITE_BACKEND_URL="https://datamedia.chinhphu.vn/api" -t hls-app:test .
docker run --rm hls-app:test sh -c "rg 'datamedia.chinhphu.vn/api' /app/hls-player/dist"
```

**Step 2: Run test to verify it fails**

Run the two commands above.
Expected: `rg` returns no matches (non-zero exit).

**Step 3: Write minimal implementation**

```dockerfile
FROM node:20-slim AS frontend-build
WORKDIR /app

# Required: build-time injection for frontend
ARG VITE_BACKEND_URL
ENV VITE_BACKEND_URL=${VITE_BACKEND_URL}

COPY hls-player/package*.json hls-player/
RUN cd hls-player && npm ci

COPY hls-player hls-player
RUN cd hls-player && npm run build
```

**Note:** A simpler functional-only verification approach (build + health check) was considered for speed, but the plan intentionally keeps the `rg`-based validation for thoroughness.

**Step 4: Run test to verify it passes**

```bash
docker build --build-arg VITE_BACKEND_URL="https://datamedia.chinhphu.vn/api" -t hls-app:test .
docker run --rm hls-app:test sh -c "rg 'datamedia.chinhphu.vn/api' /app/hls-player/dist"
```

Expected: `rg` finds matches in built assets.

**Step 5: Commit**

```bash
git add Dockerfile
git commit -m "build: pass VITE_BACKEND_URL into frontend build"
```

---

### Task 2: Replace docker-compose.yml with single immutable app service

**Files:**
- Modify: `docker-compose.yml`

**Step 1: Write the failing test**

```bash
# Baseline check: current compose should still include the old frontend service.
docker compose config | rg "^  frontend:"
```

**Step 2: Run test to verify it fails**

Run the command above.
Expected: It matches (non-zero exit if it does not).

**Step 3: Write minimal implementation**

- Remove the dedicated frontend service.
- Use a single service built from `Dockerfile`.
- Publish port `2738` to `0.0.0.0` for cross-host compatibility.
- Pass build arg `VITE_BACKEND_URL=https://datamedia.chinhphu.vn/api`.
- Keep `env_file` for secrets; only add non-secret overrides if needed.
- Use named volumes for `/app/backend/data` and `/app/backend/uploads` (final decision).
- Considered alternative: current host bind mounts (`./media:/media:ro`, `app-data:/data`) were rejected in favor of consistency with this plan.

**Step 4: Run test to verify it passes**

```bash
# After changes, the frontend service should be gone.
docker compose config | rg "^  frontend:"
```

Expected: No matches (command exits non-zero).

**Step 5: Commit**

```bash
git add docker-compose.yml
git commit -m "chore: single-image production compose for /api routing"
```

---

### Task 3: Provide Caddy reverse proxy snippet (external) + add Caddyfile

**Files:**
- Create: `Caddyfile`
- Also deliver snippet in implementation report

**Step 1: Draft Caddyfile snippet + file**

```caddyfile
handle_path /api/* {
  reverse_proxy 192.168.5.96:2738
}
reverse_proxy 192.168.5.96:2738
```

Create `Caddyfile` in the repo root with the same content.

**Step 2: Validate formatting (optional)**

```bash
caddy fmt --overwrite Caddyfile
```

Expected: No errors.

**Step 3: Commit**

N/A (no repo file changes).
