Production deploys from main on Vercel. Every pull request gets its own preview per app on a Dokploy server you host, with a sticky comment on the PR and a github deployment per app.
The workflows are preview.yml and preview-cleanup.yml, the logic lives in .github/scripts/preview*.mjs (plain node, no dependencies).
What a PR gets
- One Dokploy application per app (
web,api,dashboard, plusdocswhendocs/Dockerfileexists), built from the PR commit with the app'sDockerfile. - A domain
pr-<number>-<app>.<PREVIEW_DOMAIN>. - A github deployment per app in the environment
preview-<number>, so the PR shows View deployment. - One comment, updated in place on every push: app, status (Building, Ready, Failed), preview link, inspect link to the Dokploy deployment, and a short error excerpt when a build fails.
When the PR closes, preview-cleanup.yml deletes the Dokploy apps and marks the deployments inactive.
Set up Dokploy
- Install Dokploy on a server, see the install guide.
- Create a project and an environment for previews. Copy the environment id from the url of the environment page.
- Profile settings, API/CLI: generate an API key. It is sent as the
x-api-keyheader. - Point a wildcard DNS record at the server:
*.preview.example.com A <server ip>.
Repo settings
| Name | Kind | Value |
|---|---|---|
DOKPLOY_URL | variable | https://dokploy.example.com |
DOKPLOY_TOKEN | secret | the API key |
DOKPLOY_ENVIRONMENT_ID | variable | environment id from step 2 |
PREVIEW_DOMAIN | variable | preview.example.com |
PREVIEW_GIT_TOKEN | secret, optional | token with read access, needed to clone a private repo |
PREVIEW_APP_ENV | secret, optional | extra KEY=value lines for every preview app (database url, auth secret) |
PREVIEW_HTTPS | variable, optional | false to skip Let's Encrypt |
Without DOKPLOY_URL, DOKPLOY_TOKEN, DOKPLOY_ENVIRONMENT_ID and PREVIEW_DOMAIN the workflow exits green and says what is missing in the run summary. Previews are skipped for draft pull requests and forks, since forks cannot read secrets.
Each preview app gets WEB_URL, DASHBOARD_URL and API_PUBLIC_URL pointing at the other preview urls. A database is not created for you, set DATABASE_URL through PREVIEW_APP_ENV.
Vercel stays production only
This repo ships a vercel.json with git.deploymentEnabled so Vercel only builds main. Do the same in each Vercel project of a generated app:
{
"git": { "deploymentEnabled": { "main": true, "*": false } }
}