Skip to content

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:

  1. 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
  2. Build production Docker Compose — All services, Redis, slicer, monitoring tools (Uptime Kuma, Dozzle)
  3. Update Azure DevOps CI/CD pipeline — Build and deploy all 8 Docker images with affected detection

What this part delivers:

  1. Order-GridFlock Integration in the Order Service:
  2. GridFlock product detection in Shopify webhook handler (SKU prefix)
  3. Shopify line item property extraction (width, height, connectors, magnets)
  4. Deterministic SKU computation and product mapping lookup
  5. GridflockServiceClient calls to trigger pipeline
  6. Event subscriptions for gridflock.mapping-ready and gridflock.pipeline-failed
  7. Race condition handling (concurrent orders for same new size)
  8. Production Docker Compose (deployment/staging/docker-compose.yml):
  9. All 5 NestJS services + gateway + slicer + Redis
  10. Uptime Kuma (health monitoring) + Dozzle (log viewer)
  11. Proper service dependencies and health checks
  12. Traefik routing configuration
  13. Azure DevOps Pipeline updates:
  14. 8 Package jobs (one per service image)
  15. Affected detection for all Nx apps + slicer
  16. Updated deploy script with service ordering
  17. New pipeline variables and variable group entries
  18. All Dockerfiles for every service
  19. 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-ready event → print jobs created
  • gridflock.pipeline-failed event → error handling + notification
  • Race condition: concurrent orders for same new size
  • Tenant print settings applied to slicer

Docker & Infrastructure Tests

  • docker compose up -d starts 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

  • DetectAffected detects all new services
  • ForceFullVersioningAndDeployment=true marks 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) and Height (mm) extracted
  • Deterministic SKU computed (same dimensions → same SKU)
  • Existing mapping → print jobs created immediately
  • Missing mapping → full pipeline runs
  • gridflock.mapping-ready triggers 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=3 in 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