Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 23 additions & 13 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ on:
branches: ["main"]
workflow_dispatch:

env:
SERVICE_NAME: tracking-api

permissions:
contents: read

Expand All @@ -33,12 +30,18 @@ jobs:
- name: Security Audit (non-blocking)
run: npm audit --audit-level=high || echo "Audit found vulnerabilities, please review."

- name: Tests
- name: Test tracking-api
run: TEST_DATABASE_URL= npm run -w @tracking-base/tracking-api test

- name: Build API
- name: Build tracking-api
run: npm run -w @tracking-base/tracking-api build

- name: Test router-worker
run: npm run -w @tracking-base/router-worker test

- name: Build router-worker
run: npm run -w @tracking-base/router-worker build

deploy-gate:
name: Deploy Gate
runs-on: ubuntu-latest
Expand All @@ -55,9 +58,14 @@ jobs:
permissions:
contents: read
packages: write
outputs:
version: ${{ steps.version.outputs.version }}
image: ${{ steps.version.outputs.image }}
strategy:
fail-fast: false
matrix:
include:
- service: tracking-api
dockerfile: Dockerfile
- service: router-worker
dockerfile: apps/router-worker/Dockerfile

steps:
- name: Checkout repository
Expand All @@ -68,10 +76,11 @@ jobs:
run: |
SHORT_SHA=$(echo ${{ github.event.pull_request.merge_commit_sha || github.sha }} | cut -c1-7)
VERSION="staging-${SHORT_SHA}"
REGISTRY_IMAGE="ghcr.io/$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')/${{ env.SERVICE_NAME }}"
REGISTRY_IMAGE="ghcr.io/$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')/${{ matrix.service }}"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "image=${REGISTRY_IMAGE}:${VERSION}" >> $GITHUB_OUTPUT
echo "REGISTRY_IMAGE=${REGISTRY_IMAGE}" >> $GITHUB_ENV
echo "registry_image=${REGISTRY_IMAGE}" >> $GITHUB_OUTPUT
echo "📦 Service: ${{ matrix.service }}"
echo "📦 Version: ${VERSION}"
echo "🐳 Image: ${REGISTRY_IMAGE}:${VERSION}"

Expand All @@ -89,15 +98,16 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
images: ${{ steps.version.outputs.registry_image }}
tags: |
type=raw,value=${{ steps.version.outputs.version }}
type=raw,value=latest

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile
file: ${{ matrix.dockerfile }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
Expand All @@ -110,6 +120,6 @@ jobs:
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY
echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Service | ${{ env.SERVICE_NAME }} |" >> $GITHUB_STEP_SUMMARY
echo "| Service | ${{ matrix.service }} |" >> $GITHUB_STEP_SUMMARY
echo "| Version | ${{ steps.version.outputs.version }} |" >> $GITHUB_STEP_SUMMARY
echo "| Image | ${{ steps.version.outputs.image }} |" >> $GITHUB_STEP_SUMMARY
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ coverage
*.tsbuildinfo
.vite
/docs/13-prompts
.DS_Store
fe-prank/
21 changes: 21 additions & 0 deletions apps/router-worker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM node:20-alpine AS builder

WORKDIR /app

COPY . .
RUN npm ci
RUN npm run -w @tracking-base/router-worker build
RUN npm prune --omit=dev

FROM node:20-alpine AS runtime

WORKDIR /app
ENV NODE_ENV=production

COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/package-lock.json ./package-lock.json
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/apps/router-worker/package.json ./apps/router-worker/package.json
COPY --from=builder /app/apps/router-worker/dist ./apps/router-worker/dist

CMD ["node", "apps/router-worker/dist/worker.js"]
67 changes: 67 additions & 0 deletions deploy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Deploy Layout

This folder separates deploy concerns:

- `deploy/app`: application containers (`tracking-api`, `router-worker`) pulled from GHCR.
- `deploy/infra`: shared infrastructure containers (`redis`).

## Is this model correct for current repo flow?

Yes, this matches the current architecture:

1. `tracking-api` receives `POST /track`.
2. API enqueues delivery jobs to Redis/BullMQ.
3. `router-worker` consumes jobs and pushes to destinations.
4. Postgres remains the canonical source of truth.

## Prerequisites

1. GHCR images exist for both services:
- `tracking-api`
- `router-worker`
2. Postgres is reachable (Railway/Supabase/self-hosted).
3. `DATABASE_URL` and `REDIS_URL` are set consistently for both services.

## Run

### 1) Start infra (creates shared Docker network `tracking-base-net`)

```bash
cd deploy/infra
cp .env.example .env
docker compose up -d
```

### 2) Start app

```bash
cd deploy/app
cp .env.example .env
# edit .env values first
docker compose up -d
```

### 3) Smoke check

```bash
curl -fsS http://127.0.0.1:3000/health
curl -fsS http://127.0.0.1:3000/ready
```

## Notes

- Keep `router-worker` private (do not expose ports).
- Expose public domain only for `tracking-api` through reverse proxy (Nginx/Caddy) with TLS.
- If GHCR images are private, `docker login ghcr.io` is required on the VPS.

## GHCR image naming from CI

This repository workflow publishes images to:

- `ghcr.io/<github-owner>/<repo>/tracking-api:{latest|staging-<sha>}`
- `ghcr.io/<github-owner>/<repo>/router-worker:{latest|staging-<sha>}`

Example if your repo is `lecongthien/Tracking_Base_System`:

- `ghcr.io/lecongthien/tracking_base_system/tracking-api:latest`
- `ghcr.io/lecongthien/tracking_base_system/router-worker:latest`
41 changes: 41 additions & 0 deletions deploy/app/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: tracking-base-app

services:
tracking-api:
image: ${TRACKING_API_IMAGE:-ghcr.io/your-org/your-repo/tracking-api:latest}
container_name: tracking-api
restart: unless-stopped
env_file:
- .env
environment:
NODE_ENV: production
PORT: ${TRACKING_API_PORT:-3000}
ports:
- "${TRACKING_API_PORT:-3000}:3000"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"]
interval: 30s
timeout: 5s
retries: 5
start_period: 20s
networks:
- tracking-base-net

router-worker:
image: ${ROUTER_WORKER_IMAGE:-ghcr.io/your-org/your-repo/router-worker:latest}
container_name: router-worker
restart: unless-stopped
env_file:
- .env
environment:
NODE_ENV: production
depends_on:
tracking-api:
condition: service_started
networks:
- tracking-base-net

networks:
tracking-base-net:
external: true
name: tracking-base-net
26 changes: 26 additions & 0 deletions deploy/infra/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: tracking-base-infra

services:
redis:
image: redis:7-alpine
container_name: tracking-redis
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
ports:
- "${REDIS_PORT:-6379}:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 5s
retries: 5
networks:
- tracking-base-net

volumes:
redis_data:

networks:
tracking-base-net:
name: tracking-base-net
Loading