# Task 09: Docker & Deployment ## Objective Finalize Dockerfile, docker-compose, Woodpecker CI, and deployment docs. ## Part A: Dockerfile (finalize) ```dockerfile FROM golang:1.22-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /cast-ghl-provider ./cmd/server/ FROM alpine:3.19 RUN apk add --no-cache ca-certificates tzdata COPY --from=builder /cast-ghl-provider /cast-ghl-provider EXPOSE 3002 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD wget -qO- http://localhost:3002/health || exit 1 CMD ["/cast-ghl-provider"] ``` Key points: - Multi-stage build: ~15MB final image - `CGO_ENABLED=0` for static binary - `-ldflags="-s -w"` strips debug info - `ca-certificates` for HTTPS calls to Cast and GHL APIs - `tzdata` for timezone handling - `HEALTHCHECK` built into the image ## Part B: docker-compose.yaml (finalize) ```yaml services: bridge: build: . ports: - "${PORT:-3002}:${PORT:-3002}" env_file: .env depends_on: mongo: condition: service_started restart: unless-stopped logging: driver: json-file options: max-size: "10m" max-file: "3" mongo: image: mongo:7 volumes: - mongo-data:/data/db restart: unless-stopped # NOT exposed to host — only accessible from bridge service volumes: mongo-data: ``` ## Part C: Woodpecker CI (`.woodpecker.yml`) ```yaml steps: - name: build image: golang:1.22-alpine commands: - go build ./cmd/server/ - name: vet image: golang:1.22-alpine commands: - go vet ./... - name: test image: golang:1.22-alpine commands: - go test ./... - name: docker-build image: plugins/docker settings: repo: registry.sds.dev/cast/cast-ghl-provider registry: registry.sds.dev tags: - latest - "${CI_COMMIT_TAG}" username: from_secret: docker_username password: from_secret: docker_password when: event: tag ref: refs/tags/v* - name: deploy image: appleboy/drone-ssh settings: host: from_secret: deploy_host username: from_secret: deploy_user key: from_secret: deploy_key script: - cd /opt/cast-ghl-provider - docker compose pull - docker compose up -d --remove-orphans when: event: tag ref: refs/tags/v* ``` ### Woodpecker secrets to configure: - `docker_username` — container registry username - `docker_password` — container registry password - `deploy_host` — Vultr server IP - `deploy_user` — SSH user - `deploy_key` — SSH private key ### Release workflow: ```bash git tag v0.1.0 git push origin v0.1.0 # Woodpecker: build → vet → test → docker build+push → SSH deploy ``` ## Part D: Nginx reverse proxy config ```nginx server { listen 443 ssl; server_name ghl.cast.ph; ssl_certificate /etc/letsencrypt/live/ghl.cast.ph/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/ghl.cast.ph/privkey.pem; location / { proxy_pass http://127.0.0.1:3002; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } server { listen 80; server_name ghl.cast.ph; return 301 https://$server_name$request_uri; } ``` ## Part E: Server setup checklist 1. Create directory: `mkdir -p /opt/cast-ghl-provider` 2. Copy `docker-compose.yaml` and `.env` to server 3. Configure `.env` with production values 4. Set up Nginx + Let's Encrypt 5. `docker compose up -d` 6. Verify: `curl https://ghl.cast.ph/health` 7. Update GHL Marketplace app: - Delivery URL: `https://ghl.cast.ph/api/ghl/v1/webhook/messages` - Redirect URI: `https://ghl.cast.ph/oauth-callback` ## Acceptance Criteria - [ ] `docker compose build` succeeds - [ ] `docker compose up` starts bridge + mongo - [ ] Health check passes: `curl localhost:3002/health` - [ ] Docker image is <20MB - [ ] MongoDB not exposed to host network - [ ] `.woodpecker.yml` has build, vet, test steps - [ ] Docker build+push only on `v*` tags - [ ] Deploy step uses SSH to pull and restart - [ ] Nginx config handles HTTPS + proxy - [ ] Log rotation configured (10MB, 3 files)