Coolify vs Dokploy: Deploying the Same Application on an EU VPS

Deploy the same Dockerfile app with Coolify and Dokploy, then compare Git workflows, HTTPS, runtime variables, logs, image rollback and backup boundaries.

Adrian Silaghi

10 min read

0 views

CoolifyDokployDockerVPSSelf HostingDevOps
Coolify vs Dokploy: Deploying the Same Application on an EU VPS

Both Coolify and Dokploy can deploy a Git repository containing a Dockerfile, route a domain to the application, manage runtime configuration and expose logs. The useful difference appears when you operate the service: how you configure deployments, retain rollback images and recover the panel and application data.

This guide uses one small application and the same Dockerfile for both panels. The procedures follow current official documentation. The application's HTTP checks passed locally; neither panel was installed for this comparison, and no VPS performance or deployment-time benchmark is claimed.

Use equivalent infrastructure and separate panels

Use two fresh Linux VPS instances with equivalent resources and one test hostname per panel, such as coolify-app.example.com and dokploy-app.example.com. Keep the Git commit, base image and runtime variables identical. Avoid installing both panels on the same host: each wants to manage its proxy and infrastructure, making port conflicts an unnecessary variable.

Install and secure the panels using their official Coolify installation guide and Dokploy installation guide. Meet each guide's current requirements and leave headroom for builds, retained images and the application. A minimum specification is not a sizing guarantee for your workload.

Restrict administrative access, create the owner account promptly and configure the application DNS records before requesting certificates. Allow the public proxy's required HTTP/HTTPS traffic; keep database ports and the application's internal port private. Check both A and AAAA records so an old IPv6 address does not send some visitors elsewhere.

Choosing an EU VPS determines where that compute runs. It does not place your Git provider, registry, monitoring or backups in the same jurisdiction. DanubeData's documented location is Falkenstein, Germany. Review the entire data path rather than treating an EU address as a compliance guarantee.

Build one application that exposes useful deployment signals

Download the source, Dockerfile, Compose file and HTTP tests for the complete example.

Create server.mjs with this complete source. GET / returns a configurable greeting and a code-owned release marker. GET /health reports that this process can answer HTTP. The app stores no data, calls no external service and deliberately returns no environment dump.

import { createServer } from 'node:http';

const port = Number(process.env.PORT ?? '3000');
if (!Number.isInteger(port) || port < 0 || port > 65535) {
  throw new Error('PORT must be an integer between 0 and 65535');
}

const server = createServer((request, response) => {
  const route = request.url?.split('?')[0];
  const status = request.method !== 'GET' ? 405 : route === '/' || route === '/health' ? 200 : 404;
  const body = status === 405 ? { error: 'method_not_allowed' }
    : status === 404 ? { error: 'not_found' }
    : route === '/health' ? { status: 'ok' }
    : { greeting: process.env.APP_GREETING ?? 'Hello from an EU VPS', release: 'v1' };
  response.writeHead(status, {
    'Content-Type': 'application/json',
    'Cache-Control': 'no-store',
    ...(status === 405 ? { Allow: 'GET' } : {}),
  });
  response.end(JSON.stringify(body));
  console.log(JSON.stringify({ event: 'request', method: request.method, status }));
});

server.on('error', (error) => {
  console.error(JSON.stringify({ event: 'server_error', code: error.code }));
  process.exit(1);
});
server.listen(port, process.env.HOST ?? '0.0.0.0', () => {
  console.log(JSON.stringify({ event: 'listening', port: server.address().port }));
});
process.on('SIGTERM', () => server.close(() => process.exit(0)));

Create Dockerfile alongside it. The image runs as the non-root node user and uses Node itself for its health probe, so it needs no curl package.

FROM node:24-alpine
WORKDIR /app
COPY --chown=node:node server.mjs ./server.mjs
USER node
ENV NODE_ENV=production PORT=3000 HOST=0.0.0.0
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD node -e "fetch('http://127.0.0.1:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["node", "server.mjs"]

The node:24-alpine tag follows the Node 24 image line. For a reproducible comparison, resolve and use the same image digest in both builds. The local source checks used Node v25.2.1; the container build was not executed because the local Docker daemon was unavailable. Repeat the container check on your own test machine.

For a local Docker smoke test, create compose.yaml:

services:
  web:
    build: .
    environment:
      APP_GREETING: "Hello from an EU VPS"
    ports:
      - "127.0.0.1:18080:3000"

From that directory, run:

docker compose -p dd-panel-compare config --quiet
docker compose -p dd-panel-compare up --build -d --wait
curl --fail http://127.0.0.1:18080/
curl --fail http://127.0.0.1:18080/health
docker compose -p dd-panel-compare logs web
docker compose -p dd-panel-compare down --rmi local

The final command removes this test project's resources and locally built image. The local binding keeps port 18080 on loopback. Both panel recipes below use Application mode with the repository Dockerfile; the local Compose file is not their production deployment configuration. Keep PORT=3000 to match the image health check.

Put server.mjs and Dockerfile at the root of a repository accessible to each panel. For a private project, grant narrowly scoped repository access. Do not upload a production .env file or credentials into the source or image. See our Docker Compose VPS guide for the surrounding deployment concepts.

Deploy the application with Coolify

The official Dockerfile workflow provides these repository and build controls:

  1. Open the target project and environment, select + New, and choose a public repository or the appropriate authenticated source.
  2. For the public-repository path, enter the HTTPS repository URL and select Check Repository. Select the intended branch.
  3. Choose Dockerfile in Build pack. Set Base Directory to / and Dockerfile Location to Dockerfile for this root-level layout.
  4. Set Ports Exposes to 3000. The server must listen on 0.0.0.0, as this example does.
  5. In Environment Variables, add APP_GREETING with the value Hello from an EU VPS. Make it available in the container; it does not need build-time availability.
  6. Configure the domain, then select Deploy and inspect Deployment Logs.

Coolify's current domain settings use Domains > Add domain: choose HTTPS, enter coolify-app.example.com, internal Port 3000 and Path /. Review Redirect HTTP to HTTPS. The internal domain port does not publish port 3000 directly on the VPS.

The environment-variable guide distinguishes build-time and runtime scope. After a runtime change, restart or redeploy so the container receives it. A saved value is not evidence that the running process already uses it.

A plain repository URL does not prove push deployment is wired up. Follow the relevant source-integration or webhook path in automatic deployments, then push a harmless commit and confirm its identity in the deployment history.

Deploy the same application with Dokploy

Create an Application in the target project/environment. Use the same repository and branch. Dokploy's Git provider documentation specifies HTTPS URL, branch and Save for a public repository.

  1. In General, select the Git source and save the repository URL and branch.
  2. Choose the Dockerfile build type. Set Dockerfile Path to Dockerfile and Docker Context Path to . for this root-level layout. Leave Docker Build Stage unset.
  3. In Environment, set APP_GREETING=Hello from an EU VPS. Save the configuration.
  4. Deploy and inspect the deployment output, then the application's Logs.
  5. After DNS resolves to the VPS, open Domains and select Create Domain. Enter dokploy-app.example.com as Host, / as Path and 3000 as Container Port. Enable HTTPS, select Let's Encrypt under Certificate and select Create.

These controls are documented under Build Type, Applications and custom domains. Container Port is a proxy destination, not a request to publish 3000 on the host.

Keep the source connection separate from deployment automation. Dokploy documents an Auto Deploy toggle and provider webhook setup in its automatic deployment guide. Verify the configured branch with an actual harmless push in your lab before relying on it.

Compare logs and rollback with a controlled change

On each hostname, request / and /health. Expect the configured greeting, release v1 and an HTTP-success health response. Check build output separately from runtime output: a successful image build does not prove DNS, TLS or proxy routing works. The application emits structured request events without query strings or credential values.

Next, change the source release marker from v1 to v2 in a new commit and deploy both apps. Keep APP_GREETING unchanged. This separates image identity from mutable runtime configuration and gives you a visible result when you return to the previous image.

Coolify: open Rollback, identify the prior image under Available images, and select Roll back to this image. Its rollback guide makes retention central: a cleaned-up image is unavailable as a local rollback target, and the selected image runs with current runtime settings.

Dokploy: for explicit version selection, configure a registry and enable Deployments > Rollback Settings before producing the release images. Its rollback guide describes choosing the registry, saving deployment images and using Rollback beside the desired deployment. It separately documents Swarm health-check-based automatic rollback; that is not the same as selecting any historical version.

After either operation, verify v1 through the public URL and inspect errors. Neither a previous image nor a green process health check undoes an incompatible database migration. For this stateless example the rollback boundary is simple; a real application's schema compatibility must be designed and tested.

Keep PostgreSQL, S3 and recovery responsibilities explicit

When replacing the demo with a real application, external PostgreSQL and object storage can keep important state outside the replaceable container. That can make moving panels easier, but it adds network, authentication and availability dependencies. This example does not connect to either service; setting DATABASE_URL or S3 credentials alone would not test an integration.

Use the database provider's connection details, certificate verification requirements and network restrictions. Store credentials in the application's runtime configuration and test connectivity from the actual deployment environment. Keep uploads in an appropriately private bucket, configure the SDK's endpoint/region/addressing requirements, and separate application credentials from backup credentials. Browser uploads may need narrowly scoped CORS settings.

Back up the panel and workload state separately:

  • Coolify control plane: Settings > Backup protects its instance database. Preserve the matching APP_KEY separately; this is not an application-data backup. See instance backups.
  • Dokploy control plane: Web Server > Backups uses an S3 destination for Dokploy's PostgreSQL database and /etc/dokploy. Its backup guide describes this scope; do not assume it captures an external application database or every upload.
  • Application state: define database recovery points, object retention and any persistent-volume backups according to the application. A panel's S3 backup destination is distinct from an application writing its own objects to S3.

Rehearse recovery on an isolated target that cannot reach production servers or integration endpoints until you deliberately allow it. Restored management credentials can control surviving resources. Verify sample records and files, then document the time and recovery point achieved. Our PostgreSQL backup guide and S3 security guide cover those separate layers.

Choose using the workflow you can operate

PriorityCoolify evaluationDokploy evaluation
Git + DockerfileVerify repository, build context and runtime portVerify Git source, Dockerfile path and context
HTTPS and configurationReview Domains and build/runtime variable scopeReview domain certificate controls and service variables
Historical rollbackPlan image retention and compatible current configurationPlan registry-backed deployment images
RecoveryRestore panel database and matching key separately from stateRestore panel backup separately from workload state
Final decisionRehearse the team's normal release and recovery stepsRun the same exercise and compare operational friction

There is no measured performance winner here. If performance matters, measure the same immutable image on equivalent hosts, separating build time from application latency and recording repeated observations. Include operator time, registry storage and backup storage in cost comparisons.

For EU infrastructure, compare DanubeData VPS, managed PostgreSQL and S3-compatible storage against your chosen architecture. Running a deployment panel on a VPS still leaves you responsible for that panel and the guest operating system. The broader alternative is explained in managed apps versus self-hosting.

Frequently asked questions

Does the same Dockerfile work with both workflows?

Both documented workflows accept a repository Dockerfile. Configure the build context, internal port and runtime variables correctly. The source passed local HTTP checks; container execution and panel deployment still require your own validation.

Should I import the local Compose file into a panel?

Not for this comparison. It deliberately binds a local test port. Use Application mode and the Dockerfile for both recipes; a production Compose deployment has its own routing and configuration workflow.

Does rolling back a release restore my data?

No. Treat image rollback and data recovery as separate operations. Check schema compatibility before returning to older code and keep independent database and file recovery points.

Does either panel make the application EU-only?

No. Choose the location of compute, databases, storage and external services deliberately. Logs, registries and connected APIs can send data outside your chosen hosting region.

Sources

Coolify Dockerfile workflow; Coolify configuration; Coolify rollbacks; Dokploy Git sources; Dokploy build types; Dokploy custom domains; Dokploy rollbacks; Dockerfile reference.

Related tutorials

Browse all articles
Comparisons11 min read

Redis vs Valkey vs Dragonfly: Which to Run in 2026?

Redis, Valkey or Dragonfly in 2026? Compare licenses, threading models, memory efficiency, compatibility and HA — and when each in-memory store makes sense.

Adrian Silaghi1,542 views
Comparisons12 min read

Self-Hosted Log Management: Papertrail & Datadog Alternatives in Europe (2026)

A practical 2026 guide to European log management. Compare DanubeData Log Explorer, self-hosted Grafana Loki, OpenSearch and Graylog against Papertrail, Datadog and Loggly on cost, GDPR log residency and operational effort — with a Docker Compose Loki + Grafana stack you can run today.

Adrian Silaghi602 views

Ready to get started?

Deploy your infrastructure in minutes with DanubeData's managed services.