Skip to main content

Dokploy CI/CD

This repo now uses a split responsibility model:

  • GitHub Actions handles CI
  • GitHub Actions builds and pushes deployable images to GHCR
  • Dokploy handles runtime deployment with Docker Compose

That keeps build concerns in GitHub and runtime concerns in Dokploy.

Files

  • deploy/dokploy/docker-compose.dokploy.yml The Dokploy compose stack to use for both staging and production
  • deploy/dokploy/docker-compose.source-build.yml Optional source-build fallback, not the default deployment path
  • deploy/dokploy/.env.example Environment template for Dokploy's Environment tab
  • docker-compose.production.yml Root-level compatibility copy for the checked-in Dokploy workflow
  • env.production.example Root-level deployment env template for GitHub, Dokploy, and Convex
  • apps/app/Dockerfile Production image for the app and tenant portal surface
  • apps/marketing/Dockerfile Production image for the public marketing app
  • .github/workflows/ci.yml CI on pull requests and on pushes to staging and main
  • .github/workflows/staging-images.yml Installs the workspace, deploys the staging Convex backend, and pushes continu-app-demo, continu-web-demo, and continu-docs-demo staging images to GHCR from main
  • .github/workflows/production-release.yml Installs the workspace, deploys the production Convex backend, and pushes versioned production images to GHCR from release tags

Branch Model

  • staging Optional future branch if you later want a separate pre-main promotion step
  • main Main branch, deploys the staging Convex target and publishes :staging images for the server
  • v*.*.* tags Production release trigger, deploys the production Convex target, and publishes versioned and :latest images

If you prefer different branch names, update Dokploy's Git settings and .github/workflows/ci.yml.

GitHub CI

The CI workflow runs:

  • dependency install
  • lint
  • typecheck
  • app unit tests
  • monorepo build
  • Dokploy compose validation

The existing .github/workflows/admin-e2e.yml remains the browser-level check.

Dokploy Setup

Create two Dokploy Docker Compose apps from the same repository:

  1. starter-staging
  2. starter-production

Use the Dokploy compose file for both:

  • deploy/dokploy/docker-compose.dokploy.yml

Then configure them like this:

  1. Connect the GitHub repository through Dokploy's Git provider.
  2. Set the branch:
    • staging app: main
    • production app: main
  3. Enable Dokploy Auto Deploy for each app.
  4. Copy the values from deploy/dokploy/.env.example into each app's Environment tab and replace them with environment-specific values.
  5. Set image tags per environment:
    • staging app: IMAGE_TAG=staging
    • production app: IMAGE_TAG=latest
  6. Set GHCR_OWNER to the GitHub org or user that owns the package.
  7. Run pnpm starter:init, choose production-prep, and pick the GHCR_APP_IMAGE, GHCR_WEB_IMAGE, and GHCR_DOCS_IMAGE names this repo should publish.
  8. Let starter:init sync the checked-in compose manifests to those concrete package names.
  9. If gh is authenticated, let starter:init write the matching GHCR_*_IMAGE values to GitHub automatically. Otherwise set them in GitHub repository or environment variables yourself.
  10. Run pnpm starter:doctor and make sure the GitHub GHCR image-var check passes before relying on the release workflows.
  11. In the Domains tab, attach domains to each service:
  • app service -> app.informedenergydesign.com.au -> port 3001
  • web service -> informedenergydesign.com.au -> port 3000
  1. Create wildcard DNS for your tenant portal host so *.${NEXT_PUBLIC_PORTAL_HOST} resolves to the Dokploy server.
  2. If you need arbitrary custom domains, set up docs/dev/deployment/traefik-router-api-setup.md on the app server and set the matching Convex env vars.
  3. Redeploy after domain changes so Dokploy can apply the routing config.

The app service compose file also publishes a Traefik service named starter-app. The checked-in Python router helper targets starter-app@docker by default, so custom-domain automation stays aligned with the compose manifests.

Environment Notes

Dokploy writes Environment tab values into a local .env file next to the compose file. Those values are not injected into containers unless the compose file references them, which is why this stack uses both:

  • env_file: .env
  • explicit environment: entries
  • build.args for NEXT_PUBLIC_* values that Next.js needs at build time

GitHub Environment Variables

Create two GitHub Environments:

  • staging
  • production

Add these repository or environment-level variables and secrets so the release workflows can deploy Convex and build the images with the correct public config:

  • GHCR_OWNER (optional; defaults to the repository owner)
  • GHCR_APP_IMAGE (set this to the app image name chosen in pnpm starter:init -> production-prep)
  • GHCR_WEB_IMAGE (set this to the web image name chosen in pnpm starter:init -> production-prep)
  • GHCR_DOCS_IMAGE (set this to the docs image name chosen in pnpm starter:init -> production-prep)
  • NEXT_PUBLIC_CONVEX_URL
  • NEXT_PUBLIC_CONVEX_SITE_URL
  • NEXT_PUBLIC_APP_DOMAIN
  • NEXT_PUBLIC_APP_HOST
  • NEXT_PUBLIC_PORTAL_HOST
  • NEXT_PUBLIC_UMAMI_SCRIPT_URL
  • NEXT_PUBLIC_UMAMI_WEBSITE_ID
  • CONVEX_DEPLOY_KEY (secret)

The marketing app now uses a same-origin /api/marketing/newsletter route, so no separate public newsletter URL env is needed. Leave NEXT_PUBLIC_UMAMI_SCRIPT_URL and NEXT_PUBLIC_UMAMI_WEBSITE_ID blank to disable analytics entirely.

The intended split is:

  1. pnpm starter:init production-prep asks for the GHCR image names and syncs both checked-in compose files.
  2. starter:init can write the same GHCR_*_IMAGE values to GitHub through gh when authenticated; otherwise set them manually.
  3. pnpm starter:doctor fails if the effective GitHub GHCR vars drift from env.production.local.
  4. Dokploy runtime does not need GHCR_APP_IMAGE, GHCR_WEB_IMAGE, or GHCR_DOCS_IMAGE once the compose files are synced.

Required Runtime Variables Per Environment

App service:

  • NEXT_PUBLIC_CONVEX_URL
  • NEXT_PUBLIC_APP_DOMAIN
  • NEXT_PUBLIC_APP_HOST
  • NEXT_PUBLIC_PORTAL_HOST
  • CONVEX_DEPLOY_KEY

Optional bootstrap-on-start envs for the app container:

  • BOOTSTRAP=true
  • SEED_DEMO=true
  • SEED_DEMO_PASSWORD=<stable shared demo password>
  • DEMO_RESTRICTED_MODE=true|false
  • FIRST_PLATFORM_ADMIN_EMAIL=<platform admin email>
  • FIRST_PLATFORM_ADMIN_PASSWORD=<platform admin password>
  • CONVEX_DEPLOY_KEY=<deploy key> for Convex Cloud

For self-hosted Convex only:

  • CONVEX_SELF_HOSTED_URL=<deployment url>
  • CONVEX_SELF_HOSTED_ADMIN_KEY=<admin key>

BOOTSTRAP=true is the destructive first-time provisioning mode. It deploys the Convex backend, wipes the current Convex target, and bootstraps the first platform admin from FIRST_PLATFORM_ADMIN_EMAIL and FIRST_PLATFORM_ADMIN_PASSWORD. Leave it off during normal restarts once the environment is provisioned.

SEED_DEMO=true always reseeds the demo fixture set. When paired with BOOTSTRAP=true, it seeds into the deployment that BOOTSTRAP already wiped. When used on its own, it clears the current demo data before reseeding so old demo fixtures do not linger.

Set SEED_DEMO_PASSWORD whenever SEED_DEMO=true is used in a hosted environment. The startup reseed path is non-interactive, so without an explicit password it would otherwise generate a fresh random password and rotate every shared demo login on the next deploy.

DEMO_RESTRICTED_MODE=true forces the hosted demo restrictions on at startup. DEMO_RESTRICTED_MODE=false forces them off. Leave it unset to avoid changing the current backend demo-mode flag. This is the simplest Dokploy runtime toggle when you just want to lock or unlock the shared demo without reseeding it.

For Convex Cloud, the app container must receive CONVEX_DEPLOY_KEY for bootstrap or reseed work. NEXT_PUBLIC_CONVEX_URL only points the app at the deployment; it does not authorize CLI provisioning commands.

For normal Convex Cloud deployments, leave the self-hosted variables unset. The bootstrap script will use the active Convex Cloud deployment configured for the backend package.

Recommended tester reset env block:

  • BOOTSTRAP=true
  • SEED_DEMO=true
  • SEED_DEMO_PASSWORD=<stable shared demo password>
  • DEMO_RESTRICTED_MODE=true
  • FIRST_PLATFORM_ADMIN_EMAIL=<platform admin email>
  • FIRST_PLATFORM_ADMIN_PASSWORD=<platform admin password>

After the reset deploy finishes, set:

  • BOOTSTRAP=false
  • SEED_DEMO=false

Keep DEMO_RESTRICTED_MODE=true if that Dokploy environment should stay locked down as a shared demo. Flip it to false when you want to temporarily unlock the demo for editing.

Web service:

  • NEXT_PUBLIC_CONVEX_URL
  • NEXT_PUBLIC_CONVEX_SITE_URL
  • NEXT_PUBLIC_UMAMI_SCRIPT_URL
  • NEXT_PUBLIC_UMAMI_WEBSITE_ID

Convex dashboard:

  • EMAIL_TRANSPORT
  • MARKETING_EMAIL_TRANSPORT
  • RESEND_API_KEY
  • EMAIL_FROM
  • EMAIL_REPLY_TO
  • RESEND_MARKETING_AUDIENCE_ID
  • RESEND_MARKETING_AUDIENCE_NAME
  • AUTH_APP_BASE_URL
  • OPS_NOTIFICATION_EMAILS

If you use watch paths, include:

  • apps/app/**
  • apps/marketing/**
  • packages/**
  • deploy/dokploy/**
  • pnpm-lock.yaml
  • pnpm-workspace.yaml
  • turbo.json
  • package.json

Rollout

  1. Create GitHub Environments named staging and production.
  2. Add the NEXT_PUBLIC_* build variables and CONVEX_DEPLOY_KEY secret used by the staging and production release workflows.
  3. Merge the workflow and push to main.
  4. Confirm the staging workflow deploys the Convex backend and GHCR receives ghcr.io/<owner>/continu-app-demo:staging, ghcr.io/<owner>/continu-web-demo:staging, and ghcr.io/<owner>/continu-docs-demo:staging.
  5. Create the Dokploy staging compose app with deploy/dokploy/docker-compose.dokploy.yml.
  6. Create a production release tag such as v1.0.0.
  7. Confirm the production workflow deploys the Convex backend and GHCR receives versioned production images plus :latest for app, web, and docs.