Skip to main content

Concern

Deploying and hosting the application.

Technology

Digital Ocean App Platform

Documentation

Configuration

App spec defines services and static sites; backend http_port and frontend build command and output dir. See backend configuration and frontend vite for how apps read config.

Environment variables

Env vars that must be set in the deployed environment:

  • Backend: For acceptance and production, the backend service must have ENVIRONMENT as the first entry in envs (value acceptance or production, lowercase); when unset, the app treats the environment as development (see pattern environments). Other vars (e.g. NODE_ENV, PORT) follow. PORT — set by platform at run-time from http_port in app spec (e.g. 3000). APP_URL — the app’s public URL (e.g. https://your-app.ondigitalocean.app), set via CI variable substitution; the backend derives BACKEND_URL and FRONTEND_URL from it for same-origin deployment (see § Same-origin deployment). JWT_ACCESS_TOKEN_SECRET, JWT_REFRESH_TOKEN_SECRET, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET — set via CI variable substitution (see § GitLab CI/CD variables). See configuration.
  • Static site (frontend): VITE_ENVIRONMENT must be the first entry in static_sites envs (value acceptance or production), scope BUILD_TIME, followed by VITE_BACKEND_URL. So the built bundle knows the deployment environment (see pattern environments). VITE_BACKEND_URL (or equivalent) — set at BUILD_TIME in app spec (e.g. /api when backend is under same ingress). Use scope: BUILD_TIME under static_sites.*.envs.

Local dev uses a root .env at monorepo root (see pattern configuration); deployed uses only platform env and build-time injection for Vite (no root .env in production).

Same-origin deployment (VITE_BACKEND_URL and backend URL)

In same-origin deployment, the frontend and backend are served from one App Platform app behind a single ingress (e.g. / → static site, /api → backend). To ensure VITE_BACKEND_URL points to the backend in this setup:

  • Set VITE_BACKEND_URL to the relative path where the backend is mounted, e.g. /api. The ingress routes requests with that path prefix to the backend component. The browser resolves /api relative to the current origin (e.g. https://your-app.ondigitalocean.app/api), so the frontend always calls the correct backend URL without knowing the deployment domain. No per-environment URL is needed.
  • Keep the ingress rule that sends the chosen path prefix (e.g. prefix: /api) to the backend service. As long as that path matches the value of VITE_BACKEND_URL, the deployed frontend will talk to the deployed backend.
  • APP_URL: In the app spec, set backend env APP_URL to \${APP_URL}; the deploy job substitutes it from the APP_URL GitLab CI variable (the app’s public URL, e.g. https://your-app.ondigitalocean.app). The backend derives BACKEND_URL = APP_URL + /api and FRONTEND_URL = APP_URL so auth (e.g. Google OAuth callback and SSO redirect) use the configured domain.

If the backend is on a different origin (different domain or subdomain), set VITE_BACKEND_URL to the full backend URL (e.g. via CI variable and envsubst) at build time so the built bundle uses that URL.

Managed database (Postgres)

The app spec includes a databases entry (PostgreSQL, e.g. engine: PG, version: "17", production: true) so App Platform uses a managed database per environment. Both acceptance and production use production: true for the database. For production databases, cluster_name is required: in the spec set cluster_name to the placeholder \${DO_DB_CLUSTER_NAME} (placed directly after name). The deploy job must have DO_DB_CLUSTER_NAME set (job variables or GitLab CI/CD variables; use per-environment values when using GitLab environments). The value is the name of the existing Managed Database cluster in DigitalOcean (often an auto-generated ID such as app-b0a54db2-ee70-424f-ba6e-7ed9dcd43b59). Create the cluster in DigitalOcean (control panel or API) before deploy; envsubst replaces \${DO_DB_CLUSTER_NAME} with that name at deploy time. The backend receives DATABASE_URL via a bindable environment variable: in the backend service envs, set key: DATABASE_URL, scope: RUN_TIME, value: "\${<db-component-name>.DATABASE_URL}" (e.g. \${boilerplate-acc-db.DATABASE_URL} for the acceptance DB component). The database component name must match the name of the database in the same spec. Database size and tier (e.g. 2 GB / 1 vCPU for acceptance, 8 GB / 2 vCPU for production) are chosen when creating the cluster in the DigitalOcean control panel, not in the app spec. Migrations are run at startup: start:prod runs prisma migrate deploy then the app; start:acc runs prisma migrate reset --force then the app (full reset on every acceptance deploy). No need to set DATABASE_URL as a GitLab CI variable when using the managed DB—the app spec binding provides it.

Backend service instance_size_slug: acceptance uses apps-s-1vcpu-2gb (2 GB RAM, 1 shared vCPU); production uses apps-d-2vcpu-4gb (4 GB RAM, 2 dedicated vCPUs).

CI variable substitution into app spec

Deploy jobs substitute shell variables into the app spec so values (including secrets) are provided by GitLab CI/CD variables, not stored in Digital Ocean. This keeps credentials in GitLab and lets the same spec work across branches and projects.

  • Mechanism: The app spec YAML uses placeholders (e.g. \${GITLAB_BRANCH}, \${GITLAB_REPO}, \${SOURCE_DIR}, \${APP_URL}, \${DO_DB_CLUSTER_NAME}, \${JWT_ACCESS_TOKEN_SECRET}, \${GOOGLE_CLIENT_ID}, \${MICROSOFT_CLIENT_ID}). The CI script exports the needed variables, then runs envsubst so those placeholders are replaced. The resulting file is passed to doctl apps update.
  • Result: The spec file in the repo stays generic; CI fills APP_URL, secrets, and repo/branch/source dir at deploy time. Credentials and app URL are managed in GitLab CI/CD variables, not in the repo.

GitLab CI/CD variables to set

Configure these in the project’s Settings → CI/CD → Variables (mask and protect as appropriate):

VariableRequiredDescription
DO_ACCESS_TOKENYesDigital Ocean API token for doctl apps update.
DO_APP_IDYes (per env)App Platform app ID for the environment (acceptance vs production). Create the app once (e.g. doctl apps create --spec .do/app-acc.yaml after running envsubst manually), then set this to the returned app ID. Use separate variable names per environment if you have separate acc/prod apps (e.g. set in job or use DO_APP_ID_ACC / DO_APP_ID_PROD and map in the job).
DO_DB_CLUSTER_NAMEYes (when using production DB)Name of the existing Managed Database cluster in DigitalOcean (e.g. app-b0a54db2-ee70-424f-ba6e-7ed9dcd43b59). Set per environment when using GitLab environments so acc and prod point at their respective clusters. Create the cluster in DO before deploy.
APP_URLYesThe app’s public URL (e.g. https://your-app.ondigitalocean.app). Backend derives BACKEND_URL and FRONTEND_URL from it for auth (OAuth callback, SSO redirect).
JWT_ACCESS_TOKEN_SECRETYesSecret for signing access JWTs. Use a strong value in non-dev.
JWT_REFRESH_TOKEN_SECRETYesSecret for signing refresh JWTs. Use a strong value in non-dev.
GOOGLE_CLIENT_IDNo (for Google SSO)Google OAuth 2.0 client ID. Set to enable “Sign in with Google”; leave unset or empty to disable.
GOOGLE_CLIENT_SECRETNo (for Google SSO)Google OAuth 2.0 client secret. Set together with GOOGLE_CLIENT_ID.
MICROSOFT_CLIENT_IDNo (for Microsoft SSO)Microsoft (Azure AD) application (client) ID. Set to enable "Sign in with Microsoft"; leave unset or empty to disable.
MICROSOFT_CLIENT_SECRETNo (for Microsoft SSO)Microsoft (Azure AD) client secret. Set together with MICROSOFT_CLIENT_ID.
MAILGUN_API_KEYYes (when using Mailgun)Mailgun API key. App spec sets MAILING_TYPE=mailgun in deployed envs; set this (and MAILGUN_DOMAIN) so the backend can send email. See stack rule mailing.
MAILGUN_API_URLNoMailgun API base URL (default in app: https://api.eu.mailgun.net). Set if using a different region.
MAILGUN_DOMAINYes (when using Mailgun)Sending domain verified in Mailgun.
MAILGUN_FROM_EMAILNoFrom address; defaults to noreply@\${MAILGUN_DOMAIN} when unset.

When the app spec includes a managed database (see § Managed database), DATABASE_URL is provided by the bindable variable (e.g. \${boilerplate-acc-db.DATABASE_URL}); do not set it as a CI variable.

Implementation

The boilerplate includes deployment artifacts for Digital Ocean App Platform. backend/Dockerfile builds the Nest backend (typically dist/main.js). .do/app-acc.yaml and .do/app-prod.yaml are app spec files that define a databases entry (managed Postgres with cluster_name: \${DO_DB_CLUSTER_NAME}), the backend service, and the static site (frontend); they use placeholders SOURCE_DIR, GITLAB_REPO, GITLAB_BRANCH, and DO_DB_CLUSTER_NAME so the same spec can be used from the unified-stack repo (e.g. deploy the boilerplate with SOURCE_DIR=boilerplate) or from a project that is an instance of the boilerplate (SOURCE_DIR=.). The deploy job must set DO_DB_CLUSTER_NAME (job variables or CI/CD variables) so envsubst can substitute it. Backend run_command is start:acc (acceptance: reset DB then start) or start:prod (production: migrate deploy then start). Backend service envs list ENVIRONMENT first (value acceptance or production, lowercase) and DATABASE_URL from the DB component bindable variable; when unset, the backend treats the environment as development. Static site envs list VITE_ENVIRONMENT first (acceptance or production, BUILD_TIME), then VITE_BACKEND_URL, so both backend and frontend receive environment as primary configuration (see pattern environments). The static site uses build_command: pnpm run build:frontend-desktop, which builds only shared, frontends/shared, and the desktop frontend (via pnpm --filter "desktop..." run build), not the backend. Actual deployment is driven by GitLab CI: the deploy job runs envsubst to substitute those variables, then doctl apps update with the resulting spec. See ci for the pipeline and the required CI/CD variables (e.g. DO_ACCESS_TOKEN, DO_APP_ID, DO_DB_CLUSTER_NAME).