Deploy to Dokploy
A Heroku-like panel on your own server. Grit’s own sites run on it.
Running several apps on one box with a UI, automatic TLS and Git deploys: without paying per service.
Anyone who does not want to own an operating system. You are the one patching it.
Quick setup
- 1
Install on a fresh VPS
One command on a clean Ubuntu box. Give it 2 GB of RAM minimum: Dokploy plus a Go API plus Postgres will not fit comfortably in 1 GB.
curl -sSL https://dokploy.com/install.sh | sh - 2
Create the application
Point it at your repository and set the build context to the API directory. In a monorepo, set the watch path too, or every commit to the frontend rebuilds the backend.
Build type: DockerfileDocker file: apps/api/DockerfileDocker context path: apps/apiWatch paths: apps/api/** - 3
Know which variables are build-time
Anything read while the frontend compiles (NEXT_PUBLIC_*, and anything baked into prerendered HTML) must be a Build Argument. Set only at runtime, it is simply absent from the built output, and nothing warns you.
Build arguments: NEXT_PUBLIC_API_URLEnvironment: DATABASE_URL, REDIS_URL, JWT_SECRET - 4
Or deploy the whole stack as Compose
The steps above deploy the API as a single Dockerfile application. To bring up API, web, Postgres and Redis together, add a Compose service instead and point it at the production compose file. Choose Compose rather than Stack: Stack targets Docker Swarm and does not support the build key, and three of the services build from a Dockerfile.
Add Service: ComposeProvider: GitHub -> your repositoryCompose Path: ./docker-compose.prod.ymlCompose Type: docker-compose - 5
Give the containers their environment
Dokploy writes dashboard variables into a .env beside the compose file, and does not inject them into containers. That file only reaches a container if the compose file asks for it, so either keep the ${VAR} substitution in every environment block or add env_file to each service. Skipping this is why a stack boots with an empty DATABASE_URL despite the dashboard being full.
services:api:env_file:- .env # or keep ${VAR} in the environment blockenvironment:DATABASE_URL: ${DATABASE_URL} - 6
Attach domains per service
On a Compose deployment the Domains tab asks which service a domain belongs to, so web and api each get their own. Dokploy generates the Traefik labels; you do not write them. Preview Compose shows exactly what it will inject before you deploy, which is worth reading once.
- 7
Add the domain and TLS
Dokploy provisions TLS through Traefik. If Cloudflare sits in front with the orange cloud on, set SSL/TLS to Full (strict): Flexible produces a redirect loop that looks like an application bug.
What catches people out
Postgres and Redis run as containers you own. Backups, upgrades and disk pressure are yours: see the backup page.
The server is a single point of failure. Fine for internal tools, a real decision for anything customer-facing.
Dokploy updates itself in place. Snapshot the volume before a major upgrade.
Remove explicit container_name values from the compose file. Dokploy suffixes names so two projects can share a server, and an explicit container_name overrides that, which means a staging copy of the same stack on the same box collides with production.
Keep using ${VAR} substitution in the environment blocks. Dashboard variables are written to a .env beside the compose file, so they reach a running container through Compose substitution rather than being injected directly.
Auto-deploy re-clones the repository on every deploy, which wipes the working directory. Anything written there at runtime, uploads included, is gone on the next push. Use a named volume or Dokploy File Mounts, never a path inside the repo.
A custom deploy command replaces the default rather than adding to it. The default is docker compose -p <name> -f <path> up -d --build --remove-orphans, so anything you drop from it stops happening.
Deploying Sentex: VPS + Dokploy
The example application below is called sentex and its domains are on gritcms.com. Substitute your own project name and hostnames as you go — every other detail, including the service layout and the Compose translation, applies unchanged to any Grit project.
Your docker-compose.prod.yml is already written for Dokploy (see its header comments), so this is the least amount of translation of the four — mostly server setup, DNS, and pasting environment variables.
1.1 Provision the VPS
- Spin up a VPS (2 vCPU / 4 GB RAM minimum for four app containers + Postgres + Redis; more if traffic is expected). Ubuntu 22.04/24.04 is the best-supported OS for Dokploy’s install script.
- Point your registrar/DNS provider’s records at the VPS’s public IP — same three hosts as before:sentex.gritcms.com → <VPS public IP>admin.sentex.gritcms.com → <VPS public IP>api.sentex.gritcms.com → <VPS public IP>
- These stay A records (not CNAMEs) because Dokploy’s Traefik terminates TLS directly on your server’s IP — exactly what the Compose file’s header comments describe.
- SSH into the server as root (or a user with sudo).
1.2 Install Dokploy
- Run the official installer:curl -sSL https://dokploy.com/install.sh | sh
- Wait for it to finish — it installs Docker if missing, starts Dokploy’s own containers, and creates the
dokploy-networkDocker network that your Compose file’snetworks.dokploy-network.external: trueexpects. - Open
http://<VPS public IP>:3000in a browser and create your admin account on first load. - (Recommended) Under Settings → Server, point a domain at the Dokploy dashboard itself and enable HTTPS for it, so you’re not managing infrastructure over plain HTTP long-term. This is separate from your three app domains.
1.3 Connect GitHub
- In Dokploy, go to Settings → Git Providers → GitHub.
- Follow the prompts to install the Dokploy GitHub App on your account/org and grant it access to the
sentexrepository. (SSH deploy keys are the alternative if you’d rather not install a GitHub App — see Dokploy’s Providers docs for that flow.)
1.4 Create the project and Compose application
- In the Dokploy dashboard, click Create Project, name it
sentex. - Inside the project, click Create Service → Compose.
- Under Source, choose GitHub, select the
sentexrepository and the branch you’re deploying (e.g.main). - Set Compose Path to:docker-compose.prod.yml
- Leave the network settings alone — since your file already declares
networks: dokploy-network: external: trueplus its own internalsentexbridge network, Dokploy will attach correctly without any extra configuration.
1.5 Set environment variables
- Go to the Environment tab of the Compose service.
- Paste in the full contents of your
.env.productionfile (oneKEY=VALUEper line) — Dokploy writes this to a.envfile next to your compose file on the server and uses it to interpolate every${VARIABLE}reference indocker-compose.prod.yml, for both build args (NEXT_PUBLIC_API_URL,THEME, etc.) and runtime environment (POSTGRES_PASSWORD,APP_URL,CORS_ORIGINS, R2 credentials, and so on). This is a direct match forenv_file: [.env]already declared formigrateandapiin your Compose file. - Double-check
WEB_DOMAIN,ADMIN_DOMAIN,API_DOMAINare set to the exact hosts from step 1.1 — these are baked into the frontend bundles at build time and drive the TraefikHost()rules already written into your Compose file’s labels.
1.6 Deploy
- Click Deploy. Dokploy will:
- Build
migrate,api,admin,webfrom their Dockerfiles. - Start
postgresandredisand wait for their healthchecks. - Run
migrate(./migrate && ./seed) to completion — this works unmodified because Dokploy runs a realdocker compose up, and plain Docker Compose (unlike Railway) natively understandsdepends_on: condition: service_completed_successfullyandrestart: "no". You don’t need any pre-deploy-command workaround here. - Start
api, thenadminandwebonceapihas started.
- Build
- Watch the deployment logs in the Dokploy UI. Confirm
migrateexits 0 beforeapistarts. - After roughly 10 seconds, Traefik should finish provisioning Let’s Encrypt certificates for the three
Host()rules already defined in your Compose labels (sentex-api,sentex-admin,sentex-webrouters). No separate "Domains" configuration step is required in the UI, since the labels already declare everything — that’s what the Compose file’s own header comments mean by "routing works" this way.
1.7 Verify
- Visit
https://api.sentex.gritcms.com/<health endpoint>. - Visit
https://admin.sentex.gritcms.comandhttps://sentex.gritcms.comand confirm no CORS errors in the browser console. - Log in with the seeded demo SACCO credentials to confirm
migrate/seedactually ran.
1.8 Ongoing deploys
- In the Compose service’s General tab, enable the GitHub webhook ("Auto Deploy" / deploy-on-push) so pushes to your branch redeploy automatically — Dokploy re-clones the repo, re-reads the
.envit generated, and rerunsdocker compose up -d --build. migrate/seedreruns every deploy; it’s already idempotent per the Compose file’s own comments, so this is safe.
Platform dashboards and CLI flags change faster than these docs. For anything that looks different from what is written here, Dokploy's own documentation is the authority: docs.dokploy.com/
