Only escape $ signs that are not part of a well-formed ${VAR} reference
when writing .env files, so Docker Compose's own variable interpolation
still works for legitimate ASSET_URL=${APP_URL}-style references, while
literal dollar signs (e.g. in passwords) stay escaped.
Fixes#5151
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
The tag dialog's form is nested inside the project dialog's form in the
React tree, and React propagates events along the React tree rather than
the DOM tree, so submitting the tag form also submitted the project form.
The project was created immediately with no tags and the dialog closed
before more than one tag could be picked.
Stop propagation in the tag form's onSubmit, and select the new tag in
the selector so it is included when the project is saved.
Fixes#5117
Fixes#5108. The changePassword mutations for postgres/mysql/mariadb/mongo/redis
detected the running container via a shell one-liner (docker ps -q --filter
status=running | head -n1) executed through execAsync/execAsyncRemote. When that
shell command failed for any reason the CONTAINER_ID ended up empty and the
mutation incorrectly reported the container as not running even though it was.
Replaced it with getServiceContainer(), the existing dockerode-based lookup
already used by mount.ts and schedules/utils.ts for the same purpose.
Also fixes the postgres ALTER USER command, which never selected a database
and defaulted to a database named after the user, causing a false
'database "<user>" does not exist' failure whenever the db name differs
from the username.
Fixes#5092 — typing in the database/docker terminal overwrote text
instead of continuing on new lines. The xterm.js client never told the
backend PTY its actual size (node-pty/ssh2 always defaulted to 80x24
and were never resized), so whenever the rendered terminal width
differed from that, the remote shell's cursor math diverged from what
the client displayed.
- docker-container-terminal.ts / terminal.ts: pass the client's initial
cols/rows into node-pty spawn and ssh2 exec/shell, and resize the PTY
on a {type:"resize"} control message sent over the same ws channel.
- docker-terminal.tsx / settings/web-server/terminal.tsx: send cols/rows
on connect and on every term.onResize, and watch the container with a
ResizeObserver to refit on dialog/window resize.
Also fixes two related terminal/logs bugs surfaced while testing this:
- docker-terminal.tsx no longer opens a connection with the
"select-a-container" placeholder ID before a real container is
selected, and defers terminal creation a frame so React StrictMode's
dev-only phantom mount doesn't hit a known xterm.js dispose race
(xtermjs/xterm.js#5011, "Cannot read properties of undefined
(reading 'dimensions')").
- docker-logs-id.tsx (shared by application/compose/compose-stack logs)
had the same placeholder-ID issue, surfacing Docker's raw
"No such container: select-a-container" daemon error instead of a
proper empty state.
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.
Replaces the static quote in the onboarding layout's left panel with
an auto-rotating carousel of product highlights (Deploy / Manage /
Scale, 3 slides x 4 items), shown on the cloud signup page only.
- OnboardingLayout gets an optional leftPanel override, default
quote untouched everywhere else (login, invitation, password reset).
- Auto-advances every 6s, pauses on hover/focus, respects
prefers-reduced-motion, manual dot + arrow navigation.
- Copy pulled from the website's existing first-features.tsx section
to stay consistent with approved marketing copy.
Fires the sign_up dataLayer event already read by GTM for the three
signup paths on the cloud version: email/password (register.tsx),
GitHub/Google OAuth (via better-auth's newUserCallbackURL, distinct
from callbackURL so returning-user logins aren't counted), and
invitation acceptance. All gated behind isCloud, same as the rest of
the analytics wiring.
Mounts the existing cloud-gated Analytics component (GTM + HubSpot
script loader) in _app.tsx. It was already implemented behind
api.settings.isCloud but never wired into the app, so it never loaded
on cloud or self-hosted instances.
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