Skip to content

Full-Stack Application

This example deploys a complete task management application from a single monorepo — API, frontend, background worker, managed databases, and S3 storage.

ServiceRoleSubpath
taskflow-apiNode.js REST APIbackend/
taskflow-frontendFrontend web appfrontend/
taskflow-workerBackground job processorworker/
taskflow-dbManaged PostgreSQL
taskflow-cacheManaged Redis
taskflow-uploadsS3 object storage bucket

Step 1 — Check quota before starting

Three services require 3 × 100m CPU and 3 × 128Mi memory.

bash
cpctl quota
# Plan: basic-1-2-50 (basic)
#
# ┌──────────┬────────┬──────────┬─────┐
# │ RESOURCE │ USED   │ LIMIT    │ PCT │
# ├──────────┼────────┼──────────┼─────┤
# │ CPU      │ 0m     │ 1000m    │  0% │
# │ Memory   │ 0 MiB  │ 2048 MiB │  0% │
# │ Storage  │ —      │ 50 GiB   │  —  │
# └──────────┴────────┴──────────┴─────┘

Step 2 — Provision managed databases

Create Postgres and Redis in the same region the services will run in.

bash
cpctl db create postgres \
  --name taskflow-db \
  --region eu-west \
  --size 20

# ✓ Created postgres database "taskflow-db"
#   name    taskflow-db
#   type    postgres
#   host    db-taskflow-db.compute-portal.svc.cluster.local
#   port    5432
#   region  eu-west
#   url     postgres://admin:***@db-taskflow-db.compute-portal.svc.cluster.local:5432/taskflow-db

cpctl db create redis \
  --name taskflow-cache \
  --region eu-west \
  --size 5

# ✓ Created redis database "taskflow-cache"
#   name    taskflow-cache
#   type    redis
#   host    db-taskflow-cache.compute-portal.svc.cluster.local
#   port    6379
#   region  eu-west
#   url     redis://:***@db-taskflow-cache.compute-portal.svc.cluster.local:6379

cpctl db list
# ┌────────────────┬──────────┬─────────┬─────────┬──────┐
# │ NAME           │ TYPE     │ STATUS  │ REGION  │ SIZE │
# ├────────────────┼──────────┼─────────┼─────────┼──────┤
# │ taskflow-cache │ redis    │ running │ eu-west │ 10GB │
# │ taskflow-db    │ postgres │ running │ eu-west │ 10GB │
# └────────────────┴──────────┴─────────┴─────────┴──────┘

# Store connection URLs for later use
DB_URL=$(cpctl db url taskflow-db)
REDIS_URL=$(cpctl db url taskflow-cache)

Step 3 — Create S3 bucket for file uploads

Bucket names are prefixed with a tenant ID to ensure global uniqueness.

bash
cpctl bucket create taskflow-uploads

# ✓ Bucket <tenant-id>-taskflow-uploads created
#   Bucket      <tenant-id>-taskflow-uploads
#   Endpoint    https://s3-cli.computeportal.io
#   Access Key  <access-key>
#   Secret Key  <secret-key>
#   Region      eu-fsn1

cpctl bucket list
# ┌──────────────────────────────────┬─────────────────────────────────┐
# │ BUCKET                           │ ENDPOINT                        │
# ├──────────────────────────────────┼─────────────────────────────────┤
# │ <tenant-id>-taskflow-uploads     │ https://s3-cli.computeportal.io │
# └──────────────────────────────────┴─────────────────────────────────┘

BUCKET_ENDPOINT=https://s3-cli.computeportal.io
BUCKET_NAME=<tenant-id>-taskflow-uploads
BUCKET_ACCESS_KEY=<access-key>
BUCKET_SECRET_KEY=<secret-key>

Step 4 — Deploy the API

Deploy from the backend/ subpath.

bash
cpctl deploy \
  --repo https://github.com/your-org/taskflow \
  --subpath backend \
  --name taskflow-api \
  --port 4000

# → Deploying https://github.com/your-org/taskflow...
# ✓ Deployed taskflow-api
#   service  taskflow-api
#   url      https://taskflow-api-<hash>.cpctl.app
#   status   building
#   port     4000 (from --port flag)
#
# Building taskflow-api — job: cp-build-taskflow-api-...
# [INFO] RUN npm i
# [INFO] CMD ["npm run start"]
# [INFO] Pushed registry.example.com/cp-cli/taskflow-api:latest
# → Build complete. Service available at: https://taskflow-api-<hash>.cpctl.app

Step 5 — Inject environment variables into the API

Attach the database (auto-injects DATABASE_URL and service-specific vars), then set the rest manually.

bash
# Attach Postgres — injects DATABASE_URL, POSTGRES_URL, TASKFLOW_DB_URL
cpctl db attach taskflow-db taskflow-api

# ✓ Attached taskflow-db → taskflow-api
#
#   Injected into taskflow-api:
#     DATABASE_URL=postgres://admin:***@db-taskflow-db.compute-portal.svc.cluster.local:5432/taskflow-db
#     POSTGRES_URL=postgres://admin:***@db-taskflow-db.compute-portal.svc.cluster.local:5432/taskflow-db
#     TASKFLOW_DB_URL=postgres://admin:***@db-taskflow-db.compute-portal.svc.cluster.local:5432/taskflow-db
#
#   Redeploy the service:
#     cpctl redeploy taskflow-api

# Set remaining env vars
cpctl env set taskflow-api \
  REDIS_URL="$REDIS_URL" \
  BUCKET_ENDPOINT="$BUCKET_ENDPOINT" \
  BUCKET_NAME="$BUCKET_NAME" \
  BUCKET_ACCESS_KEY="$BUCKET_ACCESS_KEY" \
  BUCKET_SECRET_KEY="$BUCKET_SECRET_KEY" \
  NODE_ENV=production

# Rolling restart to pick up all new vars
cpctl redeploy taskflow-api

# Verify
cpctl env list taskflow-api
# ┌───────────────────┬────────────────────────────────────────────────────────────┐
# │ KEY               │ VALUE                                                      │
# ├───────────────────┼────────────────────────────────────────────────────────────┤
# │ DATABASE_URL      │ postgres://admin:***@db-taskflow-db...svc.cluster.local/…  │
# │ POSTGRES_URL      │ postgres://admin:***@db-taskflow-db...svc.cluster.local/…  │
# │ TASKFLOW_DB_URL   │ postgres://admin:***@db-taskflow-db...svc.cluster.local/…  │
# │ REDIS_URL         │ redis://:***@db-taskflow-cache...svc.cluster.local:6379    │
# │ BUCKET_ENDPOINT   │ https://s3-cli.computeportal.io                            │
# │ BUCKET_NAME       │ <tenant-id>-taskflow-uploads                               │
# │ BUCKET_ACCESS_KEY │ <access-key>                                               │
# │ BUCKET_SECRET_KEY │ <secret-key>                                               │
# │ NODE_ENV          │ production                                                 │
# │ PORT              │ 4000                                                       │
# └───────────────────┴────────────────────────────────────────────────────────────┘

Step 6 — Deploy the frontend

The frontend needs the API URL baked in at build time via --env-var.

bash
API_URL=https://taskflow-api-<hash>.cpctl.app

cpctl deploy \
  --repo https://github.com/your-org/taskflow \
  --subpath frontend \
  --name taskflow-frontend \
  --port 3000 \
  --env-var VITE_API_URL="$API_URL" \
  --env-var NODE_ENV=production

# ✓ Deployed taskflow-frontend
#   service  taskflow-frontend
#   url      https://taskflow-frontend-<hash>.cpctl.app
#   status   building
#   port     3000 (from --port flag)

Step 7 — Deploy the background worker

The worker has no inbound HTTP — it polls Redis for jobs. Single replica, no port needed.

bash
cpctl deploy \
  --repo https://github.com/your-org/taskflow \
  --subpath worker \
  --name taskflow-worker

# ✓ Deployed taskflow-worker
#   service  taskflow-worker
#   url      https://taskflow-worker-<hash>.cpctl.app
#   status   building
#   port     3000 (injected as PORT env var)

# Attach Postgres and set Redis URL, then redeploy
cpctl db attach taskflow-db taskflow-worker
cpctl env set taskflow-worker \
  REDIS_URL="$REDIS_URL" \
  NODE_ENV=production

cpctl redeploy taskflow-worker

Step 8 — Attach custom domains

DNS must already be configured before running these commands. Add CNAMEs pointing to cpctl.app at your DNS provider first:

tf-api.example.com       CNAME  cpctl.app  (DNS only, not proxied)
tf-frontend.example.com  CNAME  cpctl.app  (DNS only, not proxied)

Then attach the domains:

bash
cpctl domain add taskflow-api      tf-api.example.com --skip-dns
cpctl domain add taskflow-frontend tf-frontend.example.com --skip-dns

# ✓ Domain tf-api.example.com attached to taskflow-api
#   domain   tf-api.example.com
#   service  taskflow-api
#   status   provisioning
#   tls      active
#
# ✓ Domain tf-frontend.example.com attached to taskflow-frontend
#   domain   tf-frontend.example.com
#   service  taskflow-frontend
#   status   provisioning
#   tls      active

Check TLS provisioning status (may take a few minutes):

bash
cpctl domain status taskflow-api tf-api.example.com
# status         active
# tls            active
# cert_verified  yes

cpctl domain status taskflow-frontend tf-frontend.example.com
# status         active
# tls            active
# cert_verified  yes

Step 9 — Switch frontend to the custom API domain

bash
cpctl env set taskflow-frontend \
  VITE_API_URL=https://tf-api.example.com

cpctl redeploy taskflow-frontend

Step 10 — Lock down with firewall rules

bash
cpctl firewall add taskflow-api --deny 198.51.100.0/24

cpctl firewall list taskflow-api
# ┌──────────────────────┬──────┬─────────────────┐
# │ RULE                 │ MODE │ CIDR            │
# ├──────────────────────┼──────┼─────────────────┤
# │ deny:198.51.100.0/24 │ deny │ 198.51.100.0/24 │
# └──────────────────────┴──────┴─────────────────┘

Step 11 — Autoscale the API

bash
cpctl scale taskflow-api --min 2 --max 10 --cpu-threshold 70

# ✓ Autoscaling enabled for taskflow-api
#   min_replicas   2
#   max_replicas   10
#   cpu_threshold  70%

# Worker scales manually
cpctl scale taskflow-worker --replicas 2

# ✓ Scaled taskflow-worker
#   replicas  2

Step 12 — Wire up CI/CD deploy hooks

bash
cpctl deploy-hook create taskflow-api
# ✓ Deploy hook created for taskflow-api
# ┌──────────────────────────────────────┬──────────────────────────────────────────────────────────────────────────────┬──────────────────────────────────────────────────────────────────┐
# │ ID                                   │ URL                                                                          │ SECRET                                                           │
# ├──────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────┤
# │ hook_<id>                            │ https://api.computeportal.io/v1/hooks/hook_<id>                              │ <secret>                                                         │
# └──────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────────────────────────┘

cpctl deploy-hook create taskflow-frontend
cpctl deploy-hook create taskflow-worker

Add to your GitHub Actions workflow:

yaml
# .github/workflows/deploy.yml
- name: Deploy API
  run: |
    curl -X POST ${{ secrets.CP_HOOK_API_URL }} \
      -H "X-Hook-Secret: ${{ secrets.CP_HOOK_API_SECRET }}"

Step 13 — Verify the full stack

bash
cpctl machine list
# ┌───────────────────┬───────────────────────────────────────────────────────────────┬─────────┬─────────┬──────────────────────────────────────────┐
# │ NAME              │ IMAGE                                                         │ STATUS  │ REGION  │ URL                                      │
# ├───────────────────┼───────────────────────────────────────────────────────────────┼─────────┼─────────┼──────────────────────────────────────────┤
# │ taskflow-api      │ registry.example.com/cp-cli/taskflow-api:latest      │ running │ eu-west │ https://tf-api.example.com               │
# │ taskflow-frontend │ registry.example.com/cp-cli/taskflow-frontend:latest │ running │ eu-west │ https://tf-frontend.example.com          │
# │ taskflow-worker   │ registry.example.com/cp-cli/taskflow-worker:latest   │ running │ eu-west │ https://taskflow-worker-<hash>.cname...  │
# └───────────────────┴───────────────────────────────────────────────────────────────┴─────────┴─────────┴──────────────────────────────────────────┘

cpctl db list
# ┌────────────────┬──────────┬─────────┬─────────┬──────┐
# │ NAME           │ TYPE     │ STATUS  │ REGION  │ SIZE │
# ├────────────────┼──────────┼─────────┼─────────┼──────┤
# │ taskflow-cache │ redis    │ running │ eu-west │ 10GB │
# │ taskflow-db    │ postgres │ running │ eu-west │ 10GB │
# └────────────────┴──────────┴─────────┴─────────┴──────┘

cpctl bucket list
# ┌──────────────────────────────────┬─────────────────────────────────┐
# │ BUCKET                           │ ENDPOINT                        │
# ├──────────────────────────────────┼─────────────────────────────────┤
# │ <tenant-id>-taskflow-uploads     │ https://s3-cli.computeportal.io │
# └──────────────────────────────────┴─────────────────────────────────┘

cpctl quota
# Plan: basic-1-2-50 (basic)
#
# ┌──────────┬─────────┬──────────┬─────┐
# │ RESOURCE │ USED    │ LIMIT    │ PCT │
# ├──────────┼─────────┼──────────┼─────┤
# │ CPU      │ 700m    │ 1000m    │ 70% │
# │ Memory   │ 896 MiB │ 2048 MiB │ 44% │
# │ Storage  │ —       │ 50 GiB   │  —  │
# └──────────┴─────────┴──────────┴─────┘

# Tail API logs
cpctl logs taskflow-api --tail 50

# Exec into the API pod for a quick DB check
cpctl exec taskflow-api -- node -e \
  "const {Pool}=require('pg'); new Pool({connectionString:process.env.DATABASE_URL}).query('SELECT NOW()').then(r=>console.log(r.rows[0])).catch(console.error)"
# { now: 2026-08-28T11:01:49.555Z }

Step 14 — Rolling redeploy and rollback

bash
# Trigger redeploy from latest main branch
cpctl redeploy taskflow-api

# View deployment history
cpctl deployment list taskflow-api
# ┌──────────┬─────────┬──────────────────────────────────────────────────────────┬─────────────────────┐
# │ ID       │ STATUS  │ IMAGE                                                    │ DEPLOYED AT         │
# ├──────────┼─────────┼──────────────────────────────────────────────────────────┼─────────────────────┤
# │ 788f6b13 │ success │ registry.example.com/cp-cli/taskflow-api:latest │ 2026-08-28T10:59:56 │
# │ 8cd18393 │ success │ registry.example.com/cp-cli/taskflow-api:latest │ 2026-08-28T08:22:21 │
# └──────────┴─────────┴──────────────────────────────────────────────────────────┴─────────────────────┘

# Roll back if something broke
cpctl rollback taskflow-api
# ✓ Rolled back taskflow-api

Teardown

bash
cpctl delete taskflow-api --yes
cpctl delete taskflow-frontend --yes
cpctl delete taskflow-worker --yes
cpctl db destroy taskflow-db --force
cpctl db destroy taskflow-cache --force
cpctl bucket delete <tenant-id>-taskflow-uploads --yes

GPU Compute Platform