# Docker Watcher — Implementation Plan

## Overview

A self-hosted tool that automates Docker container deployment with Nginx Proxy Manager integration. Detects new images from Gitea/GitHub registries, deploys containers, and configures reverse proxy routing — all from a web dashboard. Supports multiple simultaneous versions of the same project. DNS is handled by a Cloudflare wildcard record (`*.dolgolyov-family.by`) — no per-project DNS management needed.

## Architecture

```text
Gitea CI → pushes image → Registry
     │                        ↓
     │              Docker Watcher (Go)
     │              ├── Secret webhook URL (instant)
     │              └── Registry poller (fallback)
     │                        ↓
     └── or: POST /api/webhook/<secret-uuid>
              with {"image": "registry/org/app:tag"}
                              ↓
                  Known project? ──────────────────┐
                  ↓ yes                             ↓ no
           Match tag → stage              Auto-create project
                  ↓                       with defaults from
           auto_deploy?                   image inspection
           ↓ yes       ↓ no              (EXPOSE, labels)
     Deploy now    Notify, wait                ↓
           ↓       for UI trigger         Deploy with defaults
           ↓           ↓
     Pull image
     Start new container on shared network
     (old container stays if multi-instance)
           ↓
     NPM API: create proxy host (if first deploy for this subdomain)
     (DNS already handled by Cloudflare wildcard *.domain)
           ↓
     Health check
     → success: done, notify
     → failure: remove new container, alert
```

## Decisions

| Decision | Choice | Rationale |
|----------|--------|-----------|
| Language | Go | Single binary, excellent Docker SDK, low resource usage |
| Web UI | SvelteKit (embedded in Go binary) | User's existing stack, lightweight |
| Reverse proxy | Nginx Proxy Manager | Already deployed, API available |
| DNS | Cloudflare wildcard `*.{domain}` | One-time setup, all subdomains auto-resolve |
| Routing | Subdomain-based | No sub-path issues with SPAs |
| Image detection | Secret webhook URL + polling | Webhook for speed, polling as fallback |
| Config storage | SQLite (YAML for initial seed only) | Editable via UI, no manual file editing |
| Credentials | Encrypted in SQLite (AES-256) | Single ENCRYPTION_KEY env var |
| Webhook auth | Secret UUID in URL | No tokens needed, simple CI integration |
| Multi-instance | Yes | Multiple tags of same project can run simultaneously |
| Deployment target | Same TrueNAS host | Docker socket mounted |

## Subdomain Convention

| Type | Pattern | Example |
|------|---------|---------|
| Dev (default) | `stage-dev-{project}.{domain}` | `stage-dev-web-app-launcher.dolgolyov-family.by` |
| Dev (specific tag) | `stage-dev-{project}-{tag}.{domain}` | `stage-dev-web-app-launcher-abc123.dolgolyov-family.by` |
| Release (default) | `stage-rel-{project}.{domain}` | `stage-rel-web-app-launcher.dolgolyov-family.by` |
| Release (specific tag) | `stage-rel-{project}-{tag}.{domain}` | `stage-rel-web-app-launcher-v1-2-0.dolgolyov-family.by` |
| Production | `{custom}.{domain}` | `launcher.dolgolyov-family.by` |

Tags are sanitized for DNS: dots → dashes, lowercase, truncated to fit DNS limits.

## Configuration

### First Launch

```text
YAML seed file exists? → import into SQLite → done
No YAML?               → empty state, configure everything via UI
```

After import, all configuration lives in SQLite and is managed via the Web UI.
YAML is never read again unless user clicks "Re-import config" or "Export config".

### Seed Config Format (optional)

```yaml
global:
  domain: dolgolyov-family.by
  server_ip: 93.84.96.191
  network: staging-net
  subdomain_pattern: "stage-{stage}-{project}"
  notification_url: https://notify.dolgolyov-family.by/webhook
  npm:
    url: http://npm:81
    email: docker-watcher@dolgolyov-family.by
    password: "npm-password-here"
registries:
  gitea:
    url: https://git.dolgolyov-family.by
    type: gitea
    token: "gitea-token-here"

projects:
  web-app-launcher:
    registry: gitea
    image: git.dolgolyov-family.by/alexei/web-app-launcher
    port: 3000
    healthcheck: /api/health
    env:
      NODE_ENV: production
    stages:
      dev:
        tag_pattern: "dev-*"
        auto_deploy: true
        max_instances: 5
      rel:
        tag_pattern: "v*"
        auto_deploy: false
        max_instances: 2
      prod:
        tag_pattern: "v*"
        auto_deploy: false
        confirm: true
        promote_from: rel
        max_instances: 2
        subdomain: launcher
```

## Web UI Sections

### Dashboard

Overview of all projects with their running instances:
- Project name, running instance count, latest activity
- Quick status indicators (healthy / stopped / failing)
- "Quick Deploy" button for ad-hoc image deployment

### Project Detail

Per-project view with stages and instances:
- Each stage shows all running instances with: tag, status, URL, uptime
- Controls per instance: Stop, Start, Restart, Remove
- "Deploy new version" dropdown — lists available tags from registry
- Deploy history log

### Quick Deploy

For deploying images not yet configured as projects:
1. Paste image URL (e.g., `git.dolgolyov-family.by/alexei/my-app:dev-abc123`)
2. Docker Watcher pulls and inspects image (EXPOSE port, HEALTHCHECK, labels)
3. Pre-fills form with sensible defaults (project name, port, stage, subdomain)
4. User reviews, tweaks, clicks "Deploy"
5. Project is auto-created in the DB for future use

### Settings

- **Registries** — add/edit/delete registries, test connection
- **Credentials** — NPM, registry tokens (encrypted, shown as `••••••••`)
- **Global** — domain, server IP, Docker network, subdomain pattern, polling interval
- **Notifications** — webhook URL
- **Webhook URL** — shows the secret deploy URL, "Regenerate" button

### Projects Config

- Add / edit / delete projects via UI
- Configure image, port, healthcheck, env vars, volumes per project
- Add / remove stages, set tag patterns, auto-deploy, subdomain overrides, max instances

## Project Structure

```text
docker-watcher/
├── cmd/
│   └── server/
│       └── main.go                 # Entry point
├── internal/
│   ├── config/
│   │   ├── config.go               # YAML seed parsing
│   │   └── config_test.go
│   ├── docker/
│   │   ├── client.go               # Docker Engine API wrapper
│   │   ├── container.go            # Create, start, stop, remove, inspect
│   │   └── client_test.go
│   ├── npm/
│   │   ├── client.go               # NPM API client (auth, CRUD proxy hosts)
│   │   └── client_test.go
│   ├── registry/
│   │   ├── registry.go             # Interface
│   │   ├── gitea.go                # Gitea registry implementation
│   │   ├── github.go               # GitHub Container Registry (future)
│   │   ├── poller.go               # Periodic tag polling
│   │   └── registry_test.go
│   ├── deployer/
│   │   ├── deployer.go             # Orchestrates full deploy flow
│   │   ├── rollback.go             # Rollback on failure
│   │   └── deployer_test.go
│   ├── health/
│   │   ├── checker.go              # HTTP health checks with retries
│   │   └── checker_test.go
│   ├── notify/
│   │   ├── notifier.go             # Webhook notifications
│   │   └── notifier_test.go
│   ├── webhook/
│   │   ├── handler.go              # Secret URL webhook receiver
│   │   └── handler_test.go
│   ├── api/
│   │   ├── router.go               # HTTP API for web UI
│   │   ├── projects.go             # Project CRUD endpoints
│   │   ├── registries.go           # Registry CRUD endpoints
│   │   ├── settings.go             # Global settings endpoints
│   │   ├── instances.go            # Instance start/stop/restart/remove
│   │   ├── deploys.go              # Deploy + quick deploy endpoints
│   │   └── middleware.go           # Auth, logging, CORS
│   ├── store/
│   │   ├── store.go                # SQLite schema, migrations
│   │   ├── projects.go             # Project queries
│   │   ├── instances.go            # Instance queries
│   │   ├── registries.go           # Registry queries
│   │   ├── settings.go             # Settings queries
│   │   ├── deploys.go              # Deploy history queries
│   │   └── store_test.go
│   └── crypto/
│       └── crypto.go               # AES-256 encrypt/decrypt for credentials
├── web/                            # SvelteKit frontend
│   ├── src/
│   │   ├── routes/
│   │   │   ├── +page.svelte        # Dashboard
│   │   │   ├── projects/
│   │   │   │   ├── +page.svelte    # Projects list + add
│   │   │   │   └── [id]/
│   │   │   │       └── +page.svelte # Project detail + instances
│   │   │   ├── deploy/
│   │   │   │   └── +page.svelte    # Quick deploy
│   │   │   └── settings/
│   │   │       ├── +page.svelte    # Global settings
│   │   │       ├── registries/
│   │   │       │   └── +page.svelte
│   │   │       └── credentials/
│   │   │           └── +page.svelte
│   │   ├── lib/
│   │   │   ├── api.ts              # API client
│   │   │   ├── types.ts            # Shared types
│   │   │   └── components/         # Reusable UI components
│   │   └── app.html
│   ├── package.json
│   ├── svelte.config.js
│   └── vite.config.ts
├── docker-watcher.example.yaml     # Example seed config
├── Dockerfile
├── docker-compose.yml
├── go.mod
└── go.sum
```

## Implementation Phases

### Phase 1: Foundation ✅

Core infrastructure — store, config import, Docker client, NPM client.

1. **Go project init** — go.mod, directory structure, dependencies
2. **SQLite store** — schema, migrations, CRUD for projects/registries/settings/instances/deploys
3. **Crypto** — AES-256 encrypt/decrypt for credential storage
4. **Config seed loader** — parse YAML, import into SQLite on first launch
5. **Docker client** — connect to socket, pull image, inspect image, list/start/stop/remove containers, manage networks
6. **NPM client** — authenticate (JWT), create/update/delete proxy hosts, list existing hosts

### Phase 2: Detection & Deployment (Registry & Poller ✅, Webhook ✅, Deployer ✅)

The core loop — detecting new images and deploying them.

8. **Registry client** ✅ — Gitea registry API: list tags for an image, detect new tags
9. **Poller** ✅ — periodic check for new tags matching configured patterns
10. **Secret webhook handler** ✅ — UUID-based URL, receives image push notifications, auto-creates unknown projects
11. **Deployer** ✅ — orchestrate: pull → start container → NPM proxy → health check
12. **Multi-instance support** ✅ — multiple versions per project/stage, tag-based subdomains, max_instances limit
13. **Health checker** ✅ — HTTP GET with retries and timeout (3 retries, 5s interval, 10s timeout)
14. **Rollback** ✅ — on health check failure: remove new container, clean up NPM, alert
15. **Notifications** ✅ — send webhook on deploy success/failure (fire-and-forget)

### Phase 3: Web UI

Full dashboard for visibility, manual control, and configuration.

16. **API layer** — REST endpoints for all CRUD operations + deploy/control actions
17. **SvelteKit dashboard** — project overview, instance status, quick status indicators
18. **Project detail view** — stages, instances, controls (stop/start/restart/remove), deploy history
19. **Quick Deploy page** — paste image URL, auto-inspect, pre-fill form, one-click deploy
20. **Settings pages** — registries, credentials, global settings, webhook URL management
21. **Project config pages** — add/edit/delete projects and stages via UI
22. **Embed in Go** ✅ — build SvelteKit to static, embed with `go:embed`, serve from Go
23. **Real-time updates** ✅ — SSE for deploy progress and instance status changes

### Phase 4: Volumes & Environment (Phase 13) -- COMPLETED

Persistent storage and app-specific configuration for deployed containers.

24. **Environment variables per project** — key/value pairs stored in SQLite, sensitive values encrypted
25. **Per-stage env overrides** — e.g., `NODE_ENV=development` for dev, `NODE_ENV=production` for prod
26. **Volume mounts per project** — configurable source/target paths with shared/isolated modes
27. **Shared volumes** — all instances of a project mount the same host path (for stateless apps or shared uploads)
28. **Isolated volumes** — each instance gets its own subdirectory: `{source}/{stage}-{tag}/` → `{target}` (for stateful apps with local DBs/files)
29. **UI for volumes & env** — project settings page with key/value editor, volume list, shared/isolated toggle, per-stage override support

#### Phase 13 Handoff Notes

- New tables: `stage_env` (id, stage_id, key, value, encrypted, timestamps), `volumes` (id, project_id, source, target, mode, timestamps)
- `stage_env` has UNIQUE(stage_id, key) constraint to prevent duplicate keys per stage
- Volume mode is either "shared" or "isolated"; default is "shared"
- Encrypted env values are encrypted with `crypto.Encrypt` before storage and decrypted at deploy time
- API masks encrypted env values as "••••••••" in responses
- Env merge order in deployer: project-level JSON `env` field parsed first, then stage-level `stage_env` records overlay (stage wins on key conflict)
- `computeVolumeMounts` appends `/{stage}-{tag}/` to source for isolated volumes
- Docker `ContainerConfig` now has `Mounts []mount.Mount` field, passed to `HostConfig.Mounts`
- Both `executeDeploy` and `blueGreenDeploy` updated to use `mergeEnvVars` and `computeVolumeMounts`
- API routes: GET/POST `/api/projects/{id}/stages/{stage}/env`, PUT/DELETE `.../env/{envId}`, GET/POST `/api/projects/{id}/volumes`, PUT/DELETE `.../volumes/{volId}`
- Frontend pages: `/projects/[id]/env` (per-stage env editor with inherited/overridden indicators), `/projects/[id]/volumes` (volume editor with shared/isolated toggle)
- Project detail page now has navigation links to env and volumes pages

Volume config per project:
```yaml
env:
  NODE_ENV: production
  DATABASE_URL: postgres://db:5432/myapp    # shared external DB
  SECRET_KEY: "..."                          # encrypted in SQLite
volumes:
  - source: /data/my-app/uploads
    target: /app/uploads
    mode: shared        # all instances share this path
  - source: /data/my-app/data
    target: /app/data
    mode: isolated      # auto-appends /{stage}-{tag}/ to source
```

Stage-level env overrides:
```yaml
stages:
  dev:
    env:
      NODE_ENV: development       # overrides project-level
      DATABASE_URL: postgres://db:5432/myapp_dev
  prod:
    env:
      NODE_ENV: production        # uses project-level default
```

### Phase 5: Hardening (Phase 12) -- COMPLETED

30. **Blue-green deploys** -- start new, health check, swap, stop old (zero downtime)
31. **Promote flow** -- enforce `promote_from` for production deploys
32. **Auth on dashboard** -- two modes, configurable via settings:
    - **Local auth** -- username/password stored in SQLite (bcrypt hashed), JWT session tokens
    - **OAuth2 / OpenID Connect** -- integration with any OIDC provider (configurable client ID/secret/discovery URL)
33. **Graceful shutdown** -- drain in-progress deploys on SIGTERM, close DB, stop poller
34. **Structured logging** -- JSON logs via `log/slog` with deploy context
35. **Config export** -- download current SQLite state as YAML
36. **Dockerfile** -- multi-stage build (Node.js 20 + Go 1.23 build, alpine runtime)
37. **docker-compose.yml** -- production-ready compose with volumes, network, env
38. **Auth middleware** -- protects all /api/* routes except webhook and auth endpoints
39. **Auth settings UI** -- settings page to toggle auth mode, configure OIDC, manage users
40. **Login page** -- username/password form with OIDC SSO option
41. **Final wiring** -- all services properly initialized and shut down in main.go

#### Phase 12 Handoff Notes

- Auth: `auth.LocalAuth` handles JWT generation/validation, `auth.OIDCProvider` handles OIDC flow
- Default admin user created on first launch (ADMIN_PASSWORD env var, default: "admin")
- JWT secret derived from ENCRYPTION_KEY via HMAC-SHA256
- Blue-green: triggered automatically when stage has `max_instances=1`; otherwise standard deploy
- Promote: validated in `TriggerDeploy` before deploy begins
- Graceful shutdown: `deployer.Drain()` waits for in-progress deploys; poller stopped; HTTP server drained; DB closed
- Structured logging: all API, deployer, and main.go use `log/slog` JSON handler
- New dependencies: `github.com/golang-jwt/jwt/v5`, `golang.org/x/crypto/bcrypt`, `github.com/coreos/go-oidc/v3`, `golang.org/x/oauth2`
- New tables: `users` (id, username, password_hash, email, role, timestamps), `auth_settings` (single-row: auth_mode, OIDC config)
- Auth middleware applied to all `/api/*` routes except `/api/auth/login`, `/api/auth/oidc/*`, `/api/webhook/*`, `/api/config/export`
- Frontend: token stored in `localStorage`, sent as `Authorization: Bearer` header
- Run `go mod tidy` after checkout to resolve transitive dependencies

## Key Dependencies (Go)

- `github.com/docker/docker` — Docker Engine API
- `github.com/go-chi/chi` or `net/http` — HTTP routing
- `gopkg.in/yaml.v3` — YAML seed config
- `modernc.org/sqlite` — SQLite (CGo-free)
- `github.com/robfig/cron` — Polling scheduler
- `github.com/google/uuid` — Webhook secret URL generation

## Docker Compose (self-deployment)

```yaml
services:
  docker-watcher:
    image: docker-watcher:latest
    container_name: docker-watcher
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./docker-watcher.yaml:/app/seed.yaml:ro  # optional, first launch only
      - ./data:/app/data                          # SQLite DB
    environment:
      - ENCRYPTION_KEY=${ENCRYPTION_KEY}           # protects all credentials in DB
    networks:
      - staging-net

networks:
  staging-net:
    external: true
```

## API Endpoints

```text
# Projects
GET    /api/projects                                    — list all projects with instance counts
POST   /api/projects                                    — create project
GET    /api/projects/:id                                — project detail + stages + instances
PUT    /api/projects/:id                                — update project config
DELETE /api/projects/:id                                — delete project + all instances

# Stages
POST   /api/projects/:id/stages                         — add stage to project
PUT    /api/projects/:id/stages/:stage                   — update stage config
DELETE /api/projects/:id/stages/:stage                   — delete stage + its instances

# Stage Env Overrides
GET    /api/projects/:id/stages/:stage/env                 — list stage env vars (secrets masked)
POST   /api/projects/:id/stages/:stage/env                 — create stage env var
PUT    /api/projects/:id/stages/:stage/env/:envId          — update stage env var
DELETE /api/projects/:id/stages/:stage/env/:envId          — delete stage env var

# Project Volumes
GET    /api/projects/:id/volumes                           — list project volumes
POST   /api/projects/:id/volumes                           — create project volume
PUT    /api/projects/:id/volumes/:volId                    — update project volume
DELETE /api/projects/:id/volumes/:volId                    — delete project volume

# Instances (running containers)
GET    /api/projects/:id/stages/:stage/instances          — list instances for stage
POST   /api/projects/:id/stages/:stage/instances          — deploy new instance (pick tag)
DELETE /api/projects/:id/stages/:stage/instances/:iid     — remove instance (container + NPM proxy)
POST   /api/projects/:id/stages/:stage/instances/:iid/stop    — stop container
POST   /api/projects/:id/stages/:stage/instances/:iid/start   — start stopped container
POST   /api/projects/:id/stages/:stage/instances/:iid/restart — restart container

# Quick Deploy
POST   /api/deploy/inspect                               — pull + inspect image, return defaults
POST   /api/deploy/quick                                 — create project + deploy in one step

# Registry
GET    /api/registries                                   — list registries
POST   /api/registries                                   — add registry
PUT    /api/registries/:id                               — update registry
DELETE /api/registries/:id                               — delete registry
POST   /api/registries/:id/test                          — test connection
GET    /api/registries/:id/tags/:image                   — list available tags

# Settings
GET    /api/settings                                     — get global settings
PUT    /api/settings                                     — update global settings
GET    /api/settings/webhook-url                          — get secret webhook URL
POST   /api/settings/webhook-url/regenerate               — regenerate webhook URL

# Deploy history
GET    /api/deploys                                      — recent deploys across all projects
GET    /api/deploys/:id/logs                             — deploy log stream (SSE)

# Webhook (secret URL — no auth needed)
POST   /api/webhook/:secret-uuid                         — receive image push notification
```

## User Workflows

### Auto-Deploy (zero effort)

```text
Push code → CI builds → pushes tag → Docker Watcher detects →
auto_deploy: true → deployed → notification with URL
```

### Manual Deploy via UI (one click)

```text
Open dashboard → project → stage → "Deploy new version" →
pick tag from dropdown → click Deploy
```

### Quick Deploy (new project, paste image URL)

```text
Open dashboard → "Quick Deploy" → paste image URL →
review auto-filled defaults → click Deploy →
project auto-created + deployed
```

### Deploy via CI Webhook (zero effort after CI setup)

```text
# In .gitea/workflows/build.yml
- name: Notify Docker Watcher
  run: |
    curl -X POST https://watcher.dolgolyov-family.by/api/webhook/d8f2a1e9-... \
      -d '{"image": "git.dolgolyov-family.by/alexei/my-app:dev-${{ github.sha }}"}'
```

Known project → deploys per stage config.
Unknown project → auto-creates with defaults from image inspection, deploys.

### Production Deploy (two clicks)

```text
Open dashboard → project → prod stage → "Deploy new version" →
dropdown shows only tags running in "rel" stage (promote_from) →
pick tag → confirmation dialog → Deploy
```
