Head of Product & Engineering a40a4aa626
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
feat: initial implementation of Cast GHL Provider
Complete MVP implementation of the Cast GHL Conversation Provider bridge:
- Go module setup with chi router and mongo-driver dependencies
- Config loading with env var validation and defaults
- MongoDB token store with upsert, get, update, delete operations
- Cast.ph SMS client with 429 retry logic and typed errors
- Phone number normalization (E.164 ↔ Philippine local format)
- GHL OAuth 2.0 install/callback/refresh flow
- GHL webhook handler with ECDSA signature verification (async dispatch)
- GHL API client for message status updates and inbound message stubs
- Multi-stage Dockerfile, docker-compose with MongoDB, Woodpecker CI pipeline
- Unit tests for phone normalization, Cast client, GHL webhook, and OAuth handlers

Co-Authored-By: SideKx <sidekx.ai@sds.dev>
2026-04-04 17:27:05 +02:00

4.3 KiB

Task 09: Docker & Deployment

Objective

Finalize Dockerfile, docker-compose, Woodpecker CI, and deployment docs.

Part A: Dockerfile (finalize)

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)

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)

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:

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

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)