Forma3D.Connect — Staging Deployment Guide¶
Complete guide for setting up automated staging deployment on DigitalOcean with Azure DevOps
Table of Contents¶
- Overview
- Architecture
- Prerequisites
- Part 1: Azure DevOps Configuration
- Part 2: Droplet Configuration
- Part 3: First Deployment
- Verification
- Troubleshooting
- Maintenance
Overview¶
The staging deployment pipeline automatically:
- Detects affected apps using Nx affected to identify what has changed
- Builds Docker images only for affected applications (API, Web, or both)
- Signs images with cosign for supply chain security
- Generates SBOM using Syft (CycloneDX format) and attaches as signed attestation
- Pushes images to DigitalOcean Container Registry
- Deploys to a DigitalOcean Droplet via SSH (only affected apps)
- Runs database migrations before starting containers (correct order for safety)
- 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:
ForceFullVersioningAndDeploymentistrueduring testing. Set tofalseonce pipeline is stable.
Architecture¶
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.
- Navigate to the Html Viewer extension in the Visual Studio Marketplace
- Click Get it free
- Select your Azure DevOps organization
- 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¶
- Navigate to Pipelines > Environments
- Click New environment
- Create environment:
staging - Name:
staging - Resource: None
- Create environment:
production - Name:
production - Resource: None
- (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:
- Click on the uploaded file
- Click Pipeline permissions
- Click + and authorize all pipelines (or specific ones)
Step 1.4: Create Variable Group¶
Navigate to Pipelines > Library > Variable groups
- Click + Variable group
- Name:
forma3d-staging - 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_ENVIRONMENTis set tostagingbydeployment/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_DSNfromSENTRY_DSN - The pipeline derives
VITE_SENTRY_TRACES_SAMPLE_RATEfromSENTRY_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: OverridesNODE_ENVfor Sentry’s environment tag (API only). If omitted, the API usesNODE_ENV.SENTRY_DEBUG: Enable/disable Sentry SDK debug logs (API only).SENTRY_RELEASE: Override the release tag (API only). Defaults toforma3d-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 |
- Click Save
Step 1.5: Variable Group Integration ✅¶
The pipeline is already configured to use the forma3d-staging variable group. The deployment step will:
- Create a
.envfile on the droplet using the variable group values - Copy the PostgreSQL CA certificate to the server
- 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
.envfile is now automatically generated by the Azure DevOps pipeline during deployment using theforma3d-stagingvariable 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¶
- Push a change to the
developbranch - Monitor the pipeline in Azure DevOps
- The pipeline will:
- Validate → Test → Build (+ Detect Affected) → Package (conditional) → Deploy (conditional)
Step 3.3: Understanding the Deployment Flow¶
The pipeline follows a specific order to ensure safe deployments:
Why this order matters:
- Migrations before restart: New API code may depend on new database columns/tables
- Breaking migrations: Use
breakingMigration=truewhen 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:
- Go to Pipelines in Azure DevOps
- Click Run pipeline
- Check Force Full Versioning and Deployment
- 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:
- Verify
azure-devopssecure file is uploaded - Check droplet has public key in
~/.ssh/authorized_keys - Verify file permissions:
chmod 600 ~/.ssh/authorized_keys
Pipeline Fails: "Cannot pull image"¶
Problem: Docker registry authentication fails
Solution:
- Verify
docker-config.jsonis uploaded to Secure Files - On droplet, verify
~/.docker/config.jsonexists and is valid - 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:
- Verify DNS points to droplet IP:
dig staging-connect-api.forma3d.be - Verify port 80 is open:
ufw status - Check Traefik logs:
docker compose logs traefik - Verify
acme.jsonpermissions:docker compose exec traefik cat /letsencrypt/acme.json
Database Migrations Fail¶
Problem: Prisma migrate fails during deployment
Solution:
- Check DATABASE_URL is correct
- Verify CA certificate path matches docker-compose.yml volume mount
- 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