Deploying on Dokploy
Dokploy is a self-hosted PaaS that runs your Docker
Compose stacks behind its own Traefik. The repository ships
docker-compose.dokploy.yml for it, so a deploy is four screens of settings and
no YAML editing.
That file is
docker-compose.prod.yml,
the stack described in Installation, with two
differences:
- No
ports:. Traefik reaches the containers over Dokploy’s owndokploy-network, so publishing on the host does nothing. It also removes a collision: Reverb’s default host port is8080, which is Dokploy’s Traefik dashboard. appandreverbjoindokploy-networkexplicitly, next to the stack’s owndefaultnetwork.
Before you start
Section titled “Before you start”- An amd64 host. The published image is built for
linux/amd64only (#205), so it will not boot on an ARM server. - A DNS A record for your host (this page uses
chat.example.com) pointing at the Dokploy server. - SMTP credentials, as with any install. See Requirements.
Create the Compose application
Section titled “Create the Compose application”In your Dokploy project, create a Compose service (not an Application) and set:
| Field | Value |
|---|---|
| Provider | GitHub → deskhq/the-desk |
| Branch | master |
| Compose Path | ./docker-compose.dokploy.yml |
| Compose Type | Docker Compose |
The branch only decides which compose file Dokploy reads. The application
image itself comes from the GitHub Container Registry and is selected by
APP_VERSION in the environment, so the two move independently. See
Pinning and upgrading below.
The Environment tab
Section titled “The Environment tab”Dokploy writes the contents of this tab to a .env file next to the compose
file. That one file satisfies both halves of how the stack reads configuration:
compose’s env_file: injects the values into every container, and the
./.env:/app/.env:ro bind mount puts the physical file inside the app so
php artisan resolves exactly the same configuration.
Start from
.env.prod.example
and paste it in, then fill in your values. Three rules are specific to this tab:
- No quotes. The editor normalises them away, so
APP_NAME="The Desk"is written asAPP_NAME=The Deskand phpdotenv aborts the boot withEncountered unexpected whitespace. Keep every value free of spaces. The shipped template already is. - No
${OTHER_VALUE}references. Values reach the containers throughenv_file:, which passes them through literally, so a reference arrives as the characters you typed. Write the value out. - No
COMPOSE_FILEline. It namesdocker-compose.prod.yml, which is the wrong file here. Dokploy passes its own Compose Path explicitly.
The values you must set by hand are the same everywhere: APP_URL, the
required secrets (APP_KEY,
DB_PASSWORD, MEILISEARCH_KEY, and the REVERB_APP_* trio), your MAIL_*
credentials, and the Reverb values from the next section. docker/gen-secrets.sh
is the bare-VPS path and has nothing to write into here, so generate the secrets
locally and paste them:
docker run --rm ghcr.io/deskhq/the-desk:latest php artisan key:generate --show
openssl rand -hex 32 # DB_PASSWORD, and again for MEILISEARCH_KEYopenssl rand 4 | od -An -tu4 | tr -d ' ' # REVERB_APP_IDopenssl rand -hex 16 # REVERB_APP_KEYopenssl rand -hex 16 # REVERB_APP_SECRET (run it a second time)These are the same recipes
docker/gen-secrets.sh
uses on the bare-VPS path, so the two installs end up with identically shaped
values. There is no artisan command for the REVERB_APP_* trio: laravel/reverb
ships only reverb:install, reverb:start and reverb:restart.
The Domains tab
Section titled “The Domains tab”Add two domain rows, one per service.
| Service | Host | Path | Strip Path | Container Port | HTTPS |
|---|---|---|---|---|---|
app |
chat.example.com |
/ |
off | 8080 |
on |
reverb |
chat.example.com |
/app |
off | 8080 |
on |
Turn HTTPS on and let Dokploy issue a Let’s Encrypt certificate. The app trusts
Traefik’s X-Forwarded-* headers out of the box, so it generates https://
links with nothing further to configure.
Reverb on the same domain (recommended)
Section titled “Reverb on the same domain (recommended)”The second row above routes /app to reverb, which is the path Echo opens its
WebSocket against (wss://chat.example.com/app/{key}). Strip Path must be
off: Reverb needs the prefix. This costs one DNS record and one certificate,
and the app has no route under /app to collide with.
Then, in the Environment tab:
REVERB_PORT_PUBLIC=443REVERB_SCHEME_PUBLIC=httpsREVERB_ALLOWED_ORIGINS=chat.example.comLeave REVERB_HOST_PUBLIC unset. The browser-facing host then defaults to the
host of APP_URL, which is exactly what you want when both share a domain.
Reverb on its own subdomain
Section titled “Reverb on its own subdomain”The alternative is a ws- style subdomain, which needs its own public DNS record
and its own certificate. Point the reverb domain row at ws.example.com with
Path /, then set:
REVERB_HOST_PUBLIC=ws.example.comREVERB_PORT_PUBLIC=443REVERB_SCHEME_PUBLIC=httpsREVERB_ALLOWED_ORIGINS=chat.example.comREVERB_ALLOWED_ORIGINS stays the app’s host in both layouts. It is checked
against the browser’s Origin header, which is the page the socket was opened
from, not the host the socket connects to. Leaving it at the template’s
chat.example.com when your host is something else rejects every handshake
silently, and all you see is a
stuck “Reconnecting…” banner.
Deploy
Section titled “Deploy”Hit Deploy. The app container runs php artisan migrate --force on boot,
so the database is ready by the time it reports healthy. Then
create the first user and workspace.
Running artisan commands
Section titled “Running artisan commands”There is no docker compose exec here, because the compose project belongs to
Dokploy. Go through the container directly:
docker ps --filter name=app --format '{{.Names}}'docker exec -it <project>-app-1 php artisan aboutThe image already sets the working directory to /app and runs as the www
user, so neither -w nor -u is needed.
Pinning and upgrading
Section titled “Pinning and upgrading”APP_VERSION selects the image tag, and upgrading is an edit in the Environment
tab followed by Redeploy (Dokploy pulls the new tag):
APP_VERSION=1.18.0 # x-release-please-versionTo track stable releases instead of pinning one, set APP_IMAGE, which overrides
the version entirely:
APP_IMAGE=ghcr.io/deskhq/the-desk:latestlatest is only ever applied to a stable release. A release candidate is
published as X.Y.Z-rc.N plus the moving rc tag, so a candidate can never land
on latest and this cannot drift onto the develop line by accident.
See Upgrading for what an upgrade does to your data and the search index.
Troubleshooting
Section titled “Troubleshooting”A bare 404 page not found from Traefik
Section titled “A bare 404 page not found from Traefik”Symptom. The domain resolves and serves TLS, but every request returns a
plain 404 page not found with no styling. That page is Traefik’s, not the
app’s.
Cause. Traefik drops the router for a container that is not running, so a
crash-looping app container looks like a missing route. The wrong Container
Port produces the same page.
Fix. Read the container logs, which name the real failure:
docker logs <project>-app-1 --tail=100Then confirm Container Port is 8080 on both domain rows.
could not translate host name "pgsql" to address
Section titled “could not translate host name "pgsql" to address”Symptom. The app container restarts in a loop with
SQLSTATE[08006] could not translate host name "pgsql" to address, while
pgsql itself is up and healthy.
Cause. The stack is split across two networks. Dokploy rewrites networks:
on whichever service a domain is attached to, replacing a custom network with
default plus dokploy-network. Every other service stays on the custom one, so
the web container can no longer resolve its own database.
Fix. Use docker-compose.dokploy.yml, which declares no custom network.
If you pasted a compose file of your own, remove any networks: block that is
not dokploy-network.
Failed to parse dotenv file. Encountered unexpected whitespace
Section titled “Failed to parse dotenv file. Encountered unexpected whitespace”Symptom. Containers restart in a loop. The error names neither the file nor the key, and the deployment log usually scrolls past it.
Cause. A value in the Environment tab contains a space. The editor stripped the quotes that made it parse.
Fix. Find the value with a space in it and remove the space, for example
APP_NAME=TheDesk. SSO_OIDC_SCOPES is the one setting that cannot be written
without spaces, so leave it commented out unless your identity provider needs
non-default scopes.
A stuck “Reconnecting…” banner
Section titled “A stuck “Reconnecting…” banner”Symptom. The app loads and you can sign in, but a “Reconnecting…” banner sits at the top and no real-time updates arrive.
Cause. On this platform it is almost always one of two things: the reverb
domain row with Path /app is missing (or has Strip Path on), or
REVERB_ALLOWED_ORIGINS is still the template’s chat.example.com while your
host is something else, in which case Reverb rejects every handshake without
saying so.
Fix. Re-check the Domains table and the
REVERB_ALLOWED_ORIGINS value for your layout, then redeploy. The full
symptom-first walkthrough is in
Troubleshooting.