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.ymlThe Dokploy compose stack to use for both staging and productiondeploy/dokploy/docker-compose.source-build.ymlOptional source-build fallback, not the default deployment pathdeploy/dokploy/.env.exampleEnvironment template for Dokploy's Environment tabdocker-compose.production.ymlRoot-level compatibility copy for the checked-in Dokploy workflowenv.production.exampleRoot-level deployment env template for GitHub, Dokploy, and Convexapps/app/DockerfileProduction image for the app and tenant portal surfaceapps/marketing/DockerfileProduction image for the public marketing app.github/workflows/ci.ymlCI on pull requests and on pushes tostagingandmain.github/workflows/staging-images.ymlInstalls the workspace, deploys the staging Convex backend, and pushescontinu-app-demo,continu-web-demo, andcontinu-docs-demostaging images to GHCR frommain.github/workflows/production-release.ymlInstalls the workspace, deploys the production Convex backend, and pushes versioned production images to GHCR from release tags
Branch Model
stagingOptional future branch if you later want a separate pre-main promotion stepmainMain branch, deploys the staging Convex target and publishes:stagingimages for the serverv*.*.*tags Production release trigger, deploys the production Convex target, and publishes versioned and:latestimages
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:
starter-stagingstarter-production
Use the Dokploy compose file for both:
deploy/dokploy/docker-compose.dokploy.yml
Then configure them like this:
- Connect the GitHub repository through Dokploy's Git provider.
- Set the branch:
- staging app:
main - production app:
main
- staging app:
- Enable Dokploy Auto Deploy for each app.
- Copy the values from
deploy/dokploy/.env.exampleinto each app's Environment tab and replace them with environment-specific values. - Set image tags per environment:
- staging app:
IMAGE_TAG=staging - production app:
IMAGE_TAG=latest
- staging app:
- Set
GHCR_OWNERto the GitHub org or user that owns the package. - Run
pnpm starter:init, chooseproduction-prep, and pick theGHCR_APP_IMAGE,GHCR_WEB_IMAGE, andGHCR_DOCS_IMAGEnames this repo should publish. - Let
starter:initsync the checked-in compose manifests to those concrete package names. - If
ghis authenticated, letstarter:initwrite the matchingGHCR_*_IMAGEvalues to GitHub automatically. Otherwise set them in GitHub repository or environment variables yourself. - Run
pnpm starter:doctorand make sure the GitHub GHCR image-var check passes before relying on the release workflows. - In the Domains tab, attach domains to each service:
appservice ->app.informedenergydesign.com.au-> port3001webservice ->informedenergydesign.com.au-> port3000
- Create wildcard DNS for your tenant portal host so
*.${NEXT_PUBLIC_PORTAL_HOST}resolves to the Dokploy server. - If you need arbitrary custom domains, set up
docs/dev/deployment/traefik-router-api-setup.mdon the app server and set the matching Convex env vars. - 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.argsforNEXT_PUBLIC_*values that Next.js needs at build time
GitHub Environment Variables
Create two GitHub Environments:
stagingproduction
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 inpnpm starter:init->production-prep)GHCR_WEB_IMAGE(set this to the web image name chosen inpnpm starter:init->production-prep)GHCR_DOCS_IMAGE(set this to the docs image name chosen inpnpm starter:init->production-prep)NEXT_PUBLIC_CONVEX_URLNEXT_PUBLIC_CONVEX_SITE_URLNEXT_PUBLIC_APP_DOMAINNEXT_PUBLIC_APP_HOSTNEXT_PUBLIC_PORTAL_HOSTNEXT_PUBLIC_UMAMI_SCRIPT_URLNEXT_PUBLIC_UMAMI_WEBSITE_IDCONVEX_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:
pnpm starter:initproduction-prepasks for the GHCR image names and syncs both checked-in compose files.starter:initcan write the sameGHCR_*_IMAGEvalues to GitHub throughghwhen authenticated; otherwise set them manually.pnpm starter:doctorfails if the effective GitHub GHCR vars drift fromenv.production.local.- Dokploy runtime does not need
GHCR_APP_IMAGE,GHCR_WEB_IMAGE, orGHCR_DOCS_IMAGEonce the compose files are synced.
Required Runtime Variables Per Environment
App service:
NEXT_PUBLIC_CONVEX_URLNEXT_PUBLIC_APP_DOMAINNEXT_PUBLIC_APP_HOSTNEXT_PUBLIC_PORTAL_HOSTCONVEX_DEPLOY_KEY
Optional bootstrap-on-start envs for the app container:
BOOTSTRAP=trueSEED_DEMO=trueSEED_DEMO_PASSWORD=<stable shared demo password>DEMO_RESTRICTED_MODE=true|falseFIRST_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=trueSEED_DEMO=trueSEED_DEMO_PASSWORD=<stable shared demo password>DEMO_RESTRICTED_MODE=trueFIRST_PLATFORM_ADMIN_EMAIL=<platform admin email>FIRST_PLATFORM_ADMIN_PASSWORD=<platform admin password>
After the reset deploy finishes, set:
BOOTSTRAP=falseSEED_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_URLNEXT_PUBLIC_CONVEX_SITE_URLNEXT_PUBLIC_UMAMI_SCRIPT_URLNEXT_PUBLIC_UMAMI_WEBSITE_ID
Convex dashboard:
EMAIL_TRANSPORTMARKETING_EMAIL_TRANSPORTRESEND_API_KEYEMAIL_FROMEMAIL_REPLY_TORESEND_MARKETING_AUDIENCE_IDRESEND_MARKETING_AUDIENCE_NAMEAUTH_APP_BASE_URLOPS_NOTIFICATION_EMAILS
Recommended Dokploy Watch Paths
If you use watch paths, include:
apps/app/**apps/marketing/**packages/**deploy/dokploy/**pnpm-lock.yamlpnpm-workspace.yamlturbo.jsonpackage.json
Rollout
- Create GitHub Environments named
stagingandproduction. - Add the
NEXT_PUBLIC_*build variables andCONVEX_DEPLOY_KEYsecret used by the staging and production release workflows. - Merge the workflow and push to
main. - 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, andghcr.io/<owner>/continu-docs-demo:staging. - Create the Dokploy staging compose app with
deploy/dokploy/docker-compose.dokploy.yml. - Create a production release tag such as
v1.0.0. - Confirm the production workflow deploys the Convex backend and GHCR receives versioned production images plus
:latestfor app, web, and docs.