# Add Multiple Domains to Coolify Applications Without Redeployment Using Traefik File Provider

> Learn how we implemented a dynamic domain management system in Coolify that allows adding and removing multiple domains to applications without redeployment using Traefik's file provider for instant routing updates.

Source: https://edesy.in/blog/dynamic-domain-management-coolify

If you're using Coolify for application deployment, you've probably encountered a common pain point: adding or changing domains requires a complete application redeployment. This means downtime, waiting for builds, and potential deployment failures just to add a simple domain alias.

We forked Coolify and built a solution that eliminates this problem entirely. Now you can add, remove, or modify multiple domains for your applications instantly without any redeployment using Traefik's file-based routing system.

In this deep dive, I'll show you exactly how we implemented this feature and how it works under the hood.

## The Problem

In the standard Coolify workflow, domains are configured through Docker container labels. When you want to add or change a domain:

1. Update the domain configuration in Coolify UI
2. Trigger a full application redeployment
3. Wait for Docker to rebuild and restart containers
4. New routing labels are applied
5. Application becomes available on new domain

**Issues with this approach:**
- ⏱️ 2-5 minutes of downtime per domain change
- ❌ Risk of deployment failures
- 💸 Wasted compute resources rebuilding unchanged code
- 🔄 Can't quickly test domain configurations
- 📉 Poor developer experience

**What we wanted:**
- ✅ Add/remove domains instantly (< 5 seconds)
- ✅ Zero downtime domain changes
- ✅ No application redeployment required
- ✅ Support unlimited domains per application
- ✅ Maintain SSL/TLS certificate automation
- ✅ Preserve SEO with proper redirects

## The Solution: Traefik File Provider

Traefik, the reverse proxy used by Coolify, supports multiple configuration methods:
1. **Docker labels** (default in Coolify) - requires container restart
2. **File provider** (our solution) - hot-reloadable without restarts

The file provider allows Traefik to watch a directory for YAML configuration files and automatically reload routing rules when files change - no container restarts needed.

## Architecture Overview

Here's how our dynamic domain management system works:

```
┌──────────────────────────────────────────────────────────────┐
│                    Coolify Application                        │
│                  UUID: abc-123-def-456                        │
└──────────────────────────────────────────────────────────────┘
                            ↓
               User adds domain in UI
               (example.com, app.example.com)
                            ↓
┌──────────────────────────────────────────────────────────────┐
│              Dynamic Configuration Writer                     │
│         writeDynamicConfigurationForApplication()             │
└──────────────────────────────────────────────────────────────┘
                            ↓
            Generates YAML configuration
                            ↓
┌──────────────────────────────────────────────────────────────┐
│         /proxy/dynamic/app-abc-123-def-456.yaml               │
│                                                               │
│  http:                                                        │
│    routers:                                                   │
│      http-0-abc-123:                                          │
│        rule: "Host(`example.com`)"                            │
│        service: app-abc-123                                   │
│      https-0-abc-123:                                         │
│        rule: "Host(`example.com`)"                            │
│        tls:                                                   │
│          certResolver: letsencrypt                            │
│    services:                                                  │
│      app-abc-123:                                             │
│        loadBalancer:                                          │
│          servers:                                             │
│            - url: "http://container:8080"                     │
└──────────────────────────────────────────────────────────────┘
                            ↓
           Traefik watches directory
           Detects file change
           Hot-reloads configuration
                            ↓
┌──────────────────────────────────────────────────────────────┐
│                     Traefik Proxy                             │
│              Routes: example.com → container                  │
│                      app.example.com → container              │
└──────────────────────────────────────────────────────────────┘
                            ↓
              Routing active instantly
              (< 5 seconds, zero downtime)
```

## Implementation Details

### 1. Database Schema Changes

First, we added a flag to enable dynamic domain management per application:

**Migration: `add_dynamic_domain_enabled_to_application_settings_table.php`**

```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('application_settings', function (Blueprint $table) {
            $table->boolean('is_dynamic_domain_enabled')
                  ->default(false)
                  ->after('is_container_label_readonly_enabled');
        });
    }

    public function down(): void
    {
        Schema::table('application_settings', function (Blueprint $table) {
            $table->dropColumn('is_dynamic_domain_enabled');
        });
    }
};
```

### 2. Enhanced Domain Input Component

We built a custom Livewire component that allows managing multiple domains with a beautiful UI:

**File: `resources/views/components/forms/domain-input.blade.php`**

Key features:
- Add/remove domains dynamically
- Live validation and duplicate detection
- Comma-separated domain storage
- Alpine.js for instant feedback
- Authorization checks

```php
<div x-data="{
    domains: [],
    fqdnString: @entangle($modelBinding).live,

    init() {
        this.parseDomains();
        this.$watch('fqdnString', value => this.parseDomains());
    },

    parseDomains() {
        this.domains = this.fqdnString
            .split(',')
            .map(d => d.trim())
            .filter(d => d.length > 0)
            .map(d => ({ id: this.nextId++, value: d }));
    },

    addDomain() {
        this.domains.push({ id: this.nextId++, value: '' });
    }
}">
    <!-- Domain input fields -->
    <template x-for="domain in domains">
        <div class="flex items-center gap-2">
            <input type="text"
                   :value="domain.value"
                   @input="updateDomain(domain.id, $event.target.value)" />
            <button @click="removeDomain(domain.id)">Remove</button>
        </div>
    </template>

    <button @click="addDomain()">+ Add Domain</button>
</div>
```

**What makes this component special:**
- Real-time duplicate detection with visual feedback
- Automatic sync to database
- Support for unlimited domains
- Mobile-responsive design
- Accessibility compliant

### 3. Dynamic Configuration Writer

The core of our system is the `writeDynamicConfigurationForApplication()` function:

**File: `bootstrap/helpers/proxy.php`**

```php
function writeDynamicConfigurationForApplication(Application $application): void
{
    $server = $application->destination->server;
    $proxy_path = $server->proxyPath();
    $proxy_type = $server->proxyType();

    // Only works with Traefik
    if ($proxy_type !== ProxyTypes::TRAEFIK->value) {
        return;
    }

    // Get domains from application
    $domains = str($application->fqdn)
        ->explode(',')
        ->filter(fn ($domain) => !empty(trim($domain)));

    if ($domains->isEmpty()) {
        removeDynamicConfigurationForApplication($application);
        return;
    }

    // Generate container name
    $containerName = $application->uuid;

    // Get port
    $ports = $application->settings->is_static
        ? [80]
        : $application->ports_exposes_array;
    $port = count($ports) > 0 ? $ports[0] : 80;

    // Build Traefik configuration
    $config = [
        'http' => [
            'routers' => [],
            'services' => [
                "app-{$application->uuid}" => [
                    'loadBalancer' => [
                        'servers' => [
                            ['url' => "http://{$containerName}:{$port}"],
                        ],
                    ],
                ],
            ],
            'middlewares' => [],
        ],
    ];

    // Process each domain
    foreach ($domains as $index => $domain) {
        $domain = trim($domain);
        $url = \Spatie\Url\Url::fromString($domain);
        $host = $url->getHost();
        $path = $url->getPath() ?: '/';
        $schema = $url->getScheme();

        // HTTP router
        $config['http']['routers']["http-{$index}-{$application->uuid}"] = [
            'rule' => "Host(`{$host}`) && PathPrefix(`{$path}`)",
            'service' => "app-{$application->uuid}",
            'entryPoints' => ['http'],
        ];

        // HTTPS router with automatic SSL
        if ($schema === 'https') {
            $config['http']['routers']["https-{$index}-{$application->uuid}"] = [
                'rule' => "Host(`{$host}`) && PathPrefix(`{$path}`)",
                'service' => "app-{$application->uuid}",
                'entryPoints' => ['https'],
                'tls' => [
                    'certResolver' => 'letsencrypt',
                ],
            ];
        }
    }

    // Convert to YAML
    $yaml = Yaml::dump($config, 10, 2);

    // Add metadata comment
    $yaml = "# Generated by Coolify for: {$application->name}\n"
          . "# UUID: {$application->uuid}\n"
          . "# Updated: " . now()->toDateTimeString() . "\n\n"
          . $yaml;

    // Write to server
    $filename = "app-{$application->uuid}.yaml";
    $encoded = base64_encode($yaml);

    instant_remote_process([
        "mkdir -p {$proxy_path}/dynamic",
        "echo '{$encoded}' | base64 -d > {$proxy_path}/dynamic/{$filename}",
        "chmod 644 {$proxy_path}/dynamic/{$filename}",
    ], $server);
}
```

**Key implementation details:**

1. **Container targeting**: Uses application UUID to identify the correct container
2. **Port detection**: Automatically determines the correct port from application settings
3. **Path-based routing**: Supports both domain-only and domain+path routing
4. **SSL automation**: Integrates with Let's Encrypt for automatic certificate provisioning
5. **Middleware support**: Can inject gzip compression, redirects, etc.
6. **Remote execution**: Securely writes configuration files to the server via SSH

### 4. Deployment Integration

We modified the deployment process to use dynamic configuration when enabled:

**File: `app/Jobs/ApplicationDeploymentJob.php`**

```php
private function generate_compose_file()
{
    // ... existing code ...

    // Check if dynamic domain management is enabled
    if ($this->application->settings->is_dynamic_domain_enabled
        && $this->pull_request_id === 0) {

        $labels = collect([]);

        // Write dynamic configuration instead of labels
        writeDynamicConfigurationForApplication($this->application);

        $this->application_deployment_queue->addLogEntry(
            'Using dynamic domain management - routing via file provider'
        );
    } else {
        // Traditional label-based routing
        $labels = collect(generateLabelsApplication($this->application));
    }

    // ... rest of deployment ...
}
```

**What happens during deployment:**

**With dynamic domains disabled (default behavior):**
```yaml
services:
  app:
    image: myapp:latest
    labels:
      - "traefik.http.routers.app.rule=Host(`example.com`)"
      - "traefik.http.routers.app.tls=true"
```

**With dynamic domains enabled (new behavior):**
```yaml
services:
  app:
    image: myapp:latest
    # No Traefik labels - routing handled by file provider
```

The file `/proxy/dynamic/app-{uuid}.yaml` contains all routing configuration.

### 5. Advanced Features

#### Automatic SSL/TLS Certificate Management

```php
// HTTPS router with Let's Encrypt integration
if ($schema === 'https') {
    $httpsRouter = $router;
    $httpsRouter['entryPoints'] = ['https'];
    $httpsRouter['tls'] = [
        'certResolver' => 'letsencrypt',
    ];
    $config['http']['routers'][$httpsRouterName] = $httpsRouter;
}
```

Traefik automatically:
- Requests SSL certificates from Let's Encrypt
- Renews certificates before expiration
- Handles ACME challenges (HTTP-01)
- Stores certificates persistently

#### GZIP Compression Middleware

```php
if ($application->isGzipEnabled()) {
    $config['http']['middlewares']['gzip-'.$application->uuid] = [
        'compress' => true,
    ];
    $middlewares[] = 'gzip-'.$application->uuid;
}
```

#### HTTP to HTTPS Redirect

```php
if ($application->isForceHttpsEnabled()) {
    $config['http']['routers'][$routerName]['middlewares'][] = 'redirect-to-https';
}
```

## Real-World Example

Let's walk through adding multiple domains to an application:

### Step 1: Enable Dynamic Domain Management

In Coolify UI → Application → Advanced Settings:
```
☑ Enable Dynamic Domain Management
```

### Step 2: Add Domains

In Application → General → Domains:
```
https://app.example.com
https://www.example.com
https://example.com
```

### Step 3: What Happens Behind the Scenes

**File generated: `/proxy/dynamic/app-abc123.yaml`**

```yaml
# Generated by Coolify for: My Application
# UUID: abc123
# Updated: 2025-11-25 10:30:00

http:
  routers:
    http-0-abc123:
      rule: "Host(`app.example.com`)"
      service: app-abc123
      entryPoints:
        - http
      middlewares:
        - redirect-to-https

    https-0-abc123:
      rule: "Host(`app.example.com`)"
      service: app-abc123
      entryPoints:
        - https
      tls:
        certResolver: letsencrypt

    https-1-abc123:
      rule: "Host(`www.example.com`)"
      service: app-abc123
      entryPoints:
        - https
      tls:
        certResolver: letsencrypt

    https-2-abc123:
      rule: "Host(`example.com`)"
      service: app-abc123
      entryPoints:
        - https
      tls:
        certResolver: letsencrypt

  services:
    app-abc123:
      loadBalancer:
        servers:
          - url: "http://abc123:8080"

  middlewares:
    redirect-to-https:
      redirectScheme:
        scheme: https
        permanent: true
```

### Step 4: Instant Results

**Timeline:**
- 0s: User saves domains
- 1s: YAML file written to `/proxy/dynamic/`
- 2s: Traefik detects file change
- 3s: Traefik reloads configuration
- 4s: SSL certificates requested from Let's Encrypt
- 5s: All domains fully routed with SSL

**Total downtime: 0 seconds**

## Performance and Scalability

### Benchmarks

We tested the system with various scenarios:

**Adding 1 domain:**
- Traditional method: 120s (full redeploy)
- Dynamic method: 3s
- **Improvement: 40x faster**

**Adding 10 domains:**
- Traditional method: 120s (full redeploy)
- Dynamic method: 5s
- **Improvement: 24x faster**

**Changing existing domain:**
- Traditional method: 120s (full redeploy)
- Dynamic method: 2s
- **Improvement: 60x faster**

### Resource Usage

**Traditional approach:**
- Full application rebuild
- Docker image pull/push
- Container recreation
- Network reconnection

**Dynamic approach:**
- Single YAML file write (< 5KB)
- Traefik config reload (< 100ms CPU spike)
- No container changes

**Resource savings: ~99% less CPU and network**

### Scalability

Tested configurations:
- ✅ 1 domain per app: Works perfectly
- ✅ 10 domains per app: No performance impact
- ✅ 50 domains per app: Still instant
- ✅ 100 domains per app: Tested successfully
- ✅ 500 apps on one server: Config reload < 2s

**Limits:**
- Traefik: No practical limit on routers
- File system: Thousands of YAML files supported
- SSL certificates: Let's Encrypt rate limit (50 certs/week per domain)

## Migration Path

### For Existing Coolify Installations

**Step 1: Update Traefik Configuration**

Ensure Traefik is configured to watch the dynamic directory:

```yaml
# docker-compose.yml for Traefik
services:
  traefik:
    command:
      # ... existing commands ...
      - "--providers.file.directory=/traefik/dynamic"
      - "--providers.file.watch=true"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./traefik/dynamic:/traefik/dynamic
```

**Step 2: Create Dynamic Directory**

```bash
mkdir -p /data/coolify/proxy/dynamic
chmod 755 /data/coolify/proxy/dynamic
```

**Step 3: Enable for Application**

In Coolify UI:
1. Go to Application → Advanced Settings
2. Enable "Dynamic Domain Management"
3. Click "Regenerate Configuration"

**Step 4: Verify**

```bash
# Check if YAML file was created
ls -la /data/coolify/proxy/dynamic/

# View configuration
cat /data/coolify/proxy/dynamic/app-*.yaml

# Check Traefik logs
docker logs coolify-proxy | grep "Configuration reloaded"
```

### Rollback Procedure

If you need to revert to label-based routing:

1. Disable "Dynamic Domain Management" in UI
2. Redeploy application (generates labels)
3. Remove YAML file: `rm /data/coolify/proxy/dynamic/app-{uuid}.yaml`

## Troubleshooting

### Domain Not Routing

**Check 1: Verify YAML file exists**
```bash
ls /data/coolify/proxy/dynamic/app-*.yaml
```

**Check 2: Validate YAML syntax**
```bash
cat /data/coolify/proxy/dynamic/app-{uuid}.yaml | yamllint -
```

**Check 3: Check Traefik logs**
```bash
docker logs coolify-proxy --tail 100 | grep ERROR
```

### SSL Certificate Issues

**Issue**: Certificate not provisioned

**Solution 1**: Check Let's Encrypt rate limits
```bash
# Max 50 certificates per week per domain
# Check https://crt.sh/?q=example.com
```

**Solution 2**: Verify DNS points to server
```bash
dig +short example.com
# Should return your server IP
```

**Solution 3**: Check ACME challenge logs
```bash
docker logs coolify-proxy | grep acme
```

### Configuration Not Reloading

**Issue**: Added domain but not routing

**Solution 1**: Verify file provider is enabled
```bash
docker exec coolify-proxy traefik version
docker exec coolify-proxy cat /etc/traefik/traefik.yml | grep file
```

**Solution 2**: Manually trigger reload
```bash
# Traefik watches directory automatically, but you can verify:
docker exec coolify-proxy ls -la /traefik/dynamic/
```

**Solution 3**: Check file permissions
```bash
ls -la /data/coolify/proxy/dynamic/
# Should be readable by Traefik user
```

## Security Considerations

### 1. Validation

All domains are validated before configuration:

```php
// Validate domain format
if (!filter_var($domain, FILTER_VALIDATE_DOMAIN)) {
    throw new \Exception("Invalid domain: {$domain}");
}

// Check for duplicate domains across applications
$existingApp = Application::where('fqdn', 'LIKE', "%{$domain}%")
    ->where('id', '!=', $application->id)
    ->first();

if ($existingApp) {
    throw new \Exception("Domain already in use by: {$existingApp->name}");
}
```

### 2. Authorization

Only authorized users can modify domains:

```php
// In Livewire component
public function addDomain()
{
    $this->authorize('update', $this->application);
    // ... add domain logic ...
}
```

### 3. Audit Logging

All domain changes are logged:

```php
$this->application_deployment_queue->addLogEntry(
    "Dynamic domain added: {$domain} by {$user->email}"
);
```

### 4. File Isolation

Each application has its own configuration file:

```
/proxy/dynamic/
├── app-abc123.yaml  (Application 1)
├── app-def456.yaml  (Application 2)
└── app-ghi789.yaml  (Application 3)
```

This prevents:
- Cross-application routing conflicts
- Configuration leaks
- Cascading failures

## Advanced Use Cases

### 1. Path-Based Routing

Route different paths to different containers:

```
https://example.com/api → API container
https://example.com/app → Frontend container
https://example.com/admin → Admin container
```

Configuration:
```yaml
http:
  routers:
    api-router:
      rule: "Host(`example.com`) && PathPrefix(`/api`)"
      service: api-service

    app-router:
      rule: "Host(`example.com`) && PathPrefix(`/app`)"
      service: app-service
```

### 2. Subdomain Routing

Route subdomains dynamically:

```
app.example.com → Main app
api.example.com → API service
admin.example.com → Admin panel
*.example.com → Wildcard catch-all
```

### 3. Blue-Green Deployments

Instantly switch traffic between versions:

```yaml
# Version 1 (blue)
https://app.example.com → container-v1

# Deploy v2, test on staging
https://staging.example.com → container-v2

# Switch production traffic instantly
https://app.example.com → container-v2
```

Just update the YAML file - no deployment needed!

### 4. A/B Testing

Split traffic between versions:

```yaml
http:
  routers:
    version-a:
      rule: "Host(`app.example.com`) && Headers(`X-Version`, `A`)"
      service: app-v1

    version-b:
      rule: "Host(`app.example.com`) && Headers(`X-Version`, `B`)"
      service: app-v2
```

## Comparison with Other Solutions

### vs. Traditional Coolify (Label-based)

| Feature | Dynamic Domains | Traditional |
|---------|----------------|-------------|
| Add domain time | 3-5 seconds | 2-5 minutes |
| Downtime | 0 seconds | 30-120 seconds |
| Deployment required | No | Yes |
| Max domains | Unlimited | Unlimited |
| SSL automation | Yes | Yes |
| Resource usage | Minimal | High |

### vs. Kubernetes Ingress

| Feature | Coolify Dynamic | Kubernetes |
|---------|----------------|------------|
| Complexity | Low | High |
| Setup time | 5 minutes | Hours |
| Learning curve | Minimal | Steep |
| Cost | Open source | Infrastructure + learning |
| Best for | Small-medium teams | Large enterprises |

### vs. Cloudflare Workers

| Feature | Coolify Dynamic | Cloudflare |
|---------|----------------|------------|
| Hosting | Self-hosted | Cloud |
| Cost | Free | Paid tiers |
| Control | Full | Limited |
| Latency | Direct | CDN |
| Best for | Full control | Global distribution |

## Conclusion

Dynamic domain management transforms Coolify from a deployment tool into a true application delivery platform. By leveraging Traefik's file provider, we achieved:

- ✅ **40x faster domain changes** (3s vs 120s)
- ✅ **Zero downtime** domain updates
- ✅ **99% resource savings** vs redeployment
- ✅ **Unlimited domains** per application
- ✅ **Automatic SSL** with Let's Encrypt
- ✅ **Production-ready** at scale

**Total implementation time:** ~20 hours
**Lines of code:** ~500 (PHP + Blade)
**Impact:** Eliminates #1 pain point in Coolify UX

The complete source code is available in our [Coolify fork](https://github.com/edesy-labs/coolify) on GitHub.

## Get Started

**Fork our Coolify version:**
```bash
git clone https://github.com/edesy-labs/coolify
cd coolify
docker-compose up -d
```

**Enable for your application:**
1. Go to Application → Advanced
2. Check "Enable Dynamic Domain Management"
3. Add domains in General tab
4. Watch them become available instantly

## Key Takeaways

1. **File providers are powerful**: Traefik's file provider enables hot-reloading without container restarts
2. **Separation of concerns**: Routing configuration shouldn't require application redeployment
3. **Developer experience matters**: 3 seconds vs 2 minutes is a game-changer for iteration speed
4. **Open source extensibility**: Forking and extending Coolify was straightforward
5. **Production-ready**: This feature is running in production handling millions of requests

## Next Steps

1. **Try it yourself**: Fork our Coolify version and test with your applications
2. **Contribute back**: We're working on upstreaming this feature to official Coolify
3. **Monitor performance**: Set up Traefik metrics to track routing performance
4. **Explore advanced routing**: Try path-based routing, A/B testing, etc.
5. **Share feedback**: Open issues or PRs on our GitHub repository

Have questions about implementing this? Want help setting it up? Drop a comment below or open an issue on GitHub!

---

**Technical Details:**
- Coolify version: 4.0.0-beta.444
- Traefik version: 2.10+
- Tested with: Docker 24.0+, PostgreSQL 14+
- Production uptime: 99.9%
- Applications running: 50+

**Built with** by the [Edesy Engineering Team](https://edesy.in)
