Back to blog
Infrastructure2026-07-17

Deploying with Dokploy: why I moved, and what it actually cost me

What deploying looked like before

Before this, deploying meant a plain VPS on Contabo running docker-compose: Postgres, the API, the web app, and Caddy handling the reverse proxy and HTTPS. CI was already solid, every PR runs lint, typecheck, tests, and a build in GitHub Actions before it can merge. What wasn't automated was CD: getting merged code onto the actual server. That part was still SSH into the Contabo box, git pull, then restart Docker, and if the deploy included a schema change, manually run the migration too. Every single time.

What I actually wanted was the feel of PaaS platforms like Railway, Vercel, and Render: push and it's live. Just without their cost or usage limits hanging over me. That's where Dokploy comes in. There are alternatives, Coolify being the other one I had my eye on, but Dokploy's what I went with.

Installation

First, SSH into your server, could be Hostinger, DigitalOcean, Contabo, whatever you're running on. Installing Dokploy is a single script, but it's picky about how you run it. Here's what I ran into, in order, and how I fixed each one.

First thing I hit: "This script must be run as root". The fix isn't to somehow become root as a "non-root user", it's to not run the script bare at all. Pipe it into a shell invoked with sudo, so it executes with root privileges from the start:

curl -sSL https://dokploy.com/install.sh | sudo sh

Traefik handles routing and HTTPS for Dokploy, and it runs on ports 80 and 443, so it can't start if something else already has them. Dokploy's own dashboard runs on port 3000 by default, same problem. Both ports still had leftover services from my old manual setup sitting on them.

Next: "something is already running on port 80". Checked what's actually bound to port 80:

sudo lsof -i :80

For me, it was Caddy, still running inside Docker from the old manual deploy setup. Stopped it:

docker stop caddy

(General-purpose command for checking everything listening on the box: sudo lsof -i -P -n | grep LISTEN.)

Last one: "something is already running on port 3000". Dokploy's own dashboard defaults to port 3000, which collided with a my-web container from the old setup already sitting on it.

sudo lsof -i :3000
docker stop my-web

With all three cleared, the installer finally finishes:

Congratulations, Dokploy is installed!
Wait 15 seconds for the server to start
Please go to http://YOUR-SERVER-IP:3000

For more on installing and configuring Dokploy beyond what I hit here, the official docs are on the Dokploy site.

First login and securing the dashboard

Visit http://YOUR-SERVER-IP:3000 and create an admin account.

First real step after that: get HTTPS on the dashboard itself, instead of leaving it on a bare IP. Much easier with a domain already pointed at the server, I had one on Cloudflare already, so all I needed was one new DNS A record (name dokploy, pointing at the server's IP).

Then, inside Dokploy: Settings → Web Server, set the domain, an email address (for Let's Encrypt), select Let's Encrypt as the certificate provider, save. Traefik provisions the certificate automatically on the first request, no separate certbot dance.

Once that's confirmed working, lock down direct IP:port access, no reason to leave port 3000 reachable once the domain works:

docker service update --publish-rm "published=3000,target=3000,mode=host" dokploy

From then on it's https://dokploy.your-domain.com, no more IP, no more port number.

Creating the first project

The dashboard is the main landing view once you're in. Creating a project is the obvious next step: Create Project, give it a name (e.g. demo-app), an optional description, and optionally a tag (e.g. production).

first project creation screen

One step before any of this works: authorize GitHub under Settings → Git Providers (https://your-domain.co/dashboard/settings/git-providers on your own instance), the same OAuth flow you'd go through on Railway or Vercel to give a platform access to your repos.

Connecting the repo: the actual push-to-deploy part

This is the part I was actually here for. For a multi-service stack (I'm running Postgres, api, and web together from one docker-compose file), the project type is Compose, not Application, Application is for a single service built from one Dockerfile. Full setup is in Dokploy's Docker Compose docs: point it at your GitHub repo and branch, tell it where your compose file lives, set Trigger Type to On Push.

That last part's the whole point. Push to main, a webhook fires, Dokploy pulls and redeploys on its own. No SSH, no git pull, no me at a keyboard.

Wiring up secrets

Dokploy has its own Environment Variables system, set your app's secrets there instead of on disk, reference them in your compose file with ${VAR_NAME}. If your app reads config from env vars (mine does, DB credentials, auth secrets, API keys), make sure they're actually set here before your first deploy.

Logs, live

Deploys stream to the browser as they happen, the git clone, the build, the whole thing, and every running service gets its own live log view too. See Dokploy's Docker Compose docs for the full feature set.

Getting notified when something happens

Dokploy has its own Notifications system, separate from anything your app does, Slack, Discord, Telegram, plain SMTP, or Resend, pick a channel type and wire it up. Each channel gets the same set of event toggles: App Deploy, App Build Error, Database Backup, Dokploy Backup, Volume Backup, Docker Cleanup, Dokploy Restart. I used Resend, flipped on the events I actually care about, hit Test Notification to confirm it actually reaches me, then Create.

So, was it worth it?

Yes. The thing I actually came here to fix, SSHing in and rebuilding by hand on every change, is gone. Push to main, it deploys itself.

This is part 1 of a series, next up: load-testing the deployed api.