Embedding the full YAML (base64-encoded) into a single SSH exec command
silently fails once the payload passes the SSH transport's packet size
ceiling (~90-100KB), truncating the dynamic config with no error
surfaced anywhere. Switch to SFTP, which has no such limit.
--project-directory was added unconditionally in 87b914996 to fix
relative bind mount resolution for git-based compose deploys with a
nested composePath (#5181). It also moves where build.context and the
generated .env resolve, breaking any compose file in a subdirectory
that has context: . alongside its Dockerfile (#5230), or interpolates
env vars (#5242).
Only pin --project-directory when the compose actually has mounts
configured; otherwise build.context and .env resolve against the
compose file's own directory like plain docker compose. Also pass
--env-file explicitly pointing at the generated .env next to the
compose file, so it's found even when --project-directory is pinned
(credit: tonnenpinguin, PR #5235).
Fixes#5230Fixes#5242
Fixes#5152. updateFileMount wrote directly to the target path without
ensuring its parent directory existed, and silently swallowed errors
instead of surfacing them. If the path already existed as a directory
(e.g. Docker pre-creating a bind mount target), the write silently
failed and left an empty directory instead of the file.
Attaching Basic Auth or a Redirect to an application with no domains
wrote an empty routers/services block to the app's dynamic config
file. Traefik's file provider rejects that as invalid and aborts its
watcher, blocking config updates for every other application.
Fixes#5189
Compose deployments using a git provider (GitHub/GitLab/Gitea/Bitbucket/
custom Git) clone the repo verbatim, so composePath can point into a
subdirectory (e.g. deploy/docker-compose.yml). Docker Compose resolves
relative bind mounts (../files) against the compose file's own directory,
not against the code/ dir where file mounts are actually written, so the
mount silently binds an empty directory instead of the configured content.
Pin --project-directory to code/ when building the docker compose command
so relative mounts always resolve the same way raw deployments already do.
Fixes#5181
Network sync compared only by name, so a network deleted and
recreated with the same name but different attributes (driver,
attachable, etc.) was reported as already in sync.
Store Docker's network Id (dockerId column) and compare it against
the live Id during sync. A name match with a dockerId mismatch is
now surfaced as a new "changed" state, with an Update action to
refresh the DB row from the live network.
Fixes#5179
Escaping in prepareEnvironmentVariablesForFile escaped every $,
including inside a deliberate ${VAR} reference, breaking Compose's
documented .env variable interpolation. Only escape $ that isn't
part of a ${IDENTIFIER} sequence.
Fixes#5151
Reaching a server implied being able to open an SSH root shell on it: the
/terminal websocket only checked that the server was in the caller's accessible
set, so granting a server to a developer necessarily granted root on it.
Add a server.terminal action, assignable on custom roles, and require it on the
remote-server terminal websocket on top of server access. Owner/admin keep it
through the enterprise bypass; the local host terminal stays owner/admin only.
A migration grants server.terminal to existing custom roles that already have
server.read, which is the permission that surfaces the terminal today, so
current setups keep working. Roles without a server entry are left alone.
Note: server.create still implies root execution (server.update persists
server.command and server.setup runs it over SSH), reflected in the Create
description in the role editor.
The swarm migration in 3848fa9c0 dropped the container.remove({force:true})
that the standalone deploy path used to run. Swarm tasks are named
dokploy-monitoring.<slot>.<id>, so there is no name collision and the
pre-v0.30.0 container survives every redeploy. It also stays pinned to an
orphaned image ID once pullRemoteImage moves the latest tag, so neither a
pull nor a Save clears it and it restarts forever.
Cloud setup spread metricsConfig straight from the row, shipping
cronJob: "" to the agent. robfig/cron rejects an empty spec, so the Go
binary exits before Fiber binds 4500 and Docker restarts it every ~60s.
- remove the legacy container in deployMonitoringService, which covers both
setupMonitoring and setupWebMonitoring. Cleanup is best effort: a failure
is logged and the deploy continues, matching the pre-migration behaviour
- default cronJob when configuring monitoring for cloud
- on build servers, clean up the legacy container but deploy no service.
They never join the swarm, yet cloud setup did create the standalone
container there before v0.30.0, and the monitoring form has always been
hidden for them, so those agents are all stuck with an empty cronJob
- cover the above with real-docker tests
docker stack deploy reads env_file literally without stripping
quotes (unlike docker compose), so the quoting/escaping added to
fix#4694 was shipping literal quote characters into stack
containers.
Fixes#5096, #5110.
The application table has 101 columns. The restricted-member branch of
project.one (apps/dokploy/server/api/routers/project.ts) queried the
applications relation with no columns narrowing, so Drizzle's relational
query builder generated a json_build_array call with one argument per
column, exceeding Postgres's FUNC_MAX_ARGS (100). Any non-owner/admin
member with limited access to a project containing at least one
application hit an opaque INTERNAL_SERVER_ERROR and got redirected away
from the project/environment page instead of seeing their project.
The same branch was also missing the "server" relation that the
owner/admin path (findProjectById) already includes, so restricted
members saw different (incomplete) data than owners/admins for the same
project.
Fixes this by extracting the existing serviceColumns column-selection
constant to a module-level export in packages/server/src/services/project.ts
and applying it (plus the server relation) to all 8 service relations in
the restricted-member query path, matching the owner/admin path.
Some self-hosted GitLab instances (e.g. older versions without
expires_in configured in doorkeeper) omit expires_in from the
OAuth token response. Computing Date.now()/1000 + undefined
produced NaN, which Postgres rejected on the expires_at integer
column, crashing both the initial OAuth callback and the token
refresh flow with a 500.
Falls back to null when expires_in is absent, matching the
existing Gitea callback behavior.
Closes#4362
- Log tail for failed-deployment classification now checks the app's
current buildServer before falling back to its server, matching the
buildServerId-then-serverId precedence used everywhere else in the
codebase (application.ts, drop.ts, directory.ts, patch-repo.ts).
- Removes a stale !isCloud gate on the Networks tab that this branch
had reintroduced from before Mauricio removed it in daba2d7fb
(Networks is intentionally shown in cloud now) — unrelated to this
PR's scope, docker.tsx's Networks tab/content now matches canary.
Tightens a handful of two-line comments left over from earlier
iterations (Health tab / failed-deployment classification) down to
one line each, no behavior change.
Server diagnostics tab (Docker dashboard) with:
- Per-network IP usage: subnet capacity vs. containers in use, including
reserved networks like dokploy-network which are excluded from the
managed Networks tab. Surfaces read errors instead of silently showing
zeroes when the Docker API call fails.
- Broadened daemon-error detection and failure classification for Swarm
network failures: IP pool exhaustion on an existing network
("could not find an available IP", "task allocation failure") and
network attach timeouts ("attaching to network failed", "context
deadline exceeded") — both previously fell through as Unclassified.
Verified end-to-end against a real systemd-managed dockerd.
- Failed-deployment rows now carry the raw log tail used for
classification, shown on hover and linked to their service/project,
so a guess (or lack of one) can be verified against the actual error
and followed straight to the app.
- Daemon-error panel is a read-only code editor, always visible (not
hidden when empty), prefixed with the actual "Logs from X to Y"
window passed to journalctl so the --since window can be verified.
- "Download report" exports everything on screen (metrics + raw logs)
as Markdown, so a customer can send one file instead of a screen-share.
- Health check now IS_CLOUD-gated the same way network sync already is:
a cloud org can no longer omit serverId and have the check silently
run against the shared platform host instead of their own server.
Includes an inotify diagnostics card (current vs. max_user_instances/
watches/queued_events) and a failed-deployments log with automatic
cause classification based on daemon/log error patterns.
SCIM provisioning created users with emailVerified: false, so better-auth
refused to link the matching SSO account (account_not_linked), and the
resulting redirect landed on better-auth's bare /error page with no
actionable feedback.
- Mark SCIM-provisioned users as email-verified at creation time, same
trust rationale already applied to admin-invited credential users.
- Sync a SCIM user's membership role to the organization's configured
default role instead of the hardcoded "member" the SCIM plugin creates.
- Set errorCallbackURL/onAPIError.errorURL to the sign-in page so failed
SSO/OAuth callbacks land back on Dokploy's UI instead of better-auth's
generic error page.
- Read the error query param on the sign-in page and show it through the
existing AlertBlock instead of failing silently.
Fixes#4973
- Images tab: list, inspect and delete docker images, with a
force-delete fallback when an image is in use.
- Disk Usage tab: docker system df summary as stat cards plus a
Build Cache table (docker system df -v) with a prune action.
@aws-sdk/client-secrets-manager (pre-existing, unrelated to any recent
PR) transitively pulled @smithy/fetch-http-handler + @smithy/core
versions published the same day, with no older compatible version to
fall back to. AWS SDK v3 is dozens of interdependent packages often
published together same-day, so a global minimumReleaseAge with no
per-scope threshold (pnpm only supports all-or-nothing exclude) was
going to keep breaking builds on any lockfile touch. Commented out
rather than deleted -- left for whoever wants to revisit it, see
discussion on #4679.
Also lets @aws-sdk/client-route-53 and @aws-sdk/client-secrets-manager
resolve to their actual latest versions now that nothing blocks it.