Compare commits

...

13 Commits

Author SHA1 Message Date
dosubot[bot]
0c5d49ca66 docs: Dosu updates for PR #3894 2026-03-05 06:48:56 +00:00
Mauricio Siu
0195119a86 Merge pull request #3894 from Dokploy/3888-deploy-error-client-version-153-is-too-new-on-synology-920
feat: enhance Docker configuration with environment variables for API…
2026-03-05 00:47:46 -06:00
Mauricio Siu
48a577e792 feat: enhance Docker configuration with environment variables for API version, host, and port 2026-03-05 00:46:13 -06:00
Mauricio Siu
bf7a75dd9f Merge pull request #3882 from aak-lear/fix/rollback-registry-auth
fix: add docker login before rollback and fix execAsyncRemote argument order
2026-03-04 22:11:04 -06:00
Mauricio Siu
d316aa4401 Merge pull request #3893 from Dokploy/3853-web-server-backup-fails-when-unreadable-files-unix-sockets-named-pipes-exist-under-etcdokploy
fix: update rsync command in web-server backup to exclude special fil…
2026-03-04 21:37:15 -06:00
Mauricio Siu
f1b2cc35b3 fix: update rsync command in web-server backup to exclude special files and devices 2026-03-04 21:21:46 -06:00
lear
d2fabc998d refactor: reuse safeDockerLoginCommand from registry.ts instead of duplicating shEscape 2026-03-04 12:45:57 +03:00
lear
7185047eb7 fix: add docker login before rollback and fix execAsyncRemote argument order 2026-03-04 11:07:42 +03:00
Mauricio Siu
7121fbe50a Merge pull request #3881 from Dokploy/3864-file-mount-content-not-updated-on-host-when-edited-in-advanced-tab-ui-wordpress-service
refactor: simplify createMount mutation by returning the promise dire…
2026-03-03 22:59:32 -06:00
Mauricio Siu
36cf3a69fc refactor: simplify createMount mutation by returning the promise directly
Updated the createMount mutation to return the promise from createMount directly, enhancing readability. Additionally, adjusted the serviceType schema definition for clarity by removing the default value assignment.
2026-03-03 22:55:46 -06:00
Mauricio Siu
c34a01a173 Merge pull request #3880 from Dokploy/3876-auth-session-ui-not-updating-after-profile-picture-change
refactor: replace authClient with api.organization.active for active …
2026-03-03 22:39:04 -06:00
Mauricio Siu
9ac147a140 refactor: replace authClient with api.organization.active for active organization queries
Updated components to use the new API method for fetching the active organization, improving consistency across the codebase. This change enhances maintainability and aligns with recent API updates.
2026-03-03 22:37:42 -06:00
Mauricio Siu
20f79ac655 fix: update import statements to include file extensions for consistency 2026-03-03 15:35:37 -06:00
15 changed files with 333 additions and 34 deletions

View File

@@ -62,6 +62,16 @@ pnpm install
cp apps/dokploy/.env.example apps/dokploy/.env
```
### Optional Docker Configuration
The following environment variables can be added to your `.env` file if you need custom Docker daemon configuration:
- **DOCKER_API_VERSION**: Specify which Docker API version to use (optional)
- **DOCKER_HOST**: Specify a custom Docker daemon host (optional)
- **DOCKER_PORT**: Specify a custom Docker daemon port (optional)
These variables are typically not needed for standard local development but can be useful if you need to connect to a remote Docker daemon or require a specific Docker API version.
## Requirements
- [Docker](/GUIDES.md#docker)
@@ -171,6 +181,11 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.39.1/pack-v0.
- **Issue Association:** For any significant change, it's highly recommended to open an issue first to discuss the proposed solution with the community and maintainers. This ensures alignment and avoids duplicated effort. If your PR resolves an existing issue, please link it in the description (e.g., `Fixes #123`, `Closes #456`).
- **Large Features:** Pull Requests that introduce very large or broad features **will not be accepted** unless the idea is first outlined and discussed in a GitHub issue. Large features should be designed together with the Dokploy team so the project stays coherent and moves in the same direction. Open an issue to propose and align on the design before implementing.
### Pull Request Guidelines
- **Keep PRs small and focused.** Avoid very large PRs; prefer several smaller PRs (e.g., one template or one logical change per PR). This speeds up review and keeps the history clear.
- **Test before submitting.** Any PR that has not been tested by the contributor will be closed. This keeps the PR queue tidy and ensures that only contributions that have been verified by their authors are considered.
Thank you for your contribution!
## Templates

View File

@@ -6,3 +6,249 @@ npm run dev
```
open http://localhost:3000
```
## Environment Variables
The API server requires the following environment variables for configuration:
### Inngest Configuration
Required for the GET /jobs endpoint to list deployment jobs:
- **INNGEST_BASE_URL** - The base URL for the Inngest instance
- Self-hosted: `http://localhost:8288`
- Production: `https://dev-inngest.dokploy.com`
- **INNGEST_SIGNING_KEY** - The signing key for authenticating with Inngest
Optional configuration for filtering and pagination:
- **INNGEST_EVENTS_RECEIVED_AFTER** (optional) - An RFC3339 timestamp to filter events received after a specific date (e.g., `2024-01-01T00:00:00Z`). If unset, no date filter is applied.
- **INNGEST_JOBS_MAX_EVENTS** (optional) - Maximum number of events to fetch when listing jobs. Default is 100, maximum is 10000. Used for pagination with cursor.
### Lemon Squeezy Integration
- **LEMON_SQUEEZY_API_KEY** - API key for Lemon Squeezy integration
- **LEMON_SQUEEZY_STORE_ID** - Store ID for Lemon Squeezy integration
### Docker Configuration
Optional configuration for customizing Docker daemon connections:
- **DOCKER_API_VERSION** (optional) - Specifies which Docker API version to use when connecting to the Docker daemon. If not set, the Docker client uses the default API version.
- **DOCKER_HOST** (optional) - Specifies the Docker daemon host to connect to. If not set, uses the default Docker socket connection.
- **DOCKER_PORT** (optional) - Specifies the port for connecting to the Docker daemon. If not set, uses the default port.
These variables allow advanced users to customize how the Dokploy API server connects to Docker, which can be useful for connecting to remote Docker daemons or using specific API versions.
## API Endpoints
### GET /jobs
Lists deployment jobs (Inngest runs) for a specified server.
**Query Parameters:**
- `serverId` (required) - The ID of the server to list deployment jobs for
**Response:**
Returns an array of deployment job objects with the same shape as BullMQ queue jobs:
```json
[
{
"id": "string",
"name": "string",
"data": {},
"timestamp": 0,
"processedOn": 0,
"finishedOn": 0,
"failedReason": "string",
"state": "string"
}
]
```
**Error Responses:**
- `400` - serverId is not provided
- `503` - INNGEST_BASE_URL is not configured
- `200` - Empty array on other errors
This endpoint is used by the UI to display deployment queue information in the dashboard.
## Search Endpoints
The following search endpoints provide flexible querying capabilities with pagination support. All search endpoints respect member permissions, returning only resources the user has access to.
### application.search
Search applications across name, appName, description, repository, owner, and dockerImage fields.
**Query Parameters:**
- `q` (optional string) - General search term that searches across name, appName, description, repository, owner, and dockerImage
- `name` (optional string) - Filter by application name
- `appName` (optional string) - Filter by app name
- `description` (optional string) - Filter by description
- `repository` (optional string) - Filter by repository
- `owner` (optional string) - Filter by owner
- `dockerImage` (optional string) - Filter by Docker image
- `projectId` (optional string) - Filter by project ID
- `environmentId` (optional string) - Filter by environment ID
- `limit` (number, default 20, min 1, max 100) - Maximum number of results
- `offset` (number, default 0, min 0) - Pagination offset
**Response:**
```json
{
"items": [
{
"applicationId": "string",
"name": "string",
"appName": "string",
"description": "string",
"environmentId": "string",
"applicationStatus": "string",
"sourceType": "string",
"createdAt": "string"
}
],
"total": 0
}
```
### compose.search
Search compose services with filtering by name, appName, and description.
**Query Parameters:**
- `q` (optional string) - General search term across name, appName, description
- `name` (optional string) - Filter by name
- `appName` (optional string) - Filter by app name
- `description` (optional string) - Filter by description
- `projectId` (optional string) - Filter by project ID
- `environmentId` (optional string) - Filter by environment ID
- `limit` (number, default 20, min 1, max 100) - Maximum results
- `offset` (number, default 0, min 0) - Pagination offset
**Response:**
```json
{
"items": [
{
"composeId": "string",
"name": "string",
"appName": "string",
"description": "string",
"environmentId": "string",
"composeStatus": "string",
"sourceType": "string",
"createdAt": "string"
}
],
"total": 0
}
```
### environment.search
Search environments by name and description.
**Query Parameters:**
- `q` (optional string) - General search term across name and description
- `name` (optional string) - Filter by name
- `description` (optional string) - Filter by description
- `projectId` (optional string) - Filter by project ID
- `limit` (number, default 20, min 1, max 100) - Maximum results
- `offset` (number, default 0, min 0) - Pagination offset
**Response:**
```json
{
"items": [
{
"environmentId": "string",
"name": "string",
"description": "string",
"createdAt": "string",
"env": "string",
"projectId": "string",
"isDefault": true
}
],
"total": 0
}
```
### project.search
Search projects by name and description.
**Query Parameters:**
- `q` (optional string) - General search term across name and description
- `name` (optional string) - Filter by name
- `description` (optional string) - Filter by description
- `limit` (number, default 20, min 1, max 100) - Maximum results
- `offset` (number, default 0, min 0) - Pagination offset
**Response:**
```json
{
"items": [
{
"projectId": "string",
"name": "string",
"description": "string",
"createdAt": "string",
"organizationId": "string",
"env": "string"
}
],
"total": 0
}
```
### Database Service Search Endpoints
The following database services all share the same search interface:
- **postgres.search**
- **mysql.search**
- **mariadb.search**
- **mongo.search**
- **redis.search**
**Query Parameters:**
- `q` (optional string) - General search term across name, appName, description
- `name` (optional string) - Filter by name
- `appName` (optional string) - Filter by app name
- `description` (optional string) - Filter by description
- `projectId` (optional string) - Filter by project ID
- `environmentId` (optional string) - Filter by environment ID
- `limit` (number, default 20, min 1, max 100) - Maximum results
- `offset` (number, default 0, min 0) - Pagination offset
**Response:**
```json
{
"items": [
{
"postgresId": "string",
"name": "string",
"appName": "string",
"description": "string",
"environmentId": "string",
"applicationStatus": "string",
"createdAt": "string"
}
],
"total": 0
}
```
*Note: The response shape is similar across all database services, with the ID field varying (e.g., `mysqlId`, `mariadbId`, `mongoId`, `redisId`).*
**Search Behavior:**
- All searches use case-insensitive pattern matching with wildcards
- Results are ordered by creation date (descending)
- Members only see services they have access to
- Returns total count for pagination UI

View File

@@ -1,4 +1,4 @@
import { logger } from "./logger";
import { logger } from "./logger.js";
const baseUrl = process.env.INNGEST_BASE_URL ?? "";
const signingKey = process.env.INNGEST_SIGNING_KEY ?? "";

View File

@@ -9,7 +9,7 @@ import {
updateCompose,
updatePreviewDeployment,
} from "@dokploy/server";
import type { DeployJob } from "./schema";
import type { DeployJob } from "./schema.js";
export const deploy = async (job: DeployJob) => {
try {

View File

@@ -24,7 +24,6 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
const organizationSchema = z.object({
@@ -55,8 +54,6 @@ export function AddOrganization({ organizationId }: Props) {
const { mutateAsync, isPending } = organizationId
? api.organization.update.useMutation()
: api.organization.create.useMutation();
const { refetch: refetchActiveOrganization } =
authClient.useActiveOrganization();
const form = useForm<OrganizationFormValues>({
resolver: zodResolver(organizationSchema),
@@ -89,7 +86,7 @@ export function AddOrganization({ organizationId }: Props) {
utils.organization.all.invalidate();
if (organizationId) {
utils.organization.one.invalidate({ organizationId });
refetchActiveOrganization();
utils.organization.active.invalidate();
}
setOpen(false);
})

View File

@@ -17,7 +17,8 @@ import { api } from "@/utils/api";
export const AddGithubProvider = () => {
const [isOpen, setIsOpen] = useState(false);
const { data: activeOrganization } = authClient.useActiveOrganization();
const { data: activeOrganization } = api.organization.active.useQuery();
const { data: session } = authClient.useSession();
const { data } = api.user.get.useQuery();
const [manifest, setManifest] = useState("");
@@ -52,7 +53,7 @@ export const AddGithubProvider = () => {
);
setManifest(manifest);
}, [data?.id, activeOrganization?.id, session?.user?.id]);
}, [activeOrganization?.id, session?.user?.id]);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
@@ -131,11 +132,7 @@ export const AddGithubProvider = () => {
Unsure if you already have an app?
</a>
<Button
disabled={
(isOrganization && organizationName.length < 1) ||
!activeOrganization?.id ||
!session?.user?.id
}
disabled={isOrganization && organizationName.length < 1}
type="submit"
className="self-end"
>

View File

@@ -55,7 +55,7 @@ export const AddInvitation = () => {
api.notification.getEmailProviders.useQuery();
const { mutateAsync: sendInvitation } = api.user.sendInvitation.useMutation();
const [error, setError] = useState<string | null>(null);
const { data: activeOrganization } = authClient.useActiveOrganization();
const { data: activeOrganization } = api.organization.active.useQuery();
const form = useForm<AddInvitation>({
defaultValues: {

View File

@@ -557,8 +557,7 @@ function SidebarLogo() {
const { mutateAsync: setDefaultOrganization, isPending: isSettingDefault } =
api.organization.setDefault.useMutation();
const { isMobile } = useSidebar();
const { data: activeOrganization } = authClient.useActiveOrganization();
const _utils = api.useUtils();
const { data: activeOrganization } = api.organization.active.useQuery();
const { data: invitations, refetch: refetchInvitations } =
api.user.getInvitations.useQuery();

View File

@@ -69,8 +69,7 @@ export const mountRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateMount)
.mutation(async ({ input }) => {
await createMount(input);
return true;
return await createMount(input);
}),
remove: protectedProcedure
.input(apiRemoveMount)

View File

@@ -355,4 +355,13 @@ export const organizationRouter = createTRPCRouter({
return { success: true };
}),
active: protectedProcedure.query(async ({ ctx }) => {
if (!ctx.session.activeOrganizationId) {
return null;
}
return await db.query.organization.findFirst({
where: eq(organization.id, ctx.session.activeOrganizationId),
});
}),
});

View File

@@ -2,8 +2,22 @@ import path from "node:path";
import Docker from "dockerode";
export const IS_CLOUD = process.env.IS_CLOUD === "true";
export const DOCKER_API_VERSION = process.env.DOCKER_API_VERSION;
export const DOCKER_HOST = process.env.DOCKER_HOST;
export const DOCKER_PORT = process.env.DOCKER_PORT;
export const CLEANUP_CRON_JOB = "50 23 * * *";
export const docker = new Docker();
export const docker = new Docker({
...(DOCKER_API_VERSION && {
version: DOCKER_API_VERSION,
}),
...(DOCKER_HOST && {
host: DOCKER_HOST,
}),
...(DOCKER_PORT && {
port: DOCKER_PORT,
}),
});
// When not set, use the legacy default so 2FA remains working for users who
// enabled it before BETTER_AUTH_SECRET was introduced .

View File

@@ -99,17 +99,15 @@ const createSchema = createInsertSchema(mounts, {
mountPath: z.string().min(1),
mountId: z.string().optional(),
filePath: z.string().optional(),
serviceType: z
.enum([
"application",
"postgres",
"mysql",
"mariadb",
"mongo",
"redis",
"compose",
])
.default("application"),
serviceType: z.enum([
"application",
"postgres",
"mysql",
"mariadb",
"mongo",
"redis",
"compose",
]),
});
export type ServiceType = NonNullable<

View File

@@ -16,7 +16,7 @@ function shEscape(s: string | undefined): string {
return `'${s.replace(/'/g, `'\\''`)}'`;
}
function safeDockerLoginCommand(
export function safeDockerLoginCommand(
registry: string | undefined,
user: string | undefined,
pass: string | undefined,

View File

@@ -23,7 +23,7 @@ import { findDeploymentById } from "./deployment";
import type { Mount } from "./mount";
import type { Port } from "./port";
import type { Project } from "./project";
import type { Registry } from "./registry";
import { type Registry, safeDockerLoginCommand } from "./registry";
export const createRollback = async (
input: z.infer<typeof createRollbackSchema>,
@@ -111,7 +111,7 @@ const deleteRollbackImage = async (image: string, serverId?: string | null) => {
const command = `docker image rm ${image} --force`;
if (serverId) {
await execAsyncRemote(command, serverId);
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
@@ -171,6 +171,23 @@ export const rollback = async (rollbackId: string) => {
);
};
const dockerLoginForRegistry = async (
registry: Registry,
serverId?: string | null,
) => {
const loginCommand = safeDockerLoginCommand(
registry.registryUrl,
registry.username,
registry.password,
);
if (serverId) {
await execAsyncRemote(serverId, loginCommand);
} else {
await execAsync(loginCommand);
}
};
const rollbackApplication = async (
appName: string,
image: string,
@@ -188,6 +205,14 @@ const rollbackApplication = async (
throw new Error("Full context is required for rollback");
}
// Ensure Docker daemon is authenticated with the rollback registry
// before updating the swarm service. The authconfig in CreateServiceOptions
// alone is not sufficient — Docker Swarm also relies on the daemon's
// cached credentials (~/.docker/config.json) to distribute auth to nodes.
if (fullContext.rollbackRegistry) {
await dockerLoginForRegistry(fullContext.rollbackRegistry, serverId);
}
const docker = await getRemoteDocker(serverId);
// Use the same configuration as mechanizeDockerContainer

View File

@@ -67,7 +67,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
await execAsync(cleanupCommand);
await execAsync(
`rsync -a --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`,
`rsync -a --ignore-errors --no-specials --no-devices ${BASE_PATH}/ ${tempDir}/filesystem/`,
);
writeStream.write("Copied filesystem to temp directory\n");