feat: add object storage support (MinIO, Garage, Alarik) - #5462
itzzjustmateo wants to merge 13 commits into
Conversation
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.
There was a problem hiding this comment.
💡 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".
| await fs.mkdir(configDir, { recursive: true }); | ||
| await fs.writeFile( | ||
| configPath, | ||
| generateGarageConfig(region || "us-east-1"), | ||
| "utf-8", |
There was a problem hiding this comment.
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 👍 / 👎.
| redisId: text("redisId").references(() => redis.redisId, { | ||
| onDelete: "cascade", | ||
| }), | ||
| objectStorageId: text("objectStorageId").references( | ||
| () => objectstorage.objectStorageId, |
There was a problem hiding this comment.
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 👍 / 👎.
| const onSubmit = async (values: DockerProvider) => { | ||
| await mutateAsync({ | ||
| externalPort: values.externalPort, | ||
| objectStorageId, | ||
| }) |
There was a problem hiding this comment.
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 👍 / 👎.
| await checkServicePermissionAndAccess(ctx, objectStorageId, { | ||
| service: ["create"], | ||
| }); |
There was a problem hiding this comment.
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 👍 / 👎.
| return `metadata_dir = "/var/lib/garage/meta" | ||
| data_dir = "/var/lib/garage/data" | ||
| db_engine = "sqlite" |
There was a problem hiding this comment.
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 👍 / 👎.
|
@greptile-apps review this. |
| const updatedObjectStorage = await db | ||
| .update(objectstorageTable) | ||
| .set({ | ||
| environmentId: input.targetEnvironmentId, | ||
| }) | ||
| .where(eq(objectstorageTable.objectStorageId, input.objectStorageId)) |
There was a problem hiding this comment.
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
| ...(providerCommand && { | ||
| Command: providerCommand.split(" "), | ||
| }), | ||
| ...(providerArgs && | ||
| providerArgs.length > 0 && { | ||
| Args: providerArgs, | ||
| }), |
There was a problem hiding this comment.
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
| 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, | ||
| }); |
There was a problem hiding this comment.
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
| } 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}` : ""}`; |
There was a problem hiding this comment.
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
| <a href="https://youtu.be/mznYKPvhcfw"> | ||
| <img src="https://dokploy.com/banner.png" alt="Watch the video" width="400"/> | ||
| </a> | ||
|  |
There was a problem hiding this comment.
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.
|  | |
| [](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!
Comments Outside DiffThese 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.
|
Description
Adds object storage as a new service type in Dokploy, supporting MinIO, Garage, and Alarik providers.
What's included
0198)objectstorage) wired into the API rootNotes
@ts-ignorewith@ts-expect-errorand tidying configsType of Change
How Has This Been Tested?
pnpm --filter @dokploy/server typecheckpnpm --filter dokploy typecheckChecklist
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.
Reviews (1) · Last reviewed commit: "chore: replace ts-ignore with ts-expect-..."