AI Prompt: Part 5 — Order-GridFlock Integration + Docker Compose + CI/CD Pipeline¶
Series: Forma3D.Connect Microservice Decomposition + GridFlock STL Pipeline (Part 5 of 6) Purpose: Wire the GridFlock pipeline into the Shopify order flow, build production Docker Compose, and update the Azure DevOps CI/CD pipeline for all microservices Estimated Effort: 24–32 hours Prerequisites: Parts 1–4 completed (all services built and tested individually) Output: End-to-end GridFlock order flow working, production Docker Compose with all services, updated Azure DevOps pipeline deploying all microservices Status: 🚧 TODO Previous Part: Part 4 — GridFlock Service + Slicer Container Next Part: Part 6 — Event Verification + Documentation + Cleanup
🎯 Mission¶
Complete three critical integration tasks:
- Wire GridFlock into the Shopify order flow — When a Shopify order contains a custom grid size, automatically detect it, trigger the GridFlock pipeline, and create print jobs when ready
- Build production Docker Compose — All services, Redis, slicer, monitoring tools (Uptime Kuma, Dozzle)
- Update Azure DevOps CI/CD pipeline — Build and deploy all 8 Docker images with affected detection
What this part delivers:
- Order-GridFlock Integration in the Order Service:
- GridFlock product detection in Shopify webhook handler (SKU prefix)
- Shopify line item property extraction (width, height, connectors, magnets)
- Deterministic SKU computation and product mapping lookup
- GridflockServiceClient calls to trigger pipeline
- Event subscriptions for
gridflock.mapping-readyandgridflock.pipeline-failed - Race condition handling (concurrent orders for same new size)
- Production Docker Compose (
deployment/staging/docker-compose.yml): - All 5 NestJS services + gateway + slicer + Redis
- Uptime Kuma (health monitoring) + Dozzle (log viewer)
- Proper service dependencies and health checks
- Traefik routing configuration
- Azure DevOps Pipeline updates:
- 8 Package jobs (one per service image)
- Affected detection for all Nx apps + slicer
- Updated deploy script with service ordering
- New pipeline variables and variable group entries
- All Dockerfiles for every service
- Server upgrade instructions (1 GB → 4 GB)
📌 Prerequisites (Parts 1–4 Completed)¶
Verify these before starting:
- All shared libraries available and tested
- Gateway running and routing correctly
- Order Service running with internal API
- Print Service running with SimplyPrint API Files upload
- Shipping Service running with event publishing
- GridFlock Service running with pipeline and feature flag
- Slicer Container built and responding to /health
- All event flows working (order → print → fulfillment → shipping)
🔧 Implementation¶
Phase 8: Order Service GridFlock Integration (6–8 hours)¶
Priority: High | Impact: High | Dependencies: Part 4 (GridFlock Service)
8.1 GridFlock Product Detection¶
Add to the Order Service orchestration module:
// In Order Service: OrchestrationService.handleOrderCreated()
async handleOrderCreated(order: Order): Promise<void> {
for (const lineItem of order.lineItems) {
if (this.isGridflockProduct(lineItem)) {
// Extract dimensions from Shopify line item properties
const dimensions = this.extractGridDimensions(lineItem.properties);
const connectorType = this.extractConnectorType(lineItem.properties);
const magnets = this.extractMagnets(lineItem.properties);
// Compute SKU and check if mapping exists
const sku = this.computeGridflockSku(dimensions, connectorType, magnets);
const existingMapping = await this.productMappingService.findBySku(tenantId, sku);
if (existingMapping) {
// Mapping exists — create print jobs immediately
await this.createPrintJobsForMapping(tenantId, order, lineItem, existingMapping);
} else {
// No mapping yet — trigger GridFlock pipeline
await this.gridflockServiceClient.generateForOrder({
tenantId, orderId: order.id, lineItemId: lineItem.id,
widthMm: dimensions.width, heightMm: dimensions.height,
connectorType, magnets,
});
// Print jobs created when gridflock.mapping-ready event arrives
}
} else {
// Standard product — existing flow
await this.createPrintJobsFromMapping(tenantId, order, lineItem);
}
}
}
private isGridflockProduct(lineItem: LineItem): boolean {
return lineItem.productSku?.startsWith('GRID-CUSTOM') ?? false;
}
private extractGridDimensions(properties: Array<{name: string; value: string}>): {width: number; height: number} {
const width = properties.find(p => p.name === 'Width (mm)')?.value;
const height = properties.find(p => p.name === 'Height (mm)')?.value;
if (!width || !height) throw new Error('Grid dimensions missing from line item properties');
return { width: parseInt(width, 10), height: parseInt(height, 10) };
}
8.2 Event Subscriptions¶
// Order Service subscribes to gridflock.mapping-ready
await this.eventBus.subscribe(
SERVICE_EVENTS.GRIDFLOCK_MAPPING_READY,
async (event: GridflockMappingReadyEvent) => {
const { tenantId, orderId, lineItemId, sku } = event;
const mapping = await this.productMappingService.findBySku(tenantId, sku);
const order = await this.ordersService.findById(tenantId, orderId);
const lineItem = order.lineItems.find(li => li.id === lineItemId);
await this.createPrintJobsForMapping(tenantId, order, lineItem, mapping);
},
);
// Order Service subscribes to gridflock.pipeline-failed
await this.eventBus.subscribe(
SERVICE_EVENTS.GRIDFLOCK_PIPELINE_FAILED,
async (event: GridflockPipelineFailedEvent) => {
// Mark line item as GENERATION_FAILED and notify operator
await this.ordersService.markLineItemFailed(event.tenantId, event.orderId, event.lineItemId, event.errorMessage);
// Send push notification
await this.notificationService.sendGridflockFailure(event);
},
);
8.3 Full GridFlock Pipeline Event Flow¶
Shopify webhook → Order Service: create order
→ detect GridFlock product (SKU "GRID-CUSTOM")
→ extract dimensions from line item properties
→ compute SKU "GF-450x320-IP-MAG"
→ check ProductMapping
├── FOUND → create print jobs → normal flow
└── NOT FOUND:
→ HTTP: GridflockServiceClient.generateForOrder()
→ GridFlock Service: FOR EACH PLATE (sequential):
→ JSCAD: STL Buffer → Slicer: gcode Buffer → SimplyPrint upload
→ GridFlock Service → Order Service: create product mapping
→ EventBus: gridflock.mapping-ready
→ Order Service: creates print jobs → normal print flow
8.4 Race Condition Handling¶
Two orders for the same new GridFlock size arriving simultaneously:
- The GridFlock pipeline checks for existing mapping before generating
- If another request created the mapping while this one was processing, it detects it and skips
- Both orders eventually get print jobs from the same mapping
Phase 9: Docker Compose & Infrastructure (6–8 hours)¶
Priority: Critical | Impact: High | Dependencies: All services built
9.1 Server Upgrade¶
Before deploying, upgrade the DigitalOcean Droplet:
Current: 1 vCPU / 1 GB RAM / 25 GB disk (~$7/month)
Required: 2 vCPU / 4 GB RAM / 80 GB disk (~$24/month, Premium AMD)
Steps:
1. Power off Droplet via DigitalOcean dashboard
2. Resize to s-2vcpu-4gb-amd
3. Power on
4. Remove pgAdmin from docker-compose.yml (saves ~272 MB)
9.2 Production Docker Compose¶
Replace deployment/staging/docker-compose.yml with the full microservice configuration:
# deployment/staging/docker-compose.yml
services:
traefik:
image: traefik:v3.0
container_name: forma3d-traefik
restart: unless-stopped
ports:
- '80:80'
- '443:443'
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/etc/traefik/traefik.yml:ro
- traefik-certs:/letsencrypt
networks:
- forma3d-network
redis:
image: redis:7-alpine
container_name: forma3d-redis
restart: unless-stopped
volumes:
- redis-data:/data
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 10s
timeout: 5s
retries: 5
networks:
- forma3d-network
gateway:
image: ${REGISTRY_URL}/forma3d-connect-gateway:${GATEWAY_IMAGE_TAG:-latest}
container_name: forma3d-gateway
restart: unless-stopped
environment:
- NODE_ENV=staging
- APP_PORT=3000
- APP_URL=${API_URL}
- FRONTEND_URL=${WEB_URL}
- DATABASE_URL=${DATABASE_URL}
- REDIS_HOST=redis
- REDIS_PORT=6379
- SESSION_SECRET=${SESSION_SECRET}
- INTERNAL_API_KEY=${INTERNAL_API_KEY}
- ORDER_SERVICE_URL=http://order-service:3001
- PRINT_SERVICE_URL=http://print-service:3002
- SHIPPING_SERVICE_URL=http://shipping-service:3003
- GRIDFLOCK_SERVICE_URL=http://gridflock-service:3004
- SENTRY_DSN=${SENTRY_DSN}
volumes:
- ./certs/do-postgresql-ca-certificate-2.crt:/app/certs/ca-certificate.crt:ro
networks:
- forma3d-network
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.api.rule=Host(`staging-connect-api.forma3d.be`)'
- 'traefik.http.routers.api.entrypoints=websecure'
- 'traefik.http.routers.api.tls=true'
- 'traefik.http.routers.api.tls.certresolver=letsencrypt'
- 'traefik.http.services.api.loadbalancer.server.port=3000'
healthcheck:
test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:3000/health']
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
depends_on:
redis:
condition: service_healthy
order-service:
image: ${REGISTRY_URL}/forma3d-connect-order-service:${ORDER_SERVICE_IMAGE_TAG:-latest}
container_name: forma3d-order-service
restart: unless-stopped
environment:
- NODE_ENV=staging
- APP_PORT=3001
- DATABASE_URL=${DATABASE_URL}
- REDIS_HOST=redis
- REDIS_PORT=6379
- INTERNAL_API_KEY=${INTERNAL_API_KEY}
- PRINT_SERVICE_URL=http://print-service:3002
- SHIPPING_SERVICE_URL=http://shipping-service:3003
- GRIDFLOCK_SERVICE_URL=http://gridflock-service:3004
- SHOPIFY_SHOP_DOMAIN=${SHOPIFY_SHOP_DOMAIN:-}
- SHOPIFY_ACCESS_TOKEN=${SHOPIFY_ACCESS_TOKEN:-}
- SHOPIFY_WEBHOOK_SECRET=${SHOPIFY_WEBHOOK_SECRET:-}
- SHOPIFY_API_VERSION=${SHOPIFY_API_VERSION:-2026-01}
- SHOPIFY_API_KEY=${SHOPIFY_API_KEY}
- SHOPIFY_API_SECRET=${SHOPIFY_API_SECRET}
- SHOPIFY_TOKEN_ENCRYPTION_KEY=${SHOPIFY_TOKEN_ENCRYPTION_KEY}
- SENTRY_DSN=${SENTRY_DSN}
volumes:
- ./certs/do-postgresql-ca-certificate-2.crt:/app/certs/ca-certificate.crt:ro
networks:
- forma3d-network
healthcheck:
test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:3001/health']
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
depends_on:
redis:
condition: service_healthy
print-service:
image: ${REGISTRY_URL}/forma3d-connect-print-service:${PRINT_SERVICE_IMAGE_TAG:-latest}
container_name: forma3d-print-service
restart: unless-stopped
environment:
- NODE_ENV=staging
- APP_PORT=3002
- DATABASE_URL=${DATABASE_URL}
- REDIS_HOST=redis
- REDIS_PORT=6379
- INTERNAL_API_KEY=${INTERNAL_API_KEY}
- SIMPLYPRINT_API_URL=${SIMPLYPRINT_API_URL:-https://api.simplyprint.io}
- SIMPLYPRINT_API_KEY=${SIMPLYPRINT_API_KEY:-placeholder}
- SIMPLYPRINT_COMPANY_ID=${SIMPLYPRINT_COMPANY_ID:-placeholder}
- SENTRY_DSN=${SENTRY_DSN}
volumes:
- ./certs/do-postgresql-ca-certificate-2.crt:/app/certs/ca-certificate.crt:ro
networks:
- forma3d-network
healthcheck:
test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:3002/health']
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
depends_on:
redis:
condition: service_healthy
shipping-service:
image: ${REGISTRY_URL}/forma3d-connect-shipping-service:${SHIPPING_SERVICE_IMAGE_TAG:-latest}
container_name: forma3d-shipping-service
restart: unless-stopped
environment:
- NODE_ENV=staging
- APP_PORT=3003
- DATABASE_URL=${DATABASE_URL}
- REDIS_HOST=redis
- REDIS_PORT=6379
- INTERNAL_API_KEY=${INTERNAL_API_KEY}
- ORDER_SERVICE_URL=http://order-service:3001
- SENDCLOUD_PUBLIC_KEY=${SENDCLOUD_PUBLIC_KEY:-}
- SENDCLOUD_SECRET_KEY=${SENDCLOUD_SECRET_KEY:-}
- SENTRY_DSN=${SENTRY_DSN}
volumes:
- ./certs/do-postgresql-ca-certificate-2.crt:/app/certs/ca-certificate.crt:ro
networks:
- forma3d-network
healthcheck:
test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:3003/health']
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
depends_on:
redis:
condition: service_healthy
gridflock-service:
image: ${REGISTRY_URL}/forma3d-connect-gridflock-service:${GRIDFLOCK_SERVICE_IMAGE_TAG:-latest}
container_name: forma3d-gridflock-service
restart: unless-stopped
environment:
- NODE_ENV=staging
- APP_PORT=3004
- DATABASE_URL=${DATABASE_URL}
- REDIS_HOST=redis
- REDIS_PORT=6379
- INTERNAL_API_KEY=${INTERNAL_API_KEY}
- SLICER_URL=http://slicer:3010
- PRINT_SERVICE_URL=http://print-service:3002
- ORDER_SERVICE_URL=http://order-service:3001
- SENTRY_DSN=${SENTRY_DSN}
volumes:
- gridflock-data:/data/gridflock
- ./certs/do-postgresql-ca-certificate-2.crt:/app/certs/ca-certificate.crt:ro
networks:
- forma3d-network
healthcheck:
test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:3004/health']
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
depends_on:
redis:
condition: service_healthy
slicer:
condition: service_healthy
slicer:
image: ${REGISTRY_URL}/forma3d-connect-slicer:${SLICER_IMAGE_TAG:-latest}
container_name: forma3d-slicer
restart: unless-stopped
environment:
- NODE_ENV=staging
- APP_PORT=3010
networks:
- forma3d-network
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:3010/health']
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
web:
image: ${REGISTRY_URL}/forma3d-connect-web:${WEB_IMAGE_TAG:-latest}
container_name: forma3d-web
restart: unless-stopped
networks:
- forma3d-network
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.web.rule=Host(`staging-connect.forma3d.be`)'
- 'traefik.http.routers.web.entrypoints=websecure'
- 'traefik.http.routers.web.tls=true'
- 'traefik.http.routers.web.tls.certresolver=letsencrypt'
- 'traefik.http.services.web.loadbalancer.server.port=80'
depends_on:
- gateway
docs:
image: ${REGISTRY_URL}/forma3d-connect-docs:${DOCS_IMAGE_TAG:-latest}
container_name: forma3d-docs
restart: unless-stopped
networks:
- forma3d-network
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.docs.rule=Host(`staging-connect-docs.forma3d.be`)'
- 'traefik.http.routers.docs.entrypoints=websecure'
- 'traefik.http.routers.docs.tls=true'
- 'traefik.http.routers.docs.tls.certresolver=letsencrypt'
- 'traefik.http.services.docs.loadbalancer.server.port=80'
uptime-kuma:
image: louislam/uptime-kuma:1
container_name: forma3d-uptime-kuma
restart: unless-stopped
volumes:
- uptime-kuma-data:/app/data
networks:
- forma3d-network
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.status.rule=Host(`staging-connect-status.forma3d.be`)'
- 'traefik.http.routers.status.entrypoints=websecure'
- 'traefik.http.routers.status.tls=true'
- 'traefik.http.routers.status.tls.certresolver=letsencrypt'
- 'traefik.http.services.status.loadbalancer.server.port=3001'
dozzle:
image: amir20/dozzle:latest
container_name: forma3d-dozzle
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- forma3d-network
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.logs.rule=Host(`staging-connect-logs.forma3d.be`)'
- 'traefik.http.routers.logs.entrypoints=websecure'
- 'traefik.http.routers.logs.tls=true'
- 'traefik.http.routers.logs.tls.certresolver=letsencrypt'
- 'traefik.http.services.logs.loadbalancer.server.port=8080'
environment:
- DOZZLE_NO_ANALYTICS=true
networks:
forma3d-network:
name: forma3d-network
driver: bridge
volumes:
traefik-certs:
redis-data:
gridflock-data:
uptime-kuma-data:
9.3 Service Ports¶
| Service | Port | Container Name |
|---|---|---|
| Traefik | 80, 443 | forma3d-traefik |
| Gateway | 3000 | forma3d-gateway |
| Order Service | 3001 | forma3d-order-service |
| Print Service | 3002 | forma3d-print-service |
| Shipping Service | 3003 | forma3d-shipping-service |
| GridFlock Service | 3004 | forma3d-gridflock-service |
| Slicer | 3010 | forma3d-slicer |
| Redis | 6379 | forma3d-redis |
9.4 Dockerfiles¶
Each NestJS service follows the same multi-stage pattern. Change the build target and port per service:
# Template — customize per service
FROM node:20-alpine AS builder
WORKDIR /app
RUN apk add --no-cache openssl
RUN corepack enable && corepack prepare pnpm@9 --activate
COPY package.json pnpm-lock.yaml ./
COPY prisma ./prisma/
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm prisma generate
RUN pnpm nx build {SERVICE_NAME} --prod
FROM node:20-alpine AS deps
WORKDIR /app
RUN apk add --no-cache openssl
RUN corepack enable && corepack prepare pnpm@9 --activate
COPY --from=builder /app/dist/apps/{SERVICE_NAME}/package.json ./
COPY --from=builder /app/pnpm-lock.yaml ./
RUN pnpm install --prod --ignore-scripts
COPY --from=builder /app/prisma ./prisma
RUN pnpm prisma generate
FROM node:20-alpine AS production
WORKDIR /app
RUN apk add --no-cache openssl
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nestjs
COPY --from=builder /app/dist/apps/{SERVICE_NAME} ./dist
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/prisma ./prisma
RUN chown -R nestjs:nodejs /app
USER nestjs
EXPOSE {PORT}
CMD ["node", "dist/main.js"]
| Dockerfile | Build target | Port |
|---|---|---|
apps/gateway/Dockerfile |
gateway |
3000 |
apps/order-service/Dockerfile |
order-service |
3001 |
apps/print-service/Dockerfile |
print-service |
3002 |
apps/shipping-service/Dockerfile |
shipping-service |
3003 |
apps/gridflock-service/Dockerfile |
gridflock-service |
3004 |
deployment/slicer/Dockerfile |
Custom | 3010 |
Phase 10: Azure DevOps Pipeline (12–16 hours)¶
Priority: Critical | Impact: Very High | Dependencies: Phase 9
10.1 New Pipeline Variables¶
variables:
- name: gatewayImageName
value: 'forma3d-connect-gateway'
- name: orderServiceImageName
value: 'forma3d-connect-order-service'
- name: printServiceImageName
value: 'forma3d-connect-print-service'
- name: shippingServiceImageName
value: 'forma3d-connect-shipping-service'
- name: gridflockServiceImageName
value: 'forma3d-connect-gridflock-service'
- name: slicerImageName
value: 'forma3d-connect-slicer'
10.2 Updated Affected Detection¶
AFFECTED_APPS=$(pnpm nx show projects --affected --base=HEAD~1 --head=HEAD --type=app 2>/dev/null || echo "")
for SERVICE in gateway order-service print-service shipping-service gridflock-service; do
if echo "$AFFECTED_APPS" | grep -qE "^${SERVICE}$"; then
echo "##vso[task.setvariable variable=${SERVICE//-/}Affected;isOutput=true]true"
else
echo "##vso[task.setvariable variable=${SERVICE//-/}Affected;isOutput=true]false"
fi
done
# Slicer detected via git diff (not an Nx project)
CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD || echo "")
if echo "$CHANGED_FILES" | grep -qE "^deployment/slicer/"; then
echo "##vso[task.setvariable variable=slicerAffected;isOutput=true]true"
else
echo "##vso[task.setvariable variable=slicerAffected;isOutput=true]false"
fi
10.3 New Package Jobs¶
One Package job per service, all running in parallel:
PackageGateway → forma3d-connect-gateway
PackageOrderService → forma3d-connect-order-service
PackagePrintService → forma3d-connect-print-service
PackageShippingService → forma3d-connect-shipping-service
PackageGridflockService → forma3d-connect-gridflock-service
PackageSlicer → forma3d-connect-slicer
Each uses Docker buildx with registry caching, cosign signing, and SBOM.
10.4 Updated Deploy Script¶
# 1. Pull affected images
# 2. Start Redis first
# 3. Run migrations from gateway container
# 4. Start backend services (order, print, shipping, gridflock, slicer)
# 5. Wait for health checks
# 6. Start gateway (after backends are healthy)
# 7. Start web/docs
# 8. Final health check via gateway
10.5 Variable Group Updates¶
Add to forma3d-staging Azure DevOps variable group:
| Variable | Secret? |
|---|---|
GATEWAY_IMAGE_TAG |
No |
ORDER_SERVICE_IMAGE_TAG |
No |
PRINT_SERVICE_IMAGE_TAG |
No |
SHIPPING_SERVICE_IMAGE_TAG |
No |
GRIDFLOCK_SERVICE_IMAGE_TAG |
No |
SLICER_IMAGE_TAG |
No |
INTERNAL_API_KEY |
Yes |
10.6 DNS Records¶
Add DNS records for monitoring subdomains:
- staging-connect-status.forma3d.be → Droplet IP (Uptime Kuma)
- staging-connect-logs.forma3d.be → Droplet IP (Dozzle)
🧪 Testing Requirements¶
Order-GridFlock Integration Tests¶
- Shopify order with GridFlock product triggers pipeline
- Line item properties correctly extracted
- Deterministic SKU computed correctly
- Existing mapping → immediate print job creation
- Missing mapping → GridFlock pipeline triggered
gridflock.mapping-readyevent → print jobs createdgridflock.pipeline-failedevent → error handling + notification- Race condition: concurrent orders for same new size
- Tenant print settings applied to slicer
Docker & Infrastructure Tests¶
docker compose up -dstarts all services- All services pass health checks
- Gateway routes to all services correctly
- Existing functionality works through gateway
- Redis accessible from all services
Pipeline Tests¶
DetectAffecteddetects all new servicesForceFullVersioningAndDeployment=truemarks all as affected- Each Package job builds successfully
- Deploy script handles service ordering correctly
- Full pipeline: push → build → deploy → health check
✅ Validation Checklist¶
GridFlock Order Flow¶
- Shopify order with "GRID-CUSTOM" SKU triggers pipeline
- Line item properties
Width (mm)andHeight (mm)extracted - Deterministic SKU computed (same dimensions → same SKU)
- Existing mapping → print jobs created immediately
- Missing mapping → full pipeline runs
-
gridflock.mapping-readytriggers print job creation - Pipeline failure publishes error event + notification
- Race condition handled correctly
Infrastructure¶
- Droplet upgraded to 2 vCPU / 4 GB RAM
- pgAdmin removed from docker-compose
- All services start via docker-compose
- Gateway health aggregates all services
- Uptime Kuma accessible at
staging-connect-status.forma3d.be - Dozzle accessible at
staging-connect-logs.forma3d.be
Dockerfiles¶
- All 6 Dockerfiles build successfully
- All images use multi-stage builds, non-root user, health checks
- All images pushed to DigitalOcean Container Registry
Pipeline¶
- Affected detection works for all services
- All Package jobs build and push images
- Deploy script starts services in correct order
- Full pipeline succeeds end-to-end
- Affected-only deployment works (change one service → only that rebuilds)
🚫 Constraints¶
- Only Gateway is exposed through Traefik — all backend services are internal
- Do NOT modify
apps/web— must work without changes - Docker Compose must use registry images (not local builds)
- Deploy script must handle ordering: Redis → backends → gateway
connection_limit=3in all service DATABASE_URL strings- No hardcoded service URLs — use environment variables
- All NestJS services must output JSON structured logs
- Keep Sentry for error tracking — Dozzle is for log viewing only
📚 Key References¶
- Current pipeline:
azure-pipelines.yml(~2700 lines) - Current Docker Compose:
deployment/staging/docker-compose.yml - Shopify line item properties: https://shopify.dev/docs/api/liquid/objects/line_item
- Uptime Kuma: https://uptime.kuma.pet/
- Dozzle: https://dozzle.dev/
- Staging server:
167.172.45.47 - Staging deployment guide:
docs/05-deployment/staging-deployment-guide.md
END OF PART 5
Previous: Part 4 — GridFlock Service + Slicer Container Next: Part 6 — Event Verification + Documentation + Cleanup