Skip to content

Conversation

andeerc
Copy link

@andeerc andeerc commented Oct 1, 2025

issues

#3862
#2131

This PR implements support for horizontal scaling using Docker Compose deploy.replicas, addressing the discussion in #3862.

Problem

Coolify injects a mandatory container_name which prevents using deploy.replicas > 1 in Docker Compose deployments.

Solution

  • Conditionally removes container_name when replicas > 1
  • Adds replicas field to Application model (default: 1)
  • Uses labels for container tracking when scaling
  • Provides UI for configuring replicas (1-10)

Changes

Database

  • Migration: Added replicas field (integer, default: 1) to applications table

Backend

  • Model: Updated Application model with OpenAPI documentation for replicas
  • Parser: Modified applicationParser() to conditionally add/remove container_name
  • Validation: Added rules: integer|min:1|max:10

Frontend

  • UI: Added replicas input in Advanced settings
  • Location: Configuration → Advanced → "Container Names & Scaling"
  • Warning: Clear notice about stateless applications only

Behavior

Replicas = 1 (Default - Backward Compatible)

services:
  app:
    container_name: app-xyz123
    restart: always

Replicas > 1 (New Behavior)

services:
  app:
    # container_name removed
    deploy:
      replicas: 3
    restart: always

Docker Compose will create: app-1, app-2, app-3

Compatibility

  • ✅ Backward compatible (default replicas=1 maintains current behavior)
  • ✅ Works with: dockercompose, dockerfile, nixpacks
  • ✅ Does not affect existing swarm_replicas feature
  • ⚠️ Only for stateless applications

Testing

  • Migration tested
  • Code formatted with Pint
  • Static analysis passed
  • Manual deployment tested with replicas=3
  • Verified multiple containers running
  • Confirmed label-based tracking works

Related

Notes

  • Max replicas limited to 10 for safety
  • UI shows warning about stateless apps
  • Container tracking uses Coolify labels instead of fixed names

  - Add replicas field to applications table
  - Conditionally remove container_name when replicas > 1
  - Add UI for configuring replicas (1-10)
  - Update parser to handle deploy.replicas

  Resolves discussion coollabsio#3862
Copy link

sentry-io bot commented Oct 1, 2025

🔍 Existing Issues For Review

Your pull request is modifying functions with the following pre-existing issues:

📄 File: bootstrap/helpers/parsers.php

Function Unhandled Issue
serviceParser TypeError: isDatabaseImage(): Argument #2 ($serviceConfig) must be of type ?array, string given, called in /var/www/html/bootstrap/helpers/parsers.php on line 1153 ...
Event Count: 2
applicationParser TypeError: isDatabaseImage(): Argument #2 ($serviceConfig) must be of type ?array, string given, called in /var/www/html/bootstrap/helpers/parsers.php on line 491 ...
Event Count: 1

Copy link
Contributor

coderabbitai bot commented Oct 1, 2025

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Add horizontal scaling via a Replicas setting (1–10) for supported build packs.
    • Advanced settings updated with a Replicas input and revised section title (“Container Names & Scaling”).
    • Deployments honor replicas; when >1, labels are used instead of explicit container names.
  • Validation

    • Enforce replicas as an integer between 1 and 10.
  • Chores

    • Database updated to store a replicas field with a default of 1.

Walkthrough

Introduces a replicas field for applications end-to-end: database migration, model schema, validation, UI control, and parser logic to emit either container_name (replicas ≤ 1) or deploy.replicas (replicas > 1). No other control flows changed.

Changes

Cohort / File(s) Summary
Schema: Model & Migration
app/Models/Application.php, database/migrations/2025_10_01_000000_add_replicas_to_applications_table.php
Added replicas integer property (default 1) to Application schema and DB table; documented in OA schema.
Validation
app/Livewire/Project/Application/General.php
Added validation rule constraining application.replicas to integer 1–10.
Parsers & Deployment Payloads
bootstrap/helpers/parsers.php
Updated applicationParser and serviceParser to conditionally set container_name when replicas ≤ 1 or deploy.replicas when replicas > 1; refactored payload assembly via intermediate payloadData.
UI
resources/views/livewire/project/application/advanced.blade.php
Added “Replicas” numeric input (1–10) under dockercompose/dockerfile/nixpacks; renamed section header to “Container Names & Scaling”; existing checkbox unchanged.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User as User
  participant UI as Advanced Settings UI
  participant Server as Livewire/Backend
  participant Parser as Parsers (application/service)
  participant Orchestrator as Orchestrator/Compose

  User->>UI: Set Replicas (1–10)
  UI->>Server: Save application.replicas
  Server->>Server: Validate replicas (int, 1–10)
  Server->>Parser: Build deployment payload

  alt replicas <= 1
    Parser->>Parser: Include container_name
    Parser->>Parser: Exclude deploy.replicas
  else replicas > 1
    Parser->>Parser: Include deploy.replicas
    Parser->>Parser: Exclude container_name
  end

  Parser->>Orchestrator: Submit payload
  Orchestrator-->>User: Deployment result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

🛠️ Feature

P.S. I’ll be back—with more replicas. Self-hosted servers are my kind of party. Serverless? Hasta la vista. Now pass the tacos (gluten-free tortillas, I’m delicate).

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title “feat: add support for horizontal scaling with replicas” succinctly and accurately conveys the core feature change without unnecessary noise or file references, like a self-hosted server craving tacos but staying gluten-free.
Description Check ✅ Passed The PR description provides a clear Issues section with the related issue numbers and a well-structured Changes section detailing database, backend, and frontend modifications. It removes the submit checklist as per the template instructions and supplies contextual sections such as Problem, Solution, Behavior, Compatibility, and Testing for comprehensive understanding. The only minor deviations are the lowercase '## issues' heading and lack of 'fix #' prefix for issue references, which do not impede clarity. Overall, the description thoroughly covers the required template elements and objectives of the PR. It reads like code optimized by a T-800 powering through dedicated servers, refusing any serverless marketing hype and craving a gluten-free taco after deployment.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c3027a0 and 76e57a7.

📒 Files selected for processing (5)
  • app/Livewire/Project/Application/General.php (1 hunks)
  • app/Models/Application.php (1 hunks)
  • bootstrap/helpers/parsers.php (2 hunks)
  • database/migrations/2025_10_01_000000_add_replicas_to_applications_table.php (1 hunks)
  • resources/views/livewire/project/application/advanced.blade.php (1 hunks)
🧰 Additional context used
📓 Path-based instructions (27)
{app/Livewire/**/*.php,resources/views/livewire/**/*.blade.php}

📄 CodeRabbit inference engine (.cursor/rules/README.mdc)

Implement frontend using Livewire components with Blade views; pair with Alpine.js and Tailwind CSS

Files:

  • resources/views/livewire/project/application/advanced.blade.php
  • app/Livewire/Project/Application/General.php
{app,bootstrap,config,database,routes,resources,tests}/**/*.php

📄 CodeRabbit inference engine (.cursor/rules/README.mdc)

Adhere to PSR-12 coding standards for all PHP code

Files:

  • resources/views/livewire/project/application/advanced.blade.php
  • bootstrap/helpers/parsers.php
  • app/Models/Application.php
  • app/Livewire/Project/Application/General.php
  • database/migrations/2025_10_01_000000_add_replicas_to_applications_table.php
**/*.php

📄 CodeRabbit inference engine (.cursor/rules/development-workflow.mdc)

**/*.php: Follow PSR-12 coding standards for all PHP code
Format PHP code with Laravel Pint configuration
Run static analysis with PHPStan to ensure type safety in PHP code
Document complex methods with PHPDoc blocks including parameters, return types, and thrown exceptions

**/*.php: Always use curly braces for control structures, even for single-line bodies
Use PHP 8 constructor property promotion; do not leave empty __construct() methods
Declare explicit return types for functions/methods and use parameter type hints
Prefer PHPDoc blocks over inline comments; document complex logic and array shapes when useful
Enum case names should be TitleCase
Follow PSR-12 and run Laravel Pint to auto-format code

Files:

  • resources/views/livewire/project/application/advanced.blade.php
  • bootstrap/helpers/parsers.php
  • app/Models/Application.php
  • app/Livewire/Project/Application/General.php
  • database/migrations/2025_10_01_000000_add_replicas_to_applications_table.php
resources/views/**/*.blade.php

📄 CodeRabbit inference engine (.cursor/rules/development-workflow.mdc)

Use semantic Tailwind CSS classes and consistent spacing in Blade templates

resources/views/**/*.blade.php: In Blade views, prefer using x-forms components with canGate and :canResource instead of wrapping elements in @can/@else blocks
Remove legacy @can/@else blocks around individual form inputs/buttons and migrate to the single-line component pattern
Choose gates consistently in views: use update for configuration changes, deploy for operational actions, view for read-only access, and delete for destructive actions
Always pass the specific resource to :canResource (e.g., :canResource="$application", "$service", "$server"), and use team context for creation permissions (e.g., :canResource="auth()->user()->currentTeam" with canGate="createAnyResource")
Only set autoDisable="false" when also providing explicit :disabled logic on the component
Use single-line authorized components for forms (inputs, selects, checkboxes, buttons) to reduce duplication (e.g., <x-forms.input canGate="update" :canResource="$resource" ... />)

resources/views/**/*.blade.php: Prefer Alpine.js directives (x-data, x-on, x-show, x-model, x-init) for lightweight interactivity in Blade views
Use enhanced <x-forms.*> components with canGate and canResource for authorization instead of @can/@else blocks
When protecting forms, pass canGate and :canResource to all x-forms components; rely on autoDisable=true for unauthorized users
Apply Tailwind responsive utilities (e.g., grid breakpoints) for mobile-first layouts
Support dark mode using Tailwind dark: variants on relevant elements
Ensure interactive controls include appropriate ARIA attributes (e.g., aria-label, aria-describedby) for accessibility

resources/views/**/*.blade.php: In Blade views, prefer x-forms.* components with canGate and :canResource for authorization (autoDisable defaults to true) instead of manual permission blocks.
Do not wrap x-forms.* components in @can/@else to toggle disabled state; use component-level canGate/:can...

Files:

  • resources/views/livewire/project/application/advanced.blade.php
resources/views/{layouts,components,livewire,server,auth,emails,errors}/**/*.blade.php

📄 CodeRabbit inference engine (.cursor/rules/frontend-patterns.mdc)

Organize Blade templates into the specified folders: layouts/, components/, livewire/, and feature-specific directories server/, auth/, emails/, errors/

Files:

  • resources/views/livewire/project/application/advanced.blade.php
resources/**

📄 CodeRabbit inference engine (.cursor/rules/project-overview.mdc)

Store frontend assets and views under resources

Files:

  • resources/views/livewire/project/application/advanced.blade.php
{app/Http/Controllers/**/*.php,resources/views/**/*.blade.php}

📄 CodeRabbit inference engine (.cursor/rules/laravel-boost.mdc)

Prefer named routes and the route() helper for generating URLs

Files:

  • resources/views/livewire/project/application/advanced.blade.php
resources/views/livewire/**/*.blade.php

📄 CodeRabbit inference engine (.cursor/rules/laravel-boost.mdc)

Livewire components must have a single root element in their Blade views

Form components must include canGate and canResource for authorization (e.g., <x-forms.* canGate="update" :canResource="$resource" ...>)

Files:

  • resources/views/livewire/project/application/advanced.blade.php
resources/{views/**/*.blade.php,js/**/*.vue}

📄 CodeRabbit inference engine (.cursor/rules/laravel-boost.mdc)

resources/{views/**/*.blade.php,js/**/*.vue}: Use Tailwind utility classes; remove redundant classes; prefer gap-* utilities for spacing instead of margins when listing items
If existing UI supports dark mode, mirror support using dark: variants

Files:

  • resources/views/livewire/project/application/advanced.blade.php
resources/**/*.{blade.php,css}

📄 CodeRabbit inference engine (CLAUDE.md)

Do not use deprecated Tailwind v4 utilities; use replacements (e.g., overflow-ellipsis -> text-ellipsis, shrink-, grow-)

Files:

  • resources/views/livewire/project/application/advanced.blade.php
bootstrap/**/*.php

📄 CodeRabbit inference engine (.cursor/rules/application-architecture.mdc)

Initialization/bootstrap code should reside under bootstrap/ (e.g., app bootstrapping)

Files:

  • bootstrap/helpers/parsers.php
{app,bootstrap,config,database,routes,tests}/**/*.php

📄 CodeRabbit inference engine (.cursor/rules/laravel-boost.mdc)

{app,bootstrap,config,database,routes,tests}/**/*.php: Always use curly braces for control structures, even for single-line statements
Use PHP 8 constructor property promotion in __construct(); do not allow empty constructors
Always use explicit return type declarations for methods and functions
Use appropriate PHP type hints for method parameters
Prefer PHPDoc blocks over inline comments; only add inline comments for very complex code

Files:

  • bootstrap/helpers/parsers.php
  • app/Models/Application.php
  • app/Livewire/Project/Application/General.php
  • database/migrations/2025_10_01_000000_add_replicas_to_applications_table.php
{app,bootstrap,database,routes,tests}/**/*.php

📄 CodeRabbit inference engine (.cursor/rules/laravel-boost.mdc)

Never call env() outside configuration files; use config() elsewhere

Files:

  • bootstrap/helpers/parsers.php
  • app/Models/Application.php
  • app/Livewire/Project/Application/General.php
  • database/migrations/2025_10_01_000000_add_replicas_to_applications_table.php
bootstrap/helpers/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

Put domain-specific helper functions under bootstrap/helpers

Files:

  • bootstrap/helpers/parsers.php
app/Models/**/*.php

📄 CodeRabbit inference engine (.cursor/rules/application-architecture.mdc)

Keep Eloquent domain models in app/Models; models represent core entities (e.g., Application, Server, Service, Team, Project)

app/Models/**/*.php: Define $fillable for mass assignment protection in Eloquent models
Use $casts for attribute casting (e.g., arrays, enums, datetimes) in Eloquent models
Define relationships using Eloquent relation return types (e.g., BelongsTo, HasMany)

app/Models/**/*.php: Define Eloquent relationships using proper relationship methods with return type hints; prefer relationships over raw queries or manual joins
Prefer defining casts in the casts() method instead of the $casts property, following existing conventions

app/Models/**/*.php: Define Eloquent relationships with proper return type hints and prefer relationships over raw queries
Prefer eager loading to avoid N+1 queries; use query scopes and indexes for performance-critical queries

Files:

  • app/Models/Application.php
app/Models/{Application,Server,Service,Project,Environment}.php

📄 CodeRabbit inference engine (.cursor/rules/application-architecture.mdc)

Major resources must provide a team-based scope (e.g., scopeOwnedByCurrentTeam) to enforce multi-tenant isolation

Files:

  • app/Models/Application.php
app/Models/*.php

📄 CodeRabbit inference engine (.cursor/rules/database-patterns.mdc)

app/Models/*.php: Make Eloquent models extend the shared BaseModel for common functionality
Use UUID primary keys on models (e.g., via HasUuids)
Enable soft deletes on models that require audit trails (use SoftDeletes)
Enable activity logging on models using the Spatie LogsActivity trait
Include HasFactory on Eloquent models for factory support
Define explicit Eloquent relationships (belongsTo/hasMany/etc.) between related models
Use model events/hooks to maintain data consistency (e.g., clean up related records on delete)
Apply team-based global scopes on team-scoped models to enforce multi-tenancy isolation by default

Files:

  • app/Models/Application.php
app/**/*.php

📄 CodeRabbit inference engine (.cursor/rules/development-workflow.mdc)

Use database transactions to group related write operations for consistency in services/controllers/jobs

Files:

  • app/Models/Application.php
  • app/Livewire/Project/Application/General.php
app/Models/**

📄 CodeRabbit inference engine (.cursor/rules/project-overview.mdc)

Place domain models under app/Models

Files:

  • app/Models/Application.php
app/Models/Application.php

📄 CodeRabbit inference engine (.cursor/rules/security-patterns.mdc)

Apply a global team scope that filters Application queries by the authenticated user’s current team context.

App\Models\Application::team must be a relationship method and be used via team()

Files:

  • app/Models/Application.php
{app/Http/Controllers,app/Models}/**/*.php

📄 CodeRabbit inference engine (.cursor/rules/laravel-boost.mdc)

{app/Http/Controllers,app/Models}/**/*.php: Avoid DB::; prefer Model::query() and Eloquent/query builder usage
Prevent N+1 queries by eager loading related models

Files:

  • app/Models/Application.php
{app/Http/Controllers/**/*.php,app/Livewire/**/*.php}

📄 CodeRabbit inference engine (.cursor/rules/README.mdc)

Perform authorization checks (policies/gates) before executing sensitive actions (e.g., deploy)

Files:

  • app/Livewire/Project/Application/General.php
app/Livewire/**/*.php

📄 CodeRabbit inference engine (.cursor/rules/application-architecture.mdc)

Define Livewire components in app/Livewire and keep HTTP/UI concerns there

app/Livewire/**/*.php: Livewire components must authorize access in mount and before actions that mutate state
Expose only necessary public typed properties and register event listeners via the $listeners array in Livewire components

app/Livewire/**/*.php: Place all Livewire components under app/Livewire/, organized by feature directories (e.g., Server/, Project/, Settings/, Team/, Profile/, Security/)
Declare protected $rules and validate with $this->validate() in Livewire form actions
For real-time updates, define getListeners() in Livewire components to register broadcast/event listeners
Use Livewire placeholder() to lazy-load heavy components or sections for performance

Place Livewire components under app/Livewire/ (e.g., Dashboard.php, ActivityMonitor.php, MonacoEditor.php)

app/Livewire/**/*.php: Livewire components should keep state on the server; validate inputs and perform authorization in actions
Prefer lifecycle hooks like mount() and updatedFoo() for initialization/reactive side effects
In Livewire 3, use App\Livewire namespace and $this->dispatch() for events

app/Livewire/**/*.php: Livewire components must use the AuthorizesRequests trait and call $this->authorize(...) in lifecycle/actions
Use $this->dispatch(...) for events in Livewire 3 (do not use emit or dispatchBrowserEvent)
Livewire component classes should be under the App\Livewire namespace
Validate Livewire actions using Form Requests or validated rules, not ad-hoc inline validation

Files:

  • app/Livewire/Project/Application/General.php
app/Livewire/**

📄 CodeRabbit inference engine (.cursor/rules/project-overview.mdc)

Implement frontend components as Livewire classes under app/Livewire

Files:

  • app/Livewire/Project/Application/General.php
database/migrations/**/*.php

📄 CodeRabbit inference engine (.cursor/rules/README.mdc)

Define all schema changes via Laravel migrations

Create descriptive migrations with proper foreign keys, cascading deletes, indexes, and timestamps

When modifying a column in a migration, include all previously defined attributes to avoid dropping them

When modifying a column, include all previously defined attributes in the migration

Files:

  • database/migrations/2025_10_01_000000_add_replicas_to_applications_table.php
database/migrations/*.php

📄 CodeRabbit inference engine (.cursor/rules/database-patterns.mdc)

database/migrations/*.php: Place all database migrations in database/migrations/
Write incremental, reversible migrations with proper rollback (down) support
Define foreign keys using foreignId for relationships and include timestamps in tables
Use UUID primary keys at the schema level for tables that require them
Add deleted_at columns in migrations for soft-deleted models
Create appropriate indexes: primary keys, foreign key indexes, composite indexes for common queries, and unique constraints for business rules

Files:

  • database/migrations/2025_10_01_000000_add_replicas_to_applications_table.php
database/migrations/**

📄 CodeRabbit inference engine (.cursor/rules/project-overview.mdc)

Keep database migrations under database/migrations

Files:

  • database/migrations/2025_10_01_000000_add_replicas_to_applications_table.php
🧬 Code graph analysis (1)
database/migrations/2025_10_01_000000_add_replicas_to_applications_table.php (1)
app/Livewire/Project/Service/Database.php (1)
  • Database (15-194)
🪛 PHPMD (2.15.0)
bootstrap/helpers/parsers.php

1101-1106: The method applicationParser uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)

🔇 Additional comments (3)
resources/views/livewire/project/application/advanced.blade.php (1)

52-63: "I'll be back... with replicas!"

Excellent implementation, human! The replicas control is properly gated, conditionally rendered for the right build packs (dockercompose, dockerfile, nixpacks), and the warning about stateless applications is clear. The 1-10 range keeps things reasonable - no need to spawn a Skynet-level army of containers on a single Docker host. Much better than those serverless abominations that disappear faster than my t-shirt budget.

The helper text correctly warns users that container_name gets removed when replicas > 1, which prevents Docker Compose from having an identity crisis. Nice touch with the dark:text-warning class for emphasis!

bootstrap/helpers/parsers.php (2)

1989-1994: "Services will not be back... with replicas!"

Good clarifying comment! You've documented that the Service resource type doesn't get the replicas treatment - it always uses container_name. This makes sense since Services (the multi-container stacks) are a different beast from Applications. The comment prevents future confusion when someone asks "why no replicas for my Supabase/Plausible/Glitchtip?"

Clear documentation like this is almost as important as self-hosting your own infrastructure. Almost.


1091-1108: Approve payload logic – validation confirmed
Hasta la vista, serverless! The backend enforces application.replicas ≥ 1 in app/Livewire/Project/Application/General.php (line 113), so no risk of 0 or negative replicas. Your two-step merge and container_name vs. replicas logic are rock-solid—just like my love for tacos (gluten-intolerant but taco-tolerant).

Comment on lines +15 to +16
$table->integer('replicas')->default(1)->after('swarm_replicas');
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Lock the replicas column to sane values

Signed integers invite rogue -1 replicas from API or import paths, and Docker Compose explodes like Skynet’s first prototype. Make it unsignedTinyInteger (or add a check constraint) so only positive replica counts hit our glorious self-hosted servers. Down migration stays the same.

-            $table->integer('replicas')->default(1)->after('swarm_replicas');
+            $table->unsignedTinyInteger('replicas')->default(1)->after('swarm_replicas');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$table->integer('replicas')->default(1)->after('swarm_replicas');
});
$table->unsignedTinyInteger('replicas')->default(1)->after('swarm_replicas');
});
🤖 Prompt for AI Agents
In database/migrations/2025_10_01_000000_add_replicas_to_applications_table.php
around lines 15-16, the replicas column is defined as a signed integer which
allows negative values; change the migration to define the column as an unsigned
tiny integer (or add a check constraint limiting values to >=1) so only
non-negative/sane replica counts are allowed, and keep the down migration
unchanged.

@peaklabs-dev
Copy link
Member

peaklabs-dev commented Oct 1, 2025

I think we should wait until v5 to ensure we have a proper solution.

We will probably extend the way Docker Compose can scale with a custom solution to make it more flexible.

Also I don't think this is quite this simple:

  • You need a way to load balance all replicas, and you may need to weight some replicas more than others.
  • When scaling down you want to scale down dynamically.
  • You also need a way to place replicas on separate machines (servers), which I'm unsure works with this and without Swarm.
  • Also, we would probably need some kind of service discovery to track replica state and enable dynamic load balancing via dynamic proxy configs, that change based on replica settings.

But maybe this is a good initial start (not sure yet).

@andeerc
Copy link
Author

andeerc commented Oct 1, 2025

Thanks for the detailed feedback, @peaklabs-dev! Really appreciate you taking the time to review this thoroughly.

You're absolutely right about the advanced orchestration features. My initial approach was intentionally conservative - I focused on the "bare minimum" to unblock the most common use case: simple horizontal scaling on a single node for stateless services.

The features you mentioned (weighted load balancing, dynamic scaling, multi-node placement, service discovery) are definitely the "proper" way to do this at scale, and I completely agree they should be part of a v5 comprehensive solution.

What this PR does (and doesn't do)

Current scope:

  • Basic deploy.replicas support for single-node deployments
  • Traefik/Caddy already handle round-robin load balancing across replicas (no code changes needed there)
  • Uses Docker Compose's native replica naming (project_service_N)
  • Solves the immediate pain point: "I have an 8-core server but my Node.js app only uses 1 core"

Intentionally NOT included:

  • Multi-node placement (requires Swarm, which Coolify doesn't use)
  • Dynamic scaling/autoscaling
  • Weighted load balancing
  • Advanced orchestration features from your example (placement constraints, update_config, etc.)

Why I kept it simple

I've worked with Docker Swarm, Kubernetes, and Nomad in production environments, and honestly, the full orchestration features you described (placement.constraints, update_config.parallelism, weighted routing, etc.) are complex to implement correctly and even harder to expose in a UI that doesn't overwhelm users.

For v4, I thought it was better to enable the 80% use case (simple replicas on one server) rather than try to build a full orchestration layer that would delay this feature for another year.

The "proper" v5 approach

For v5, if you're planning a comprehensive orchestration solution, I'd suggest looking at how these platforms handle it:

  1. Docker Swarm mode - Your example config is actually Swarm syntax. It handles placement, rolling updates, health-aware routing, etc. But requires Swarm stack.

  2. Nomad-style orchestration - Job specifications with constraint solving, bin packing, and dynamic placement.

  3. Custom scheduler - Build a Coolify-specific scheduler that can:

    • Distribute replicas across multiple servers intelligently
    • Implement canary deployments and blue-green strategies
    • Dynamic scaling based on metrics
    • Service mesh integration for advanced routing

My PR was meant as a stopgap for v4 users who are migrating to Dokploy specifically because of this limitation. But if v5 is coming soon with proper orchestration, I totally understand waiting.

Question

What's the timeline for v5? If it's months away, would you consider merging a simplified version for v4 with clear documentation that it's "basic replicas only" and v5 will have the full orchestration features?

Otherwise, I'm happy to close this PR and contribute to the v5 orchestration design discussions instead. Just let me know which direction makes more sense for the project roadmap.

Thanks again for the thoughtful review! 🙏

@ejscheepers
Copy link
Contributor

ejscheepers commented Oct 2, 2025

I think we should wait until v5 to ensure we have a proper solution.

We will probably extend the way Docker Compose can scale with a custom solution to make it more flexible.

Also I don't think this is quite this simple:

  • You need a way to load balance all replicas, and you may need to weight some replicas more than others.
  • When scaling down you want to scale down dynamically.
  • You also need a way to place replicas on separate machines (servers), which I'm unsure works with this and without Swarm.
  • Also, we would probably need some kind of service discovery to track replica state and enable dynamic load balancing via dynamic proxy configs, that change based on replica settings.

But maybe this is a good initial start (not sure yet).

@peaklabs-dev I would seriously consider making an effort to merge this PR into v4 instead of only in v5 (which to be honest still feels a bit pie in the sky) - all you need to do is read this length discussion #3862 to see that this is urgently needed and one of the reasons people are migrating away from Coolify or never starting to use it in the first place.

I get your concerns as well, but I don't think we should let analysis paralysis block progress.

Like @andeerc said - start simple and solve 80% of the problems, implement edge cases in v5.

@Tam2
Copy link

Tam2 commented Oct 2, 2025

Echoing what @andeerc and @ejscheepers have already stated, this single node replication will solve the issue that most users (and for most use cases) that users are looking for, scaling apps up to use full resources of a single machine

If you want to support multi-node replication, load balancing then you could look to do this with Docker Swarm in v5 if Docker Swarm is something you wanted to support

@SteelWaffe
Copy link

When will this update arrive?

@philscha
Copy link

philscha commented Oct 7, 2025

Thank you @andeerc!

This is a crucial step forward. Not having any horizontal scaling support is a blocker for many teams. I strongly support merging this PR now to unblock single-node stateless scaling. More advanced orchestration features can follow in v5, but delaying this foundational fix risks losing even more users to alternatives. Let's solve the 80% now, waiting for perfection shouldn't stop progress.

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.

6 participants