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 services (Gateway, Order, Print, Shipping, GridFlock, Slicer, Web)
- 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 services)
- Starts Redis before backend services (required for BullMQ events and sessions)
- Runs database migrations before starting containers (correct order for safety)
- Configures TLS certificates via Let's Encrypt
Trigger: Push to main branch
Deployment Order¶
Services must start in the correct order:
- Redis — Required by all backend services (BullMQ, sessions, Socket.IO)
- Logging stack — ClickHouse, OTel Collector, Grafana (receives logs from all services)
- Slicer — Required by GridFlock Service
- Backend services — Gateway, Order, Print, Shipping, GridFlock (can start in parallel)
- Web — Frontend (depends on Gateway being available)
- Monitoring — Uptime Kuma, Dozzle
Conditional Deployment¶
The pipeline uses Nx affected to avoid unnecessary Docker builds and deployments:
| Change Type | Gateway | Order | 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:
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:
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_EMAILandSEED_ADMIN_EMAILmust have the same valueTEST_USER_PASSWORDandSEED_ADMIN_PASSWORDmust have the same valueThe seed script uses
SEED_ADMIN_*variables to create the admin user, and the acceptance tests useTEST_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 inmailto: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 |
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 |
- 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
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¶
- Push a change to the
developbranch - Monitor the pipeline in Azure DevOps
- The pipeline will:
- 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:
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-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:
- 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/or "Acceptance Test" as skipped.
Common causes:
- Nothing to deploy —
Deploy Stagingruns only onmainwhen 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. - Acceptance Test stage condition — The stage runs only when
Buildsucceeded 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. - Missing
dependsOn— Stage conditions that readdependencies.Build.outputs[...]must listBuildindependsOn(andDeployStagingwhen 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 10MBmax-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:
- Binary backup — Full backup on Sundays, incremental on weekdays. Stored on the
s3_backupsdisk (DigitalOcean Spaces) for fast restore viaRESTORE. - 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-datavolume 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 fromDATABASE_URL.
First-Time Setup¶
- Start pgAdmin via Settings → Developer Tools or CLI
- Navigate to https://staging-connect-db.forma3d.be
- Log in with your pgAdmin credentials (
PGADMIN_DEFAULT_EMAIL/PGADMIN_DEFAULT_PASSWORD) - Right-click Servers → Register → Server...
- In the General tab:
- Name:
Forma3D Staging - In the Connection tab:
- Host name/address:
db-postgresql-ams3-forma3d-staging-do-user-1196005-0.m.db.ondigitalocean.com - Port:
25060 - Maintenance database:
defaultdb - Username:
doadmin - Password: (from DATABASE_URL in Azure DevOps variable group)
- Check Save password
- In the SSL tab:
- SSL mode:
Require - 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