Docker and Deployment Deployment guide Intermediate

Deploy a Full Ecommerce Stack on One VPS with Dokploy

A four-service storefront stack needs internal networking, TLS, zero-downtime redeploys and tested backups on one VPS, not ad-hoc scripts and unmanaged secrets.

Tested with

Dokploy
v0.26.0
Ubuntu
24.04 LTS
Docker
28.1.1
Postgresql
17.5
Meilisearch
v1.15

Before you start

  • Root or sudo SSH access to an Ubuntu 24.04 LTS VPS with at least 2 GB RAM, 30 GB disk, and ports 80, 443 and 3000 free
  • No Docker installation needed beforehand — Dokploy's installer sets up Docker 28.1.1 and a single-node Swarm itself
  • A domain name with DNS you control, pointed at the VPS's public IP, for the storefront's TLS certificate
  • Git repositories for the storefront and API, each able to expose a health-check route such as GET /health
  • An S3-compatible bucket and access keys (AWS S3, MinIO or Backblaze B2) for scheduled backups
On this page

Install Dokploy on a fresh Ubuntu 24.04 VPS

Provision an Ubuntu 24.04 LTS VPS with at least 2 GB of RAM and 30 GB of disk, then confirm ports 80, 443 and 3000 are free before running the installer. These are hard requirements in Dokploy’s installation docs (opens in a new tab), and the install script exits partway through if something else already owns one of those ports.

bash
ss -tulpn | grep -E ':80|:443|:3000'

Expected output:

text
(no output — all three ports are free)

Pin the release instead of installing whatever is current. The v0.26.0 release (opens in a new tab) is what every screenshot and menu path in this article assumes, and Dokploy’s UI has moved between minor versions before.

Dokploy documents this as a single piped one-liner. This is the same install with one step added: fetch the script, read it, then run it. Granting root to a script nobody has read is a habit worth not building, however reputable the vendor.

bash
export DOKPLOY_VERSION=v0.26.0
curl -fsSL https://dokploy.com/install.sh -o dokploy-install.sh
less dokploy-install.sh   # confirm what it installs and which ports it opens
sh dokploy-install.sh

Expected output:

The installer also sets up a single-node Docker Swarm running Docker 28.1.1 (opens in a new tab) without asking. You won’t run docker swarm init yourself — this Swarm is what gives you rolling updates and internal service DNS by name later, with no extra setup. Once it finishes, the dashboard is live at http://<VPS_IP>:3000, and the first thing you do there is create the admin account.

You should now see: the Dokploy dashboard loading at port 3000, with an empty organization ready for a first project.

Group the storefront, API, PostgreSQL and Meilisearch in one Dokploy project

Dokploy’s structure is Organization > Project > Environment > Service, and every new project gets a default environment automatically (opens in a new tab). That environment, not the project, is the boundary that matters for what you’re building next.

Services in the same environment share the same internal Docker network. That’s the actual mechanism behind hostname-based service discovery a few sections from now — not a naming convention, a network boundary enforced by Swarm.

graph TD
    Org[Organization] --> Proj[Project: storefront-prod]
    Proj --> Env[Environment: production]
    Env --> S1[Storefront service]
    Env --> S2[API service]
    Env --> S3[PostgreSQL service]
    Env --> S4[Meilisearch service]

Create one project — storefront-prod works fine — and add all four services inside its default environment: storefront, API, PostgreSQL and Meilisearch. Resist the urge to split them across separate projects or environments for tidiness. That split is the single most common cause of the host-resolution failures you’ll see later, when the API can’t find postgres or meilisearch by name.

You should now see: the project view listing all four services inside one environment, each with its own status indicator.

Deploy PostgreSQL 17.5 and Meilisearch v1.15 with persistent volumes

Create PostgreSQL as a Dokploy Database service and set the image tag to 17.5 explicitly — don’t accept whatever default tag the form offers. PostgreSQL 17.5 (opens in a new tab) is a specific patch release, and letting Dokploy pick a floating tag means a later redeploy can silently move you off it.

Meilisearch needs the same explicit treatment, more so. Dokploy’s official Meilisearch template (opens in a new tab) currently pins getmeili/meilisearch:v1.35.1, not v1.15 — you have to override the tag by hand if v1.15 is the target. Worth flagging before you commit to it: v1.15’s security support ended 2025-08-04 (opens in a new tab), and as of 2026-07-27 the latest stable release is v1.51.0 (opens in a new tab). Run a current release in production; use v1.15 only if something in your stack genuinely depends on it.

yaml
services:
  meilisearch:
    image: getmeili/meilisearch:v1.15
    volumes:
      - meili_data:/meili_data
    environment:
      - MEILI_MASTER_KEY=${MEILI_MASTER_KEY}
      - MEILI_NO_ANALYTICS=true

volumes:
  meili_data:
    external: true
bash
MEILI_MASTER_KEY=<GENERATED_MEILI_MASTER_KEY>
POSTGRES_DB=storefront
POSTGRES_USER=storefront_app
POSTGRES_PASSWORD=<POSTGRES_PASSWORD>

The template generates MEILI_MASTER_KEY as base64-encoded 32 bytes on first deploy. Copy that exact value into an explicit environment variable now — a later redeploy that regenerates it will silently break every client holding the old key. Mount both services on named Docker volumes (opens in a new tab), never on a path inside the repo checkout: AutoDeploy clones the repository fresh on every redeploy, and anything living in that path is gone by the next deploy.

You should now see: both services running and healthy in the dashboard, with data intact after a manual redeploy.

Wire internal networking between the storefront, API and data services

Services in the same environment resolve each other by service name over the shared internal network. DATABASE_URL on the API should point at a host literally named postgres, not localhost and not the VPS’s public IP — neither of those addresses the container Dokploy actually started.

Leave the Postgres and Meilisearch ports unpublished. Starting with Docker Engine 28 (opens in a new tab), unsolicited inbound traffic to a container’s internal IP is dropped unless the port is explicitly published, so nothing outside the internal network can reach them anyway — publishing would only open a hole you don’t need.

bash
DATABASE_URL=postgresql://storefront_app:<POSTGRES_PASSWORD>@postgres:5432/storefront
MEILI_HOST=http://meilisearch:7700
MEILI_MASTER_KEY=<GENERATED_MEILI_MASTER_KEY>

Set DATABASE_URL on the API service, and MEILI_HOST plus MEILI_MASTER_KEY on both the API and the storefront. Check that it worked by reading the API’s own container logs in the Dokploy dashboard, not by curling the VPS from outside — an external curl only tells you Traefik is up, not that the API reached Postgres or Meilisearch internally.

You should now see: API logs showing successful connections to both PostgreSQL and Meilisearch, with no host-resolution errors.

Put TLS on the storefront domain

Point your domain’s DNS A record at the VPS’s public IP first. Dokploy can’t request a certificate for a domain that doesn’t resolve to it yet, and the request will fail silently retry until DNS catches up.

Add the domain under the storefront service’s Domains tab once DNS has propagated. Dokploy’s built-in Traefik integration (opens in a new tab) requests and renews a Let’s Encrypt certificate automatically from there — no certbot, no cron job, nothing to script.

graph LR
    subgraph Public["Public internet"]
        User[Browser]
    end
    subgraph Internal["Internal Docker network only"]
        API[API service]
        PG[(PostgreSQL 17.5)]
        MS[(Meilisearch v1.15)]
    end
    User -->|HTTPS via Traefik| Storefront[Storefront service]
    Storefront --> API
    API --> PG
    API --> MS

Only the storefront needs this. PostgreSQL and Meilisearch are never given a domain — they have no reason to face the internet at all. The API can stay entirely internal or get its own subdomain, depending on whether anything outside your project needs to call it directly.

You should now see: the storefront domain loading over HTTPS with a valid, auto-renewing certificate.

Configure health checks and rolling updates for zero-downtime deploys

By default, Dokploy stops the running container and only then starts its replacement — that’s Docker Swarm’s default update behaviour, and Dokploy inherits it as-is (opens in a new tab). The gap between “stopped” and “started” is a few seconds where Traefik has nothing to route to, and every deploy pays that cost.

sequenceDiagram
    participant Git as Git push
    participant Dokploy
    participant Old as Old container
    participant New as New container
    participant Traefik

    Git->>Dokploy: webhook triggers redeploy
    Dokploy->>Old: stop
    Note over Traefik: nothing to route to — 502 Bad Gateway
    Dokploy->>New: start
    New-->>Dokploy: healthcheck passes
    Dokploy->>Traefik: switch routing to New

Close it with a health check under Advanced > Cluster Settings > Swarm Settings > Health Check — a Docker healthcheck block pointing at a route like GET /health that returns 200. Time fields here are nanoseconds, not seconds, which trips up anyone typing the numbers by hand.

yaml
healthcheck:
  test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
  interval: 30000000000
  timeout: 5000000000
  startPeriod: 10000000000
  retries: 3

A health check alone doesn’t close the gap — it only tells Dokploy when the new container is ready. Add a rolling-update policy alongside it so traffic doesn’t switch until that check passes.

yaml
update:
  type: rolling
  max_unavailable: 1
  delay: 10s

Enable AutoDeploy on the storefront and API services once both are in place, so a Git push triggers a redeploy gated on the health check passing.

You should now see: a Git push to the storefront repository triggering a redeploy with no Bad Gateway response during the switch.

Schedule automated PostgreSQL backups to S3-compatible storage

Create the destination before you go anywhere near the database’s Backup tab — it has nothing to select until one exists. Under Settings > Destinations, add an S3-compatible endpoint (opens in a new tab): AWS S3, MinIO, Backblaze B2, or anything else that speaks the S3 API.

With the destination in place, open the PostgreSQL service’s Backup tab and set the destination, a database name, a cron schedule and a bucket prefix.

bash
# Daily, keep 7 backups
0 2 * * *

# Weekly on Sunday, keep 12 backups
0 3 * * 0

Set a retention count for each schedule. Dokploy prunes the oldest backup automatically once that count is exceeded, so you don’t need a separate cleanup job. Before you trust either schedule unattended, run Test backup once and confirm it actually reaches the bucket — a bad access key fails quietly otherwise.

You should now see: a manual Test backup run succeeding, with a dump file appearing at the configured path in the S3 bucket.

Rehearse a database restore before you need one

A backup schedule proves nothing until someone has restored from it. Treat the first restore as a required step in setting up the stack, not an optional check you’ll get to later.

flowchart TD
    A[Scheduled backup runs] --> B[Open Backup tab → Restore]
    B --> C[Restore into a new, differently named database]
    C --> D{Table and row counts match production?}
    D -->|Yes| E[Backup path is trustworthy]
    D -->|No| F[Investigate schedule, destination or restore command before relying on it]

Restore from the same Backup tab’s Restore action: pick the S3 bucket, select the backup file from the autocompleted list, name a target database, and restore. Dokploy’s own docs warn (opens in a new tab) that dumps not produced by its own backup feature aren’t guaranteed to work, since Dokploy applies engine-specific restore commands — pg_dump and gzip for Postgres — that assume its own backup format.

Restore into a differently named database first, never directly over production. Check table and row counts against the live database before you decide the backup path is trustworthy. Only promote it once those numbers match.

You should now see: a second PostgreSQL database, restored from the scheduled backup, with the same tables and row counts as production.

Limitations

This covers a single VPS and a single-node Swarm — it doesn’t cover scaling PostgreSQL or Meilisearch across multiple nodes, or what changes when Dokploy manages more than one server. It also doesn’t cover secrets rotation beyond the initial Meilisearch master key, WAF or rate-limiting in front of the storefront, or a staging environment that mirrors production before a deploy reaches it. The restore rehearsal here checks structural integrity — table and row counts — not application-level correctness of restored data, and it doesn’t cover point-in-time recovery, only the latest scheduled dump. The exact console output of the Dokploy install script was not captured for this article, and it varies by version, so nothing here reproduces it verbatim.

Sources

  1. Dokploy documentation Dokploy Primary source
  2. Backups | Dokploy Primary source
  3. Restore | Dokploy Primary source
  4. Docker Compose | Dokploy Primary source
  5. Zero Downtime | Dokploy Primary source
  6. Meilisearch | Dokploy (template) Primary source
  7. Installation | Dokploy Primary source
  8. Multi-Tenancy | Dokploy Primary source
  9. Release v0.26.0 · Dokploy/dokploy Primary source
  10. Docker Engine version 28 release notes Primary source
  11. PostgreSQL 17.5 Release Notes Primary source
  12. Releases · meilisearch/meilisearch Primary source
  13. Meilisearch 1.15 — official blog Primary source
  14. Meilisearch | endoflife.date
7 min read
  • Docker intermediate

    How to Back Up Docker Volumes Automatically

    A nightly backup for a self-hosted Docker host: database dumps taken with the right tools, file volumes snapshotted with restic, a systemd timer to run it, and a restore test that proves it works.

    4 min AI-assisted
  • Docker intermediate

    Dokploy vs Coolify: Choosing a Self-Hosting Platform

    Both put a Heroku-style deploy UI on your own VPS, but on different container primitives. What that means for zero-downtime deploys, multi-server growth and the day the control plane breaks.

    6 min AI-assisted