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 services (Gateway, Order, Print, Shipping, GridFlock, Slicer, Web)
  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 services)
  7. Starts Redis before backend services (required for BullMQ events and sessions)
  8. Runs database migrations before starting containers (correct order for safety)
  9. Configures TLS certificates via Let's Encrypt

Trigger: Push to main branch

Deployment Order

Services must start in the correct order:

  1. Redis — Required by all backend services (BullMQ, sessions, Socket.IO)
  2. Logging stack — ClickHouse, OTel Collector, Grafana (receives logs from all services)
  3. Slicer — Required by GridFlock Service
  4. Backend services — Gateway, Order, Print, Shipping, GridFlock (can start in parallel)
  5. Web — Frontend (depends on Gateway being available)
  6. Monitoring — Uptime Kuma, Dozzle

Conditional Deployment

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

Change Type Gateway Order Print Shipping GridFlock Web
Only order-service 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:

Infrastructure & Registry

Variable Default / Value Secret? Notes
DOCR_TOKEN (DigitalOcean API token) Yes Used for docker login and doctl auth in all Package jobs
REGISTRY_URL registry.digitalocean.com/forma-3d No DigitalOcean Container Registry URL
API_URL https://staging-connect-api.forma3d.be No Public API Gateway URL
WEB_URL https://staging-connect.forma3d.be No Public web app URL
DOCS_URL https://staging-connect-docs.forma3d.be No Public docs site URL (used by registry cleanup)
DATABASE_URL (PostgreSQL connection string) Yes Managed PostgreSQL with ?sslmode=require
COSIGN_PASSWORD (password for cosign.key) Yes See Cosign Setup Guide

Authentication & Sessions

Variable Default / Value Secret? Notes
INTERNAL_API_KEY (generate: openssl rand -hex 32) Yes Used for inter-service X-Internal-Key header
SESSION_SECRET (generate: openssl rand -base64 32) Yes Express session secret
STOREFRONT_ALLOWED_ORIGINS .myshopify.com No Comma-separated storefront domains for CORS & guard. Prefix . = suffix match (e.g. .myshopify.com,forma3d.be)

Shopify Integration

Variable Default / Value Secret? Notes
SHOPIFY_SHOP_DOMAIN your-shop.myshopify.com No Legacy mode (optional with OAuth)
SHOPIFY_ACCESS_TOKEN (your access token) Yes Legacy mode (optional with OAuth)
SHOPIFY_WEBHOOK_SECRET (= SHOPIFY_API_SECRET) Yes API-registered webhooks use the app client secret
SHOPIFY_API_KEY (from Shopify Dev Dashboard) Yes OAuth app credentials
SHOPIFY_API_SECRET (from Shopify Dev Dashboard) Yes OAuth app credentials
SHOPIFY_APP_URL https://staging-connect-api.forma3d.be No OAuth redirect base URL (defaults to API_URL)
SHOPIFY_SCOPES read_orders,write_orders,read_products,write_products,read_fulfillments,write_fulfillments,read_inventory No OAuth requested scopes
SHOPIFY_API_VERSION 2026-01 No Shopify API version
SHOPIFY_TOKEN_ENCRYPTION_KEY (generate: openssl rand -hex 32) Yes Encrypts stored OAuth access tokens

Observability

Variable Default / Value Secret? Notes
SENTRY_DSN (your Sentry DSN) Yes DSN with auth token
SENTRY_TRACES_SAMPLE_RATE 1.0 (staging) / 0.1 (prod) No Performance monitoring sample rate
SENTRY_PROFILES_SAMPLE_RATE 1.0 (staging) / 0.1 (prod) No Profiling sample rate

SimplyPrint Integration

Variable Default / Value Secret? Notes
SIMPLYPRINT_API_URL https://api.simplyprint.io No API base URL
SIMPLYPRINT_API_KEY (your API key) Yes SimplyPrint account API key
SIMPLYPRINT_COMPANY_ID (e.g., S123456) No Company/account ID
SIMPLYPRINT_WEBHOOK_SECRET (your webhook secret) Yes Webhook payload verification
SIMPLYPRINT_POLLING_ENABLED false No Fallback polling when webhooks unavailable

Shipping (SendCloud)

Variable Default / Value Secret? Notes
SHIPPING_ENABLED false No Feature flag for shipping integration
SENDCLOUD_PUBLIC_KEY (your public key) No SendCloud API credentials
SENDCLOUD_SECRET_KEY (your secret key) Yes SendCloud API credentials
DEFAULT_SHIPPING_METHOD_ID 8 No Default Sendcloud shipping method
DEFAULT_SENDER_ADDRESS_ID (your sender address ID) No Sendcloud sender address

Rate Limiting

Variable Default / Value Secret? Notes
RATE_LIMIT_DISABLED true (staging) / false (prod) No Disable rate limiting for testing
RATE_LIMIT_DEFAULT 6000 No Default rate limit per TTL window
RATE_LIMIT_WEBHOOK 100 No Webhook endpoint rate limit

Push Notifications (PWA)

Variable Default / Value Secret? Notes
VAPID_PUBLIC_KEY (generated VAPID key) No Web Push encryption
VAPID_PRIVATE_KEY (generated VAPID key) Yes Web Push signing
VAPID_SUBJECT mailto:jan.wielemans@devgem.be No VAPID contact URL

Logging Infrastructure (ClickHouse + Grafana)

Variable Default / Value Secret? Notes
CLICKHOUSE_PASSWORD (generate: openssl rand -hex 32) Yes Password for ClickHouse otel user
GRAFANA_ADMIN_PASSWORD (generate: openssl rand -hex 32) Yes Grafana admin login password
DO_SPACES_KEY (DigitalOcean Spaces access key) Yes S3-compatible access key for log backups
DO_SPACES_SECRET (DigitalOcean Spaces secret) Yes S3-compatible secret key for log backups
DO_SPACES_REGION ams3 No DigitalOcean Spaces region
DO_SPACES_BUCKET forma3d-space No DigitalOcean Spaces bucket name
DO_SPACES_LOG_PREFIX forma3d-logging No Subfolder within the bucket for log backups

Database Administration

Variable Default / Value Secret? Notes
PGADMIN_DEFAULT_EMAIL admin@forma3d.be No pgAdmin login email
PGADMIN_DEFAULT_PASSWORD (secure password) Yes pgAdmin login password

Seeding & Testing

Variable Default / Value Secret? Notes
SEED_ADMIN_EMAIL admin@forma3d.be No Admin user created during DB seed
SEED_ADMIN_PASSWORD (secure password) Yes Admin user password for DB seed
TEST_USER_EMAIL admin@forma3d.be No Must match SEED_ADMIN_EMAIL
TEST_USER_PASSWORD (must match SEED_ADMIN_PASSWORD) Yes Must match SEED_ADMIN_PASSWORD

Acceptance Test Credentials (RBAC)

The acceptance tests authenticate against the web UI using RBAC credentials. These must match the admin user created during database seeding:

Variable Description Current Staging Value
TEST_USER_EMAIL Email address of the test admin user admin@forma3d.be
TEST_USER_PASSWORD Password for the test admin user (must match SEED_ADMIN_PASSWORD) Admin1234!
SEED_ADMIN_EMAIL Email used when seeding the admin user during initial database setup admin@forma3d.be
SEED_ADMIN_PASSWORD Password used when seeding the admin user during initial database setup Admin1234!

Current Staging Configuration (as of 2026-01-25):

SEED_ADMIN_EMAIL=admin@forma3d.be
SEED_ADMIN_PASSWORD=Admin1234!
TEST_USER_EMAIL=admin@forma3d.be
TEST_USER_PASSWORD=Admin1234!

Important:

  • TEST_USER_EMAIL and SEED_ADMIN_EMAIL must have the same value
  • TEST_USER_PASSWORD and SEED_ADMIN_PASSWORD must have the same value

The seed script uses SEED_ADMIN_* variables to create the admin user, and the acceptance tests use TEST_USER_* variables to authenticate.

Initial Database Seeding:

When the database is first seeded (manually or during first deployment), the admin user is created from the SEED_ADMIN_* variables:

# On the staging server during initial setup
SEED_ADMIN_EMAIL=admin@forma3d.be \
SEED_ADMIN_PASSWORD=Admin1234! \
npx prisma db seed

Default values (if environment variables not set):

  • Email: admin@forma3d.local
  • Password: Admin123!

For staging, we use the explicitly configured values above.

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 3 No Prisma database connection pool size per service
DATABASE_CONNECT_TIMEOUT_SECONDS 5 No Database connection timeout in seconds
DATABASE_POOL_TIMEOUT_SECONDS 10 No Timeout waiting for pool connection in seconds

Webhook and Backfill Configuration (Phase 5n - Resilience)

These variables control webhook handling and backfill/reconciliation services:

Variable Default Secret? Description
SHOPIFY_BACKFILL_ENABLED true No Enable Shopify order backfill (catches missed orders)
SHOPIFY_BACKFILL_BATCH_SIZE 50 No Orders to fetch per backfill batch
SIMPLYPRINT_RECONCILIATION_ENABLED true No Enable SimplyPrint job status reconciliation
SENDCLOUD_WEBHOOK_SECRET - Yes HMAC secret for SendCloud webhook verification
SENDCLOUD_RECONCILIATION_ENABLED true No Enable SendCloud shipment status reconciliation

Webhook Secrets:

Service Variable How to obtain
Shopify SHOPIFY_WEBHOOK_SECRET Set to SHOPIFY_API_SECRET (app client secret). API-registered webhooks are signed with this, not the secret shown in Shopify Admin > Notifications > Webhooks.
SimplyPrint SIMPLYPRINT_WEBHOOK_SECRET Self-generated with openssl rand -hex 32, configured in SimplyPrint
SendCloud SENDCLOUD_WEBHOOK_SECRET Same as SENDCLOUD_SECRET_KEY for "Sendcloud API" integration

SendCloud Webhook Signature:

For "Sendcloud API" integrations (direct API access), Sendcloud uses your API Secret Key to sign webhooks. Set SENDCLOUD_WEBHOOK_SECRET to the same value as SENDCLOUD_SECRET_KEY.

Note: Other Sendcloud integration types (e.g., Shopify, WooCommerce connectors) may have a separate "Webhook Signature Key" field in their configuration.

Example: MySecretKey123!@#XyZ

Backfill/Reconciliation Schedule:

Service Frequency Purpose
Shopify 5 minutes Fetch any orders missed during downtime
SimplyPrint 1 minute Sync print job statuses (QUEUED/PRINTING)
SendCloud 5 minutes Sync shipment statuses (IN_TRANSIT/DELIVERED)

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 3 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
GATEWAY_IMAGE_TAG=20260215143709
ORDER_SERVICE_IMAGE_TAG=20260215143709
PRINT_SERVICE_IMAGE_TAG=20260215143709
SHIPPING_SERVICE_IMAGE_TAG=20260215143709
GRIDFLOCK_SERVICE_IMAGE_TAG=20260215143709
SLICER_IMAGE_TAG=20260215143709
WEB_IMAGE_TAG=20260215143709

# Redis (BullMQ + Sessions + Socket.IO)
REDIS_URL=redis://redis:6379

# 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=...  # Must equal SHOPIFY_API_SECRET (API-registered webhooks use the app client secret for HMAC)
SHOPIFY_API_VERSION=2026-01

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

# 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

# Storefront Origins (comma-separated, '.domain' = suffix match)
STOREFRONT_ALLOWED_ORIGINS=.myshopify.com

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

# Shopify Backfill (Phase 5n - Resilience)
SHOPIFY_BACKFILL_ENABLED=true
SHOPIFY_BACKFILL_BATCH_SIZE=50

# 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=3
# 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
├── otel-collector-config.yaml          # OpenTelemetry Collector config
├── clickhouse-config.xml               # ClickHouse server config (S3 backups)
├── clickhouse-users.xml                # ClickHouse user config
├── grafana/
│   └── provisioning/
│       └── datasources/
│           └── clickhouse.yaml         # Grafana ClickHouse datasource
└── scripts/
    └── backup-clickhouse-logs.sh       # Daily ClickHouse backup script

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. Validate & Test (Lint + TypeCheck + Unit Tests in parallel) → Build & Package (Detect Affected + Docker builds) → 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-traefik         traefik:v3.0                                                           Up
forma3d-redis           redis:7-alpine                                                         Up (healthy)
forma3d-gateway         registry.digitalocean.com/forma-3d/forma3d-connect-gateway:xxx         Up (healthy)
forma3d-order-service   registry.digitalocean.com/forma-3d/forma3d-connect-order-service:xxx   Up (healthy)
forma3d-print-service   registry.digitalocean.com/forma-3d/forma3d-connect-print-service:xxx   Up (healthy)
forma3d-shipping-service registry.digitalocean.com/forma-3d/forma3d-connect-shipping-service:xxx Up (healthy)
forma3d-gridflock-service registry.digitalocean.com/forma-3d/forma3d-connect-gridflock-service:xxx Up (healthy)
forma3d-slicer          registry.digitalocean.com/forma-3d/forma3d-connect-slicer:xxx          Up (healthy)
forma3d-web             registry.digitalocean.com/forma-3d/forma3d-connect-web:xxx             Up (healthy)
forma3d-otel-collector  otel/opentelemetry-collector-contrib:latest                             Up
forma3d-clickhouse      clickhouse/clickhouse-server:24-alpine                                  Up (healthy)
forma3d-grafana         grafana/grafana-oss:latest                                              Up
forma3d-uptime-kuma     louislam/uptime-kuma:1                                                 Up
forma3d-dozzle          amir20/dozzle:latest                                                   Up

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

Database Connection Pool Management

The DigitalOcean managed PostgreSQL instance has a limited max_connections setting (25 on the Basic plan). Since 5 services share this database, connection pool sizing is critical.

Connection Budget

Consumer Connections Notes
gateway 3 Auth, sessions, permissions
order-service 3 Order CRUD, Shopify sync
print-service 3 Print job management
shipping-service 3 Shipment tracking
gridflock-service 3 STL pipeline
pg_cron 1 Internal scheduler
DigitalOcean internal ~2 Managed DB overhead
pgAdmin (on-demand) 3-5 Only when running
Total 21-23 of 25 max

Pool Size Formula

pool_size_per_service = (max_connections - reserved - headroom) / num_services

For staging: (25 - 3 - 7) / 5 = 3

Each service's DATABASE_POOL_SIZE is set to 3 in docker-compose.yml. This is enforced via the Prisma connection_limit query parameter appended to DATABASE_URL at startup.

Symptoms of Connection Exhaustion

  • FATAL: remaining connection slots are reserved for roles with the SUPERUSER attribute
  • Services fail health checks
  • pgAdmin cannot connect

Recovery

# Check current connection usage
docker run --rm --network forma3d-network postgres:15-alpine \
  psql "$DATABASE_URL" -c "SELECT usename, application_name, state, count(*) FROM pg_stat_activity GROUP BY 1,2,3 ORDER BY 4 DESC;"

# Restart services to reclaim leaked connections
cd /opt/forma3d && docker compose restart gateway order-service print-service shipping-service gridflock-service

Scaling

If upgrading the DO database plan (higher max_connections), increase DATABASE_POOL_SIZE accordingly in docker-compose.yml for each service.

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/or "Acceptance Test" as skipped.

Common causes:

  1. Nothing to deployDeploy Staging runs only on main when at least one app (web, docs, EventCatalog, gateway, services, slicer) is affected. If Nx affected says no deployable app changed, deploy is skipped by design.
  2. Acceptance Test stage condition — The stage runs only when Build succeeded and either (a) Deploy Staging succeeded and a runtime app was affected (gateway, web, order/print/shipping/gridflock services, slicer — not docs or EventCatalog), or (b) Deploy Staging was skipped and acceptance tests were the only affected area (Gherkin against existing staging). So for a docs-only or EventCatalog-only deploy, Acceptance Test is usually Skipped; that is expected.
  3. Missing dependsOn — Stage conditions that read dependencies.Build.outputs[...] must list Build in dependsOn (and DeployStaging when evaluating deploy results).

Attest Staging can still run on the docs/EventCatalog path when Deploy Staging succeeded and signing is enabled; see Pipeline Reference — Attest Staging Promotion.

Example — DeployStaging must depend on Build to use DetectAffected outputs:

# ❌ Wrong - cannot access Build outputs from another stage
- stage: DeployStaging
  dependsOn: SomeOtherStage
  condition: eq(dependencies.Build.outputs['DetectAffected.affected.webAffected'], 'true')

# ✅ Correct
- stage: DeployStaging
  dependsOn: Build
  condition: and(succeeded('Build'), eq(dependencies.Build.outputs['DetectAffected.affected.webAffected'], 'true'))

Stages and their dependsOn (see azure-pipelines.yml for the full graph):

Stage Includes in dependsOn (minimum)
DeployStaging Build
AcceptanceTest Build, DeployStaging
AttestStaging Build, DeployStaging, AcceptanceTest, LighthouseAudit, RegistryMaintenance
DeployProduction Build, AcceptanceTest, AttestStaging, LoadTest (when production stage is enabled)

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

ClickHouse Log Backups

ClickHouse stores OpenTelemetry logs in the otel.otel_logs table. A daily backup job runs at 03:00 UTC via cron, performing two operations:

  1. Binary backup — Full backup on Sundays, incremental on weekdays. Stored on the s3_backups disk (DigitalOcean Spaces) for fast restore via RESTORE.
  2. JSONL export — Human-readable daily export of yesterday's logs, compressed with gzip. Stored at forma3d-space/forma3d-logging/exports/<date>.jsonl.gz.

Files:

Location Path
Backup script (repo) deployment/staging/scripts/backup-clickhouse-logs.sh
Backup script (server) /opt/forma3d/scripts/backup-clickhouse-logs.sh
Backup log (server) /var/log/clickhouse-backup.log
JSONL exports (S3) forma3d-space/forma3d-logging/exports/
Binary backups (S3) forma3d-space/forma3d-logging/backup/full/ and .../backup/incremental/

Cron setup is handled automatically by: - The CI/CD pipeline (azure-pipelines.yml, Step 9 in the deploy heredoc) - The manual deploy script (deploy.sh, Step 1)

Both use the same idempotent pattern: remove any existing cron line, then add it fresh.

Verifying the cron is active:

crontab -l | grep backup-clickhouse
# Expected: 0 3 * * * /opt/forma3d/scripts/backup-clickhouse-logs.sh >> /var/log/clickhouse-backup.log 2>&1

Checking backup history:

# Recent backup log entries
tail -20 /var/log/clickhouse-backup.log

# ClickHouse internal backup log
docker exec forma3d-clickhouse clickhouse-client --query "SELECT * FROM system.backup_log ORDER BY event_time DESC LIMIT 10"

# Data per day (to verify exports cover all days)
docker exec forma3d-clickhouse clickhouse-client --query "SELECT toDate(Timestamp) as day, count() FROM otel.otel_logs GROUP BY day ORDER BY day"

Manual catch-up export (if the cron was missing and you need to export missed days):

# On the server — export a specific day
source /opt/forma3d/.env
DATE="2026-03-01"           # the day to export
NEXT_DAY="2026-03-02"       # day after
docker exec forma3d-clickhouse clickhouse-client --query "
  INSERT INTO FUNCTION s3(
    'https://${DO_SPACES_BUCKET}.${DO_SPACES_REGION}.digitaloceanspaces.com/${DO_SPACES_LOG_PREFIX}/exports/${DATE}.jsonl.gz',
    '${DO_SPACES_KEY}', '${DO_SPACES_SECRET}',
    'JSONEachRow',
    'Timestamp DateTime64(9), SeverityText String, ServiceName String, Body String, TraceId String, SpanId String, LogAttributes Map(String,String), ResourceAttributes Map(String,String)',
    'gzip')
  SELECT Timestamp, SeverityText, ServiceName, Body, TraceId, SpanId, LogAttributes, ResourceAttributes
  FROM otel.otel_logs
  WHERE Timestamp >= toDateTime64('${DATE} 00:00:00', 9)
    AND Timestamp < toDateTime64('${NEXT_DAY} 00:00:00', 9)
"

Restoring from binary backup:

# Restore a full backup
docker exec forma3d-clickhouse clickhouse-client --query \
  "RESTORE TABLE otel.otel_logs FROM Disk('s3_backups', 'full/full_2026-03-09/')"

Required environment variables (set in Azure DevOps variable group forma3d-staging):

Variable Purpose
DO_SPACES_KEY S3-compatible access key
DO_SPACES_SECRET S3-compatible secret key
DO_SPACES_REGION Spaces region (default: ams3)
DO_SPACES_BUCKET Spaces bucket (default: forma3d-space)
DO_SPACES_LOG_PREFIX Subfolder prefix (default: forma3d-logging)

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

pgAdmin - Web-Based Database Administration (On-Demand)

pgAdmin provides a web-based interface for managing the PostgreSQL database without SSH access or desktop tools. It runs as an on-demand container — start it from the Developer Tools settings page when needed, stop it when done.

Access URL: https://staging-connect-db.forma3d.be (only accessible when the container is running)

Starting and Stopping pgAdmin

pgAdmin is managed via the web UI or CLI:

  • UI: Navigate to Settings → Developer Tools and click Start pgAdmin / Stop pgAdmin
  • CLI (on the staging server):
# Start
cd /opt/forma3d/deployment/staging
docker compose -f docker-compose.pgadmin.yml up -d

# Stop
docker compose -f docker-compose.pgadmin.yml down

# Status
docker compose -f docker-compose.pgadmin.yml ps

Note: pgAdmin is not monitored by Uptime Kuma since it is intentionally stopped most of the time. The pgadmin-data volume is preserved across stop/start cycles — saved server connections, queries, and settings persist.

Credentials

Credential Type Where to Find
pgAdmin Login Email PGADMIN_DEFAULT_EMAIL in Azure DevOps variable group
pgAdmin Login Password PGADMIN_DEFAULT_PASSWORD in Azure DevOps variable group
Database Credentials DATABASE_URL in Azure DevOps variable group

Important: pgAdmin login credentials are separate from database credentials. You log into pgAdmin with PGADMIN_DEFAULT_EMAIL/PGADMIN_DEFAULT_PASSWORD, then configure the database connection using the database credentials from DATABASE_URL.

First-Time Setup

  1. Start pgAdmin via Settings → Developer Tools or CLI
  2. Navigate to https://staging-connect-db.forma3d.be
  3. Log in with your pgAdmin credentials (PGADMIN_DEFAULT_EMAIL / PGADMIN_DEFAULT_PASSWORD)
  4. Right-click ServersRegisterServer...
  5. In the General tab:
  6. Name: Forma3D Staging
  7. In the Connection tab:
  8. Host name/address: db-postgresql-ams3-forma3d-staging-do-user-1196005-0.m.db.ondigitalocean.com
  9. Port: 25060
  10. Maintenance database: defaultdb
  11. Username: doadmin
  12. Password: (from DATABASE_URL in Azure DevOps variable group)
  13. Check Save password
  14. In the SSL tab:
  15. SSL mode: Require
  16. Click Save

Parsing DATABASE_URL

The DATABASE_URL format is:

postgresql://USERNAME:PASSWORD@HOST:PORT/DATABASE?sslmode=require&sslrootcert=...

Example:

Current Staging DATABASE_URL:

postgresql://doadmin:<PASSWORD>@db-postgresql-ams3-forma3d-staging-do-user-1196005-0.m.db.ondigitalocean.com:25060/defaultdb?sslmode=require
Component Staging Value
Username doadmin
Password (stored in Azure DevOps forma3d-staging variable group)
Host db-postgresql-ams3-forma3d-staging-do-user-1196005-0.m.db.ondigitalocean.com
Port 25060
Database defaultdb

Common Tasks in pgAdmin

Task How To
Browse table data Servers → Forma3D Staging → Databases → defaultdb → Schemas → public → Tables → Right-click → View/Edit Data
Run SQL query Tools → Query Tool (or press Alt+Shift+Q)
View table structure Right-click table → Properties
Export data Right-click table → Import/Export Data
View active connections Dashboard → Server Activity
Check database size Right-click database → Properties → Statistics

Troubleshooting pgAdmin

Issue Solution
Cannot access pgAdmin URL Start the container first via Developer Tools or CLI
Cannot connect to database Verify DATABASE_URL credentials, check SSL mode is "Require"
"Connection refused" Ensure host is the external hostname, not localhost
Forgot pgAdmin password Reset by removing pgadmin-data volume and restarting
# Reset pgAdmin (removes all saved connections!)
docker compose -f docker-compose.pgadmin.yml down
docker volume rm pgadmin-data
docker compose -f docker-compose.pgadmin.yml up -d

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 -
Grafana (Logs) https://staging-connect-grafana.forma3d.be -
pgAdmin (DB) https://staging-connect-db.forma3d.be -

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: 2.0
Last Updated: Microservice Architecture — Updated for multi-service deployment with Redis, Slicer, GridFlock, Uptime Kuma, and Dozzle
Maintainer: AI-generated, review before production use