Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
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>
179 lines
4.3 KiB
Markdown
179 lines
4.3 KiB
Markdown
# 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)
|