- Onboarding wizard (Welcome -> Plan -> Project -> Server -> Deploy ->
Complete), shown once to an org owner with zero projects and no active
plan/trial; skippable per step or entirely
- Billing page shows the org's current plan, and a no-card 14-day trial
card when eligible
- Post-checkout "Welcome to Dokploy Cloud" modal simplified to reuse the
onboarding wizard's own project/server/deploy steps behind a modal
instead of its previous standalone 6-step flow, using the app's regular
typography instead of the wizard's display serif
- Onboarding wizard validates a persisted project still exists before
resuming a stale session, and the dashboard layout no longer gets stuck
redirecting to /dashboard/home once the local onboarding-active flag
goes stale mid-session
- onboardingCompletedAt column on user, with a backfill so existing users
aren't shown the wizard
- pnpm reset-onboarding dev script to reset a test account's onboarding
state end to end
findComposeById embedded the full github/gitlab/bitbucket/gitea
relations (client secrets, OAuth tokens, private keys, app passwords)
and compose.one only used canEditDeployGitSource to set a
hasGitProviderAccess flag, never to hide the fields — so any member
with read access to a compose service got the connected git
provider's raw credentials, regardless of their access to that
provider itself.
Exclude the same secret columns findApplicationById already excludes.
Deploys are unaffected: the actual clone step always re-fetches the
provider fresh by id (findGithubById/findGitlabById/...), it never
reads secrets off the embedded relation.
gitlab.one, github.one, gitea.one and bitbucket.one returned the full
DB record (OAuth access/refresh tokens, client secrets, private keys,
webhook secrets, app passwords) to any org member who merely had
access to *use* a shared provider (sharedWithOrganization: true),
not just its owner or an org owner/admin.
Add canViewGitProviderSecrets() and null out the secret fields in
each .one response when the caller isn't the provider owner or an
org owner/admin.
Show the services attached to a remote server directly in its delete
confirmation modal, with a link to each service and a per-service
delete action, instead of only showing a generic 'has active
services' blocker.
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
Adds Porkbun as a supported DNS provider alongside Cloudflare and
AWS Route53, allowing Dokploy to automatically create DNS records
for domains managed on Porkbun.
- New DnsClient implementation for the Porkbun API v3
- porkbun enum value and config schema (apiKey/secretApiKey)
- Drizzle migration for the new DnsProviderType enum value
- UI: provider icon, form fields and provider selector entry
- Unit tests covering listZones/listRecords/upsertRecord/updateRecord/deleteRecord/testConnection
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.
Route53 returns a record set as a list of values, but listing joined them
into one string and writing sent that string back as a single
ResourceRecord. Editing a multi-value NS, MX or TXT set therefore either
failed validation or collapsed the set into one bogus value, and creating
a record for a name that already had values replaced them silently.
Values are now newline separated end to end: listing joins with a
newline, writing splits back into one ResourceRecord per line, and
creating merges into the existing set instead of replacing it. Unquoted
TXT values get the quotes Route53 requires. The record panel shows a
textarea for Route53 and validates every line.
Cloudflare treats content as read-only for SRV and CAA and expects a
data object instead, so both types were rejected on write. The client
now parses the inline value into the fields Cloudflare wants, and
builds the payload before the lookup request so a malformed value
fails without spending an API call.
The form rejects a malformed SRV or CAA value up front and shows the
expected shape, so the error lands on the field instead of coming
back from the provider.
Providers, domains and records now each have their own page instead of
a stack of modals. A provider opens its domains as cards, with the
record count loading separately so the domains appear right away. A
domain opens its records as a table with search, type filter and
pagination. Creating or editing a record happens in a panel that
slides in next to the table.
The type list was capped at A and CNAME in the zod schema, so widening
the dropdown alone would not have worked. AAAA, MX, TXT, NS, SRV, CAA
and PTR work now. MX carries a priority that Cloudflare takes as a
separate field and Route53 takes inline in the value, so the
Cloudflare client splits it out on write and puts it back on read.
The form keeps one value field for both providers.
Cloudflare proxy status is now editable and visible. A, AAAA and CNAME
records get a Proxied / DNS only toggle in the form and a cloud icon
in the table. The proxy field only goes to the API when the caller
sets it, so an update from another path cannot silently disable the
proxy.
Tests cover the MX priority round trip and the proxy rules in the
Cloudflare client.
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.