Skip to content

feat: add object storage support (MinIO, Garage, Alarik) - #5462

Open
itzzjustmateo wants to merge 13 commits into
Dokploy:canaryfrom
itzzjustmateo:feat/object-storage
Open

itzzjustmateo wants to merge 13 commits into
Dokploy:canaryfrom
itzzjustmateo:feat/object-storage

Conversation

@itzzjustmateo

@itzzjustmateo itzzjustmateo commented Sep 14, 2026

Copy link
Copy Markdown

Description

Adds object storage as a new service type in Dokploy, supporting MinIO, Garage, and Alarik providers.

What's included

  • Database schema for object storage services (migration 0198)
  • Service layer with deploy / update / remove / environment operations
  • tRPC router (objectstorage) wired into the API root
  • Create dialog with provider selection (MinIO, Garage, Alarik) and provider-specific defaults
  • Detail page with general settings, internal/external credentials, advanced settings, and update/delete actions
  • Integration across environments, breadcrumbs, overview, home totals, mounts, transfers, backups, and server queries
  • Official provider logos (MinIO, Garage, Alarik) used across the UI

Notes

  • MinIO was archived on April 25, 2026 — the create dialog recommends Garage or Alarik when MinIO is selected
  • Includes a small cleanup commit replacing @ts-ignore with @ts-expect-error and tidying configs

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

  • pnpm --filter @dokploy/server typecheck
  • pnpm --filter dokploy typecheck
  • Biome check on changed files
  • Migration applied and verified against a running database

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

RetriggerConfidence Score: 1/5

This PR is not safe to merge until cross-environment relocation is authorized correctly and default deployments and port updates can reliably produce runnable services.

Summary

This PR introduces object-storage services across persistence, API operations, Docker Swarm deployment, dashboard management, mounts, overview data, transfers, and backup-related type integration.

  • Adds MinIO, Garage, Alarik, and RustFS provider configuration and persistent mounts.
  • Adds object-storage creation, lifecycle, credentials, logs, resources, networks, and environment integration.
  • The review found blocking issues in target-environment authorization, provider startup command construction, and published-port validation.
  • Backup integration and generated-secret persistence remain incomplete.

Reviews (1) · Last reviewed commit: "chore: replace ts-ignore with ts-expect-..."

itzzjustmateo and others added 9 commits July 29, 2026 18:20
Updated README.md to enhance texts and visuals.
Add MinIO, Garage, and Alarik object storage support as a new service
type. Includes the database schema, migration 0198, service layer with
deploy/update/remove operations, tRPC router, and integration across
mounts, transfers, overview, backups, and server queries.
Add the object storage management interface: create dialog with
provider selection, detail page with general settings, credentials,
advanced settings, and update/delete actions. Wire object storage into
the environment page, breadcrumbs, overview, home totals, and service
management flows.
Add the official MinIO, Garage, and Alarik logos used across the
object storage UI.
Replace @ts-ignore comments with @ts-expect-error across the codebase,
sort UI component exports, fix shell escaping in server setup, guard
Stripe initialization, and update biome/gitignore/package configs.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 72bca30867

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +103 to +107
await fs.mkdir(configDir, { recursive: true });
await fs.writeFile(
configPath,
generateGarageConfig(region || "us-east-1"),
"utf-8",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Write Garage config on the target server

When a Garage service is assigned to a remote server, these filesystem calls still write garage.toml on the Dokploy web server. The subsequently created remote Docker service bind-mounts the same /etc/dokploy/.../garage.toml path on the target host, where the file was never created, so Garage deployments to remote servers fail when Swarm validates the bind source.

Useful? React with 👍 / 👎.

Comment on lines 66 to +70
redisId: text("redisId").references(() => redis.redisId, {
onDelete: "cascade",
}),
objectStorageId: text("objectStorageId").references(
() => objectstorage.objectStorageId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Authorize Object Storage mount operations

Adding Object Storage mounts here requires updating the mount router's remove, one, and update service-ID chains as well. Those chains omit mount.objectStorageId, so they skip checkServicePermissionAndAccess for every Object Storage mount; any authenticated user who retains or obtains a mount ID can read, modify, or delete it without the owning service's volume permission.

Useful? React with 👍 / 👎.

Comment on lines +96 to +100
const onSubmit = async (values: DockerProvider) => {
await mutateAsync({
externalPort: values.externalPort,
objectStorageId,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist the MinIO console port

The form exposes consolePort, but its save request only includes externalPort; the corresponding API schema and mutation also only accept/update externalPort. Since deployment publishes MinIO port 9001 only when os.consolePort is set, saving a console port silently discards it and leaves the displayed external console URL unreachable.

Useful? React with 👍 / 👎.

Comment on lines +394 to +396
await checkServicePermissionAndAccess(ctx, objectStorageId, {
service: ["create"],
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Scope object storage updates to the active organization

For a user who retains an Object Storage ID after losing access to its organization, checkServicePermissionAndAccess only validates membership and role in the currently active organization; owners and admins bypass its per-service list. This handler never verifies the object's owning project organization, unlike one and remove, so such a user can update another organization's Object Storage while active in an organization where they are privileged.

Useful? React with 👍 / 👎.

Comment on lines +39 to +41
return `metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "sqlite"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Persist Garage metadata alongside its data

Garage is configured to store its SQLite metadata separately under /var/lib/garage/meta, while the creation path adds only one default volume at /var/lib/garage/data. Each redeploy force-replaces the Swarm task, so the unmounted metadata directory is discarded even though the data volume survives; existing bucket and object metadata is then lost or inaccessible after a redeploy.

Useful? React with 👍 / 👎.

@imrja8

imrja8 commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

@greptile-apps review this.

Comment on lines +535 to +540
const updatedObjectStorage = await db
.update(objectstorageTable)
.set({
environmentId: input.targetEnvironmentId,
})
.where(eq(objectstorageTable.objectStorageId, input.objectStorageId))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Target environment is unauthorized

Both relocation paths authorize only the source service. The move mutation writes the caller-controlled targetEnvironmentId without loading or authorizing that environment, while the general update schema also accepts environmentId and forwards it directly. A member who can update one object-storage service can therefore move it into a known environment outside their permitted project or active organization. Restrict environmentId to the dedicated move operation and validate access to the target environment and project before updating it.

How this was verified: Both mutations check only the source object-storage ID before writing an unvalidated target environment ID to the service record.

Knowledge Base Used: Identity, permissions, and audit

Comment on lines +192 to +198
...(providerCommand && {
Command: providerCommand.split(" "),
}),
...(providerArgs &&
providerArgs.length > 0 && {
Args: providerArgs,
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Provider entrypoints are overridden

Provider startup defaults are written to Docker Swarm's ContainerSpec.Command, which replaces the image entrypoint instead of supplying arguments to it. With the default MinIO selection, server /data --console-address :9001 becomes an attempt to execute a binary named server, bypassing MinIO's image entrypoint. A newly created service therefore cannot start or converge. Preserve the provider image entrypoint and place these values in Args, or explicitly invoke the actual executable.

Knowledge Base Used: Managed databases and storage

Comment on lines +234 to +250
if (input.externalPort) {
const portCheck = await checkPortInUse(
input.externalPort,
objectStorage.serverId || undefined,
);
if (portCheck.isInUse) {
throw new TRPCError({
code: "CONFLICT",
message: `Port ${input.externalPort} is already in use by ${portCheck.conflictingContainer}`,
});
}
}

await updateObjectStorageById(input.objectStorageId, {
externalPort: input.externalPort,
consolePort: input.consolePort,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Published ports are misvalidated

This validates only externalPort, even though MinIO and RustFS publish consolePort independently. It also checks an unchanged external port against the currently running service, causing the service to conflict with itself. Saving an occupied console port—or simply resaving the current external port—therefore either persists an undeployable configuration or incorrectly returns a conflict. Validate every changed published port, reject duplicate external and console values, and skip values unchanged from the existing record.

Knowledge Base Used: Managed databases and storage

Comment on lines +118 to +122
} else if (provider === "alarik") {
const jwtSecret = crypto.randomBytes(32).toString("hex");
providerEnv = `ADMIN_USERNAME="${rootUser}"\nADMIN_PASSWORD="${rootPassword}"\nJWT="${jwtSecret}"\nALLOW_ACCOUNT_CREATION=true${
bucket ? `\nDEFAULT_BUCKETS="${bucket}"` : ""
}${env ? `\n${env}` : ""}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 JWT rotates on redeploy

Alarik's JWT signing secret is regenerated during every deployment instead of being stored with the service. A normal redeploy therefore invalidates all existing signed sessions and can make persisted authentication state unusable. Generate this secret once during creation and reuse a persisted encrypted value on later deployments.

Knowledge Base Used: Managed databases and storage

Comment thread README.md
<a href="https://youtu.be/mznYKPvhcfw">
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400"/>
</a>
![./.github/dokploy_yt.jpg](https://youtu.be/ELkPcuO5ebo?si=l6LcKhTDBoxu6DGW)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Tutorial image cannot render

The Markdown image syntax puts the local JPG in the alt text and the YouTube page in the image-source position. The README consequently tries to render a video page as an image and never displays the added thumbnail. Use the JPG as the image source and wrap it in a normal link to the video.

Suggested change
![./.github/dokploy_yt.jpg](https://youtu.be/ELkPcuO5ebo?si=l6LcKhTDBoxu6DGW)
[![Dokploy video tutorial](./.github/dokploy_yt.jpg)](https://youtu.be/ELkPcuO5ebo?si=l6LcKhTDBoxu6DGW)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@greptile-apps

greptile-apps Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Comments Outside Diff

These findings sit on lines the diff does not cover, so they could not be posted inline. Each one leaves this list once its file changes.

  • P2 Backup ownership is incomplete apps/dokploy/components/dashboard/application/volume-backups/handle-volume-backups.tsx:76

    The form now accepts objectstorage, but the volume-backup table has no objectStorageId, the runtime lookup cannot obtain the service's project, organization, app name, or remote server, and the component prop still excludes this type. An object-storage schedule cannot be associated through the UI, while a schedule created directly through the API can execute on the local host instead of the service's server. Complete the schema, relations, runtime helpers, and UI entry point together, or remove the unsupported enum value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants