Skip to content

Forma3D.Connect — Staging Deployment Guide

Complete guide for setting up automated staging deployment on DigitalOcean with Azure DevOps


Table of Contents

  1. Overview
  2. Architecture
  3. Prerequisites
  4. Part 1: Azure DevOps Configuration
  5. Part 2: Droplet Configuration
  6. Part 3: First Deployment
  7. Verification
  8. Troubleshooting
  9. Maintenance

Overview

The staging deployment pipeline automatically:

  1. Detects affected apps using Nx affected to identify what has changed
  2. Builds Docker images only for affected applications (API, Web, or both)
  3. Signs images with cosign for supply chain security
  4. Generates SBOM using Syft (CycloneDX format) and attaches as signed attestation
  5. Pushes images to DigitalOcean Container Registry
  6. Deploys to a DigitalOcean Droplet via SSH (only affected apps)
  7. Runs database migrations before starting containers (correct order for safety)
  8. Configures TLS certificates via Let's Encrypt

Trigger: Push to develop branch

Conditional Deployment

The pipeline uses Nx affected to avoid unnecessary Docker builds and deployments:

Change Type API Deployed Web Deployed
Only backend code changed
Only frontend code changed
Shared library changed
Only docs/config changed
Force full deployment

Pipeline Parameters

Parameter Default Description
ForceFullVersioningAndDeployment true Bypass affected detection, deploy all apps
breakingMigration false Stop API before migrations (for breaking schema)

Note: ForceFullVersioningAndDeployment is true during testing. Set to false once pipeline is stable.


Architecture

uml diagram


Prerequisites

Before starting, ensure you have:

  • Access to Azure DevOps project
  • SSH access to DigitalOcean Droplet (167.172.45.47)
  • DigitalOcean Container Registry credentials
  • DigitalOcean Managed PostgreSQL connection details
  • Shopify app credentials
  • Sentry DSN

Files located in /Users/jeankedotcom/Library/CloudStorage/OneDrive-Personal/DevGem/projects/forma-3d-connect/sec/:

File Purpose
/Users/jeankedotcom/Library/CloudStorage/OneDrive-Personal/DevGem/projects/forma-3d-connect/sec/droplet/azure-devops SSH private key
/Users/jeankedotcom/Library/CloudStorage/OneDrive-Personal/DevGem/projects/forma-3d-connect/sec/droplet/azure-devops.pub SSH public key
/Users/jeankedotcom/Library/CloudStorage/OneDrive-Personal/DevGem/projects/forma-3d-connect/sec/droplet/droplet-info.txt Droplet connection info
/Users/jeankedotcom/Library/CloudStorage/OneDrive-Personal/DevGem/projects/forma-3d-connect/sec/container-registry/docker-config.json Docker registry authentication
/Users/jeankedotcom/Library/CloudStorage/OneDrive-Personal/DevGem/projects/forma-3d-connect/sec/container-registry/url.txt Registry URL
/Users/jeankedotcom/Library/CloudStorage/OneDrive-Personal/DevGem/projects/forma-3d-connect/sec/managed-postgresql/db-info.txt Database connection details

Part 1: Azure DevOps Configuration

Step 1.1: Install Required Marketplace Extension

The pipeline uses the Html Viewer extension to display Cucumber and Playwright test reports as tabs in Azure DevOps build results.

  1. Navigate to the Html Viewer extension in the Visual Studio Marketplace
  2. Click Get it free
  3. Select your Azure DevOps organization
  4. Click Install

Note: This requires organization administrator permissions. If you don't have access, contact your Azure DevOps admin.

After installation, the pipeline will automatically publish:

Tab Name Content
Cucumber Report Gherkin-formatted HTML report with scenarios and steps
Playwright Report Full Playwright report with traces and screenshots

Step 1.2: Create Environments

  1. Navigate to Pipelines > Environments
  2. Click New environment
  3. Create environment: staging
  4. Name: staging
  5. Resource: None
  6. Create environment: production
  7. Name: production
  8. Resource: None
  9. (Optional) Add approval gate

Step 1.3: Upload Secure Files

Navigate to Pipelines > Library > Secure files

Upload the following files:

Secure File Name Source File Description
azure-devops /Users/jeankedotcom/Library/CloudStorage/OneDrive-Personal/DevGem/projects/forma-3d-connect/sec/droplet/azure-devops SSH private key for droplet
docker-config.json /Users/jeankedotcom/Library/CloudStorage/OneDrive-Personal/DevGem/projects/forma-3d-connect/sec/container-registry/docker-config.json Docker registry auth
do-postgresql-ca-certificate-2.crt /Users/jeankedotcom/Library/CloudStorage/OneDrive-Personal/DevGem/projects/forma-3d-connect/sec/managed-postgresql/ (download from DO) PostgreSQL TLS certificate
cosign.key Generated locally (see Cosign Setup Guide) Private key for image signing

For each file, set permissions:

  1. Click on the uploaded file
  2. Click Pipeline permissions
  3. Click + and authorize all pipelines (or specific ones)

Step 1.4: Create Variable Group

Navigate to Pipelines > Library > Variable groups

  1. Click + Variable group
  2. Name: forma3d-staging
  3. Add the following variables:
Variable Value Secret?
DROPLET_IP 167.172.45.47 No
REGISTRY_URL registry.digitalocean.com/forma3d No
API_URL https://staging-connect-api.forma3d.be No
WEB_URL https://staging-connect.forma3d.be No
DATABASE_URL (see /Users/jeankedotcom/Library/CloudStorage/OneDrive-Personal/DevGem/projects/forma-3d-connect/sec/managed-postgresql/db-info.txt) Yes
SHOPIFY_SHOP_DOMAIN your-shop.myshopify.com No
SHOPIFY_API_KEY (your API key) Yes
SHOPIFY_API_SECRET (your API secret) Yes
SHOPIFY_ACCESS_TOKEN (your access token) Yes
SHOPIFY_WEBHOOK_SECRET (your webhook secret) Yes
SHOPIFY_API_VERSION 2024-01 No
SENTRY_DSN (your Sentry DSN) Yes
SENTRY_TRACES_SAMPLE_RATE 1.0 (staging) / 0.1 (production) No
SENTRY_PROFILES_SAMPLE_RATE 1.0 (staging) / 0.1 (production) No
SENTRY_DEBUG true (staging) / false (production) No
SENTRY_ENVIRONMENT staging / production No
COSIGN_PASSWORD (password for cosign.key - see Cosign Setup Guide) Yes
INTERNAL_API_KEY (generate with openssl rand -hex 32) Yes
SIMPLYPRINT_API_URL https://api.simplyprint.io/v1 No
SIMPLYPRINT_API_KEY (your SimplyPrint API key) Yes
SIMPLYPRINT_COMPANY_ID (your SimplyPrint company ID, e.g., S123456) No
SIMPLYPRINT_WEBHOOK_SECRET (your webhook secret for verification) Yes
SIMPLYPRINT_POLLING_ENABLED false (set to true for fallback polling) No
SHIPPING_ENABLED true or false No
SENDCLOUD_PUBLIC_KEY (your Sendcloud public key) Yes
SENDCLOUD_SECRET_KEY (your Sendcloud secret key) Yes
DEFAULT_SHIPPING_METHOD_ID 8 (default shipping method ID) No
DEFAULT_SENDER_ADDRESS_ID (your sender address ID) No
VAPID_PUBLIC_KEY (generated VAPID public key) No
VAPID_PRIVATE_KEY (generated VAPID private key) Yes
VAPID_SUBJECT mailto:jan.wielemans@devgem.be No

Push Notifications (Phase 7 - PWA)

VAPID keys are required for Web Push notifications. Generate them once and use across all environments:

# Generate VAPID keys (do this once, reuse the keys)
npx web-push generate-vapid-keys --json

This outputs:

{
  "publicKey": "BGAS6q-nMn1lt8-foIqSoJORZlarc0RbEmxzz2qU4O-...",
  "privateKey": "99Rho7TKzIlIIIkeNgJ-BTvqD9t5bjSHJkKwsiXDxAY"
}

Add these values to the variable group: - VAPID_PUBLIC_KEY - The public key (shared with frontend, not secret) - VAPID_PRIVATE_KEY - The private key (must be kept secret) - VAPID_SUBJECT - Contact email in mailto: format (for push service providers)

Important: VAPID keys should be the same across staging and production to avoid subscription issues when users move between environments. Generate once and reuse.

Operational Configuration (Phase 5k)

The following variables control operational behavior. They have sensible defaults and are optional - only add them if you need to tune behavior for your environment:

Observability sampling (Sentry performance)

  • API (runtime) reads SENTRY_* env vars from the droplet .env.
  • For staging, SENTRY_ENVIRONMENT is set to staging by deployment/staging/docker-compose.yml.
  • Web (build-time) reads VITE_* env vars baked into the Docker image during the pipeline build.
  • The pipeline derives VITE_SENTRY_DSN from SENTRY_DSN
  • The pipeline derives VITE_SENTRY_TRACES_SAMPLE_RATE from SENTRY_TRACES_SAMPLE_RATE

Recommended starting values:

  • staging: SENTRY_TRACES_SAMPLE_RATE=1.0, SENTRY_PROFILES_SAMPLE_RATE=1.0 (full visibility)
  • production: SENTRY_TRACES_SAMPLE_RATE=0.1, SENTRY_PROFILES_SAMPLE_RATE=0.1 (lower volume)

Optional Sentry tuning variables (rarely needed):

  • SENTRY_ENVIRONMENT: Overrides NODE_ENV for Sentry’s environment tag (API only). If omitted, the API uses NODE_ENV.
  • SENTRY_DEBUG: Enable/disable Sentry SDK debug logs (API only).
  • SENTRY_RELEASE: Override the release tag (API only). Defaults to forma3d-connect@{version}.
Variable Default Secret? Description
RETRY_QUEUE_MAX_RETRIES 5 No Max retry attempts before job is marked as failed
RETRY_QUEUE_INITIAL_DELAY_MS 1000 No Initial delay before first retry (1 second)
RETRY_QUEUE_MAX_DELAY_MS 3600000 No Maximum delay cap (1 hour)
RETRY_QUEUE_BACKOFF_MULTIPLIER 2 No Exponential backoff multiplier
RETRY_QUEUE_CLEANUP_RETENTION_DAYS 7 No Days to keep completed retry jobs before cleanup
API_TIMEOUT_SIMPLYPRINT_MS 30000 No SimplyPrint API timeout (30 seconds)
API_TIMEOUT_SENDCLOUD_MS 30000 No Sendcloud API timeout (30 seconds)
API_TIMEOUT_SHOPIFY_MS 30000 No Shopify API timeout (30 seconds)
WEBHOOK_IDEMPOTENCY_RETENTION_HOURS 24 No Hours to retain webhook idempotency keys
RATE_LIMIT_TTL_SECONDS 60 No Rate limit window duration in seconds
RATE_LIMIT_DEFAULT 100 No Default max requests per rate limit window
RATE_LIMIT_WEBHOOK 50 No Max webhook requests per rate limit window
DATABASE_POOL_SIZE 10 No Prisma database connection pool size
DATABASE_CONNECT_TIMEOUT_SECONDS 5 No Database connection timeout in seconds
DATABASE_POOL_TIMEOUT_SECONDS 10 No Timeout waiting for pool connection in seconds

Production Recommendations:

Variable Staging Production
RETRY_QUEUE_MAX_RETRIES 5 10
RETRY_QUEUE_INITIAL_DELAY_MS 1000 2000
RETRY_QUEUE_MAX_DELAY_MS 3600000 7200000
RETRY_QUEUE_CLEANUP_RETENTION_DAYS 7 30
API_TIMEOUT_*_MS 30000 60000
WEBHOOK_IDEMPOTENCY_RETENTION_HOURS 24 48
RATE_LIMIT_TTL_SECONDS 60 60
RATE_LIMIT_DEFAULT 100 200
RATE_LIMIT_WEBHOOK 50 100
DATABASE_POOL_SIZE 10 20
DATABASE_CONNECT_TIMEOUT_SECONDS 5 10
DATABASE_POOL_TIMEOUT_SECONDS 10 15
  1. Click Save

Step 1.5: Variable Group Integration ✅

The pipeline is already configured to use the forma3d-staging variable group. The deployment step will:

  1. Create a .env file on the droplet using the variable group values
  2. Copy the PostgreSQL CA certificate to the server
  3. Run the deployment with the configured environment

Pipeline configuration (in azure-pipelines.yml):

variables:
  - group: forma3d-staging
  # ... other variables

The deployment script automatically writes the .env file to /opt/forma3d/.env on the droplet, so you don't need to manually create or maintain it on the server.

Note: The current pipeline uses the variable group variables via the deploy script. You may need to modify the pipeline to pass these as environment variables.


Part 2: Droplet Configuration

Step 2.1: SSH to Droplet

# From your local machine (use the private key from /Users/jeankedotcom/Library/CloudStorage/OneDrive-Personal/DevGem/projects/forma-3d-connect/sec/droplet/)
ssh -i /Users/jeankedotcom/Library/CloudStorage/OneDrive-Personal/DevGem/projects/forma-3d-connect/sec/droplet/azure-devops root@167.172.45.47

Step 2.2: Install Docker

# Update packages
apt update && apt upgrade -y

# Install Docker
curl -fsSL https://get.docker.com | sh

# Verify installation
docker --version
# Expected: Docker version 24.x or later

# Verify Docker Compose plugin
docker compose version
# Expected: Docker Compose version v2.x

Step 2.3: Configure Docker Registry Access

# Create Docker config directory
mkdir -p ~/.docker

# Copy the docker-config.json content (from /Users/jeankedotcom/Library/CloudStorage/OneDrive-Personal/DevGem/projects/forma-3d-connect/sec/container-registry/docker-config.json)
# Either upload the file via scp or create it manually:
cat > ~/.docker/config.json << 'EOF'
{
  "auths": {
    "registry.digitalocean.com": {
      "auth": "YOUR_BASE64_AUTH_TOKEN"
    }
  }
}
EOF

# Verify login works
docker pull registry.digitalocean.com/forma-3d/forma3d-connect-api:latest || echo "Image not yet available"

Step 2.4: Create Deployment Directory Structure

# Create deployment directory
mkdir -p /opt/forma3d/certs

# Navigate to directory
cd /opt/forma3d

Step 2.5: Copy Deployment Files

Option A: Copy from your local machine via SCP

# From your local machine (project root)
scp -i /Users/jeankedotcom/Library/CloudStorage/OneDrive-Personal/DevGem/projects/forma-3d-connect/sec/droplet/azure-devops \
  deployment/staging/docker-compose.yml \
  deployment/staging/traefik.yml \
  root@167.172.45.47:/opt/forma3d/

scp -i /Users/jeankedotcom/Library/CloudStorage/OneDrive-Personal/DevGem/projects/forma-3d-connect/sec/droplet/azure-devops \
  deployment/staging/deploy.sh \
  root@167.172.45.47:/opt/forma3d/

# Upload the PostgreSQL CA certificate
# (Get this from DigitalOcean Managed Database dashboard)
scp -i /Users/jeankedotcom/Library/CloudStorage/OneDrive-Personal/DevGem/projects/forma-3d-connect/sec/droplet/azure-devops \
  /path/to/do-postgresql-ca-certificate-2.crt \
  root@167.172.45.47:/opt/forma3d/certs/

Option B: Create files directly on the droplet

# On the droplet
cd /opt/forma3d

# Create docker-compose.yml (copy content from deployment/staging/docker-compose.yml)
nano docker-compose.yml

# Create traefik.yml (copy content from deployment/staging/traefik.yml)
nano traefik.yml

# Create deploy.sh (copy content from deployment/staging/deploy.sh)
nano deploy.sh
chmod +x deploy.sh

Step 2.6: Environment File (Auto-Generated)

Note: The .env file is now automatically generated by the Azure DevOps pipeline during deployment using the forma3d-staging variable group. You don't need to create it manually.

The pipeline will create /opt/forma3d/.env with the following structure:

# =============================================================================
# Forma3D.Connect Staging Environment
# Auto-generated by Azure DevOps Pipeline
# =============================================================================

# Docker Registry
REGISTRY_URL=registry.digitalocean.com/forma3d
API_IMAGE_TAG=20260110143709
WEB_IMAGE_TAG=20260110143709

# Application URLs
API_URL=https://staging-connect-api.forma3d.be
WEB_URL=https://staging-connect.forma3d.be

# Database
DATABASE_URL=postgresql://...

# Shopify Integration
SHOPIFY_SHOP_DOMAIN=your-shop.myshopify.com
SHOPIFY_API_KEY=...
SHOPIFY_API_SECRET=...
SHOPIFY_ACCESS_TOKEN=...
SHOPIFY_WEBHOOK_SECRET=...
SHOPIFY_API_VERSION=2024-01

# SimplyPrint Integration (Phase 2)
SIMPLYPRINT_API_URL=https://api.simplyprint.io/v1
SIMPLYPRINT_API_KEY=your-api-key
SIMPLYPRINT_COMPANY_ID=S123456
SIMPLYPRINT_WEBHOOK_SECRET=your-webhook-secret
SIMPLYPRINT_POLLING_ENABLED=false

# Observability
SENTRY_DSN=https://...
SENTRY_TRACES_SAMPLE_RATE=1.0
SENTRY_PROFILES_SAMPLE_RATE=1.0
# Optional:
# SENTRY_DEBUG=true
# SENTRY_ENVIRONMENT=staging

# API Security
INTERNAL_API_KEY=your-api-key-here

# Shipping Integration (SendCloud) - Phase 5
SHIPPING_ENABLED=true
SENDCLOUD_PUBLIC_KEY=your-public-key
SENDCLOUD_SECRET_KEY=your-secret-key
DEFAULT_SHIPPING_METHOD_ID=8
DEFAULT_SENDER_ADDRESS_ID=your-sender-address-id

# Rate Limiting (optional - defaults shown)
# RATE_LIMIT_TTL_SECONDS=60
# RATE_LIMIT_DEFAULT=100
# RATE_LIMIT_WEBHOOK=50

# Database Pool (optional - defaults shown)
# DATABASE_POOL_SIZE=10
# DATABASE_CONNECT_TIMEOUT_SECONDS=5
# DATABASE_POOL_TIMEOUT_SECONDS=10

# Push Notifications (Phase 7 - PWA)
VAPID_PUBLIC_KEY=BGAS6q-nMn1lt8-foIqSoJORZlarc0RbEmxzz2qU4O-...
VAPID_PRIVATE_KEY=99Rho7TKzIlIIIkeNgJ-BTvqD9t5bjSHJkKwsiXDxAY
VAPID_SUBJECT=mailto:jan.wielemans@devgem.be

For manual testing only: If you need to create a temporary .env file before the first pipeline run:

# On the droplet - only for manual testing
cd /opt/forma3d
touch .env
chmod 600 .env
# The pipeline will overwrite this on first deployment

Step 2.7: Verify Directory Structure

# On the droplet
ls -la /opt/forma3d/

Expected output:

/opt/forma3d/
├── .env                    # Environment variables (600 permissions)
├── certs/
│   └── do-postgresql-ca-certificate-2.crt  # PostgreSQL CA cert
├── deploy.sh               # Deployment script (executable)
├── docker-compose.yml      # Docker Compose stack
└── traefik.yml             # Traefik configuration

Step 2.8: Configure SSH Key for Azure DevOps

The SSH public key needs to be authorized on the droplet:

# On the droplet
cat >> ~/.ssh/authorized_keys << 'EOF'
ssh-rsa AAAAB3... (content of azure-devops.pub)
EOF

# Set correct permissions
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

Step 2.9: Open Firewall Ports

# On the droplet
ufw allow 22/tcp   # SSH
ufw allow 80/tcp   # HTTP (for Let's Encrypt challenge)
ufw allow 443/tcp  # HTTPS
ufw enable
ufw status

Part 3: First Deployment

Step 3.1: Manual Test Deployment

Before running the pipeline, test the deployment manually:

# On the droplet
cd /opt/forma3d

# Set the image tag (use 'latest' for first deployment)
export IMAGE_TAG=latest

# Pull images (will fail if not yet pushed - that's OK)
docker compose pull

# Start the stack
docker compose up -d

# Check status
docker compose ps

# View logs
docker compose logs -f

Step 3.2: Trigger Pipeline Deployment

  1. Push a change to the develop branch
  2. Monitor the pipeline in Azure DevOps
  3. The pipeline will:
  4. ValidateTestBuild (+ Detect Affected) → Package (conditional) → Deploy (conditional)

Step 3.3: Understanding the Deployment Flow

The pipeline follows a specific order to ensure safe deployments:

uml diagram

Why this order matters:

  • Migrations before restart: New API code may depend on new database columns/tables
  • Breaking migrations: Use breakingMigration=true when migrations are not backward-compatible
  • Only affected apps: Reduces deployment time and avoids unnecessary container restarts

Step 3.4: Manual Override

To force a full deployment (all apps) regardless of changes:

  1. Go to Pipelines in Azure DevOps
  2. Click Run pipeline
  3. Check Force Full Versioning and Deployment
  4. Click Run

This is useful for:

  • First deployment to a new environment
  • Recovering from failed partial deployments
  • Testing the full deployment flow

Step 3.3: Monitor Deployment

# On the droplet - watch containers
watch docker compose ps

# View real-time logs
docker compose logs -f

# Check specific service
docker compose logs api
docker compose logs web
docker compose logs traefik

Verification

Verify Services Are Running

# On the droplet
docker compose ps

Expected output:

NAME              IMAGE                                                        STATUS
forma3d-api       registry.digitalocean.com/forma-3d/forma3d-connect-api:xxx   Up (healthy)
forma3d-traefik   traefik:v3.0                                                 Up
forma3d-web       registry.digitalocean.com/forma-3d/forma3d-connect-web:xxx   Up (healthy)

Verify External Access

# From any machine

# API health check (includes database status and build info)
curl https://staging-connect-api.forma3d.be/health
# Expected: {
#   "status": "ok",
#   "database": "connected",
#   "timestamp": "...",
#   "version": "0.0.1",
#   "build": {
#     "number": "20260111143948",
#     "date": "2026-01-11T14:39:48Z",
#     "commit": "abc123..."
#   },
#   "uptime": 3600
# }

# API liveness probe (simple check)
curl https://staging-connect-api.forma3d.be/health/live
# Expected: {"status":"ok"}

# API readiness probe (checks database)
curl https://staging-connect-api.forma3d.be/health/ready
# Expected: {"status":"ok","database":"connected"}

# Web health check (includes build info)
curl https://staging-connect.forma3d.be/health
# Expected: {"status":"ok","build":{"number":"...","date":"...","commit":"..."}}

# Web liveness probe
curl https://staging-connect.forma3d.be/health/live
# Expected: {"status":"ok"}

curl https://staging-connect-api.forma3d.be/api/docs
# Expected: Swagger UI HTML

Verify TLS Certificates

# Check certificate
echo | openssl s_client -connect staging-connect-api.forma3d.be:443 2>/dev/null | openssl x509 -noout -dates
# Expected: notAfter=... (valid date in the future)

Verify Database Connection

# On the droplet
docker compose exec api npx prisma db execute --stdin <<< "SELECT 1"
# Expected: No errors

Troubleshooting

Pipeline Fails: "Permission denied (publickey)"

Problem: SSH connection fails

Solution:

  1. Verify azure-devops secure file is uploaded
  2. Check droplet has public key in ~/.ssh/authorized_keys
  3. Verify file permissions: chmod 600 ~/.ssh/authorized_keys

Pipeline Fails: "Cannot pull image"

Problem: Docker registry authentication fails

Solution:

  1. Verify docker-config.json is uploaded to Secure Files
  2. On droplet, verify ~/.docker/config.json exists and is valid
  3. Test manually: docker pull registry.digitalocean.com/forma-3d/forma3d-connect-api:latest

Containers Not Starting

Problem: Containers exit immediately

Solution:

# Check logs
docker compose logs api
docker compose logs web

# Common issues:
# - DATABASE_URL incorrect → check .env file
# - Certificate not found → check /opt/forma3d/certs/
# - Port conflict → check `docker ps` for conflicting containers

TLS Certificates Not Issued

Problem: Let's Encrypt challenge fails

Solution:

  1. Verify DNS points to droplet IP: dig staging-connect-api.forma3d.be
  2. Verify port 80 is open: ufw status
  3. Check Traefik logs: docker compose logs traefik
  4. Verify acme.json permissions:
    docker compose exec traefik cat /letsencrypt/acme.json
    

Database Migrations Fail

Problem: Prisma migrate fails during deployment

Solution:

  1. Check DATABASE_URL is correct
  2. Verify CA certificate path matches docker-compose.yml volume mount
  3. Test connection manually:
    docker compose run --rm api npx prisma db execute --stdin <<< "SELECT 1"
    

Deploy to Staging / Acceptance Test Stages Skipped

Problem: Pipeline shows "Deploy to Staging" and "Acceptance Test" as skipped, even though previous stages succeeded.

Cause: These stages use conditions that reference output variables from the Build stage (specifically stageDependencies.Build.DetectAffected.outputs['affected.apiAffected']). In Azure DevOps YAML, stage conditions can only reliably access stageDependencies from stages explicitly listed in dependsOn.

Solution:

Ensure the stages that need to access Build stage outputs include Build in their dependsOn array:

# ❌ Wrong - cannot access Build outputs
- stage: DeployStaging
  dependsOn: Package
  condition: |
    and(
      succeeded(),
      eq(stageDependencies.Build.DetectAffected.outputs['affected.apiAffected'], 'true')
    )

# ✅ Correct - explicitly depends on Build
- stage: DeployStaging
  dependsOn:
    - Build
    - Package
  condition: |
    and(
      succeeded(),
      eq(stageDependencies.Build.DetectAffected.outputs['affected.apiAffected'], 'true')
    )

The affected stages and their required dependencies:

Stage Must include in dependsOn
DeployStaging Build, Package
AcceptanceTest Build, DeployStaging
DeployProduction Build, AcceptanceTest

Health Check Failing

Problem: Container marked as unhealthy

Solution:

# Check API health endpoint directly
docker compose exec api wget -qO- http://localhost:3000/health
# Returns full health info with database status and build info

# Check API liveness (simpler check)
docker compose exec api wget -qO- http://localhost:3000/health/live

# Check Web health endpoint
docker compose exec web wget -qO- http://localhost:80/health
# Returns build info

# Check Web liveness
docker compose exec web wget -qO- http://localhost:80/health/live

# Common issues:
# - Database not connected (check health.database field)
# - Missing environment variables
# - Application startup error (check logs)

Disk Space Full / Container Health Fails with "no space left on device"

Problem: Containers show as unhealthy, health checks fail with OCI runtime exec failed: no space left on device

Solution:

# Check disk usage
df -h /

# Clean up Docker resources
docker system prune -af --volumes

# If images are in use, remove old dangling images
docker image prune -f
docker images -q -f 'dangling=true' | xargs -r docker rmi -f

# Restart containers after cleanup
docker compose restart

Docker Log Rotation Not Configured

Problem: Docker logs grow indefinitely and fill the disk

Solution:

The pipeline now automatically configures log rotation. For manual setup:

# Create or update /etc/docker/daemon.json
cat > /etc/docker/daemon.json << 'EOF'
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}
EOF

# Restart Docker daemon
systemctl restart docker

# Recreate containers to apply new log settings
cd /opt/forma3d
docker compose up -d --force-recreate

# Verify log rotation is applied
docker inspect forma3d-api --format='{{.HostConfig.LogConfig.Config}}'
# Expected: map[max-file:3 max-size:10m]

Note: Log rotation settings: - max-size: 10m - Each log file max 10MB - max-file: 3 - Keep 3 rotated files - Maximum 30MB per container (90MB total for all 3 containers)


Maintenance

View Logs

# All services
docker compose logs -f

# Specific service (last 100 lines)
docker compose logs --tail=100 api

# Since timestamp
docker compose logs --since="2024-01-10T10:00:00" api

Restart Services

# Restart all
docker compose restart

# Restart specific service
docker compose restart api

Update Images

# Pull latest images (all)
docker compose pull

# Or pull specific service
docker compose pull api
docker compose pull web

# Recreate containers with new images
docker compose up -d

# Or restart specific service
docker compose up -d --no-deps api
docker compose up -d --no-deps web

# Clean up old images
docker image prune -f

Manual Conditional Deployment

To manually deploy only specific services (mimicking the pipeline behavior):

# For API-only deployment
docker compose pull api
docker compose run --rm api npx prisma migrate deploy
docker compose up -d --no-deps api

# For Web-only deployment
docker compose pull web
docker compose up -d --no-deps web

# For breaking migrations (stop first)
docker compose stop api
docker compose pull api
docker compose run --rm api npx prisma migrate deploy
docker compose up -d --no-deps api

Rollback

# List available image tags
docker images | grep forma3d

# Update .env or export to use specific tag
export IMAGE_TAG=20240110120000

# Redeploy
docker compose up -d

View Image Information

# Check current running image
docker inspect forma3d-api --format='{{.Config.Labels}}'

# Check image build info
docker inspect registry.digitalocean.com/forma-3d/forma3d-connect-api:latest --format='{{json .Config.Labels}}' | jq

Database Operations

# Run migrations manually
docker compose run --rm api npx prisma migrate deploy

# Check migration status
docker compose run --rm api npx prisma migrate status

# Open Prisma Studio (requires port forwarding)
docker compose exec api npx prisma studio

Quick Reference

Important Paths

Location Path
Deployment files (local) deployment/staging/
Deployment files (droplet) /opt/forma3d/
Secrets documentation /Users/jeankedotcom/Library/CloudStorage/OneDrive-Personal/DevGem/projects/forma-3d-connect/sec/
Azure Pipeline azure-pipelines.yml
API Dockerfile apps/api/Dockerfile
Web Dockerfile apps/web/Dockerfile

URLs

Environment API Web
Staging https://staging-connect-api.forma3d.be https://staging-connect.forma3d.be
Health Check https://staging-connect-api.forma3d.be/health https://staging-connect.forma3d.be/health
Liveness Probe https://staging-connect-api.forma3d.be/health/live https://staging-connect.forma3d.be/health/live
Readiness Probe https://staging-connect-api.forma3d.be/health/ready -
API Docs https://staging-connect-api.forma3d.be/api/docs -

Key Commands

# SSH to droplet
ssh -i /Users/jeankedotcom/Library/CloudStorage/OneDrive-Personal/DevGem/projects/forma-3d-connect/sec/droplet/azure-devops root@167.172.45.47

# View running containers
docker compose ps

# View logs
docker compose logs -f

# Redeploy
cd /opt/forma3d && ./deploy.sh <image_tag>

# Manual deployment
docker compose pull && docker compose up -d

Appendix: File Contents Reference

docker-compose.yml Location

  • Local: deployment/staging/docker-compose.yml
  • Droplet: /opt/forma3d/docker-compose.yml

traefik.yml Location

  • Local: deployment/staging/traefik.yml
  • Droplet: /opt/forma3d/traefik.yml

.env Template

  • Local: deployment/staging/env.staging.template
  • Droplet: /opt/forma3d/.env (must be created manually with real values)

Document Version: 1.1
Last Updated: Phase 1d Implementation
Maintainer: AI-generated, review before production use