Skip to main content

Prerequisites

Before setting up custom domains, ensure you have:
  1. A Laravel Forge account with an active subscription
  2. A DigitalOcean account with API access
  3. A server configured in Laravel Forge
  4. Queue workers running on your server
  5. Laravel Scheduler configured and running

Step 1: Configure Laravel Forge

1.1 Create a Forge API Token

  1. Log in to Laravel Forge
  2. Navigate to AccountAPICreate New Token
  3. Give your token a name (e.g., “Custom Domains API”)
  4. Copy the generated token (you won’t be able to see it again)

1.2 Get Your Server ID

  1. In Laravel Forge, navigate to your server
  2. Look at the URL in your browser: https://forge.laravel.com/servers/{SERVER_ID}
  3. Copy the SERVER_ID from the URL

1.3 Get Your Nginx Template ID

  1. In your Forge server, go to Nginx Templates
  2. Create a new template or use an existing one suitable for your application
  3. The template ID will be in the URL or template details
  4. Common default template ID is 14570 (you can use this initially)

Step 2: Configure DigitalOcean

2.1 Create a DigitalOcean API Token

  1. Log in to DigitalOcean
  2. Navigate to APITokens/KeysGenerate New Token
  3. Give your token a name (e.g., “DNS Validation”)
  4. Set the scopes to Read and Write
  5. Copy the generated token
The DigitalOcean token is used for DNS validation during SSL certificate provisioning. Keep it secure.

Step 3: Configure Environment Variables

Add the following variables to your .env file:
.env
# Laravel Forge Configuration
FORGE_API_KEY=your_forge_api_token_here
FORGE_SERVER_ID=your_forge_server_id
FORGE_NGINX_TEMPLATE=14570
FORGE_DIGITALOCEAN_TOKEN=your_digitalocean_api_token
The FORGE_NGINX_TEMPLATE can be left as 14570 (default PHP template) or changed to your custom template ID.

Step 4: Run Database Migration

The custom domains feature requires a new domains table. Run the migration:
php artisan migrate
This creates the domains table with the following structure:
Schema::create('domains', function (Blueprint $table) {
    $table->id();
    $table->foreignId('team_id')->constrained('teams')->cascadeOnDelete();
    $table->string('domain');
    $table->boolean('dns_verified')->default(false);
    $table->string('forge_site_id')->nullable();
    $table->string('forge_certificate_id')->nullable();
    $table->boolean('ssl_installed')->default(false);
    $table->boolean('ssl_active')->default(false);
    $table->datetime('ssl_installed_at')->nullable();
    $table->integer('dns_verification_attempts')->default(0);
    $table->jsonb('domain_installation_history')->nullable();
    $table->timestamps();
});

Step 5: Configure Queue Workers

Custom domains rely heavily on background jobs. Ensure your queue worker is running:

For Development

php artisan queue:work --tries=3

For Production (with Supervisor)

Create a Supervisor configuration file at /etc/supervisor/conf.d/laravel-worker.conf:
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /path/to/your/application/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=forge
numprocs=2
redirect_stderr=true
stdout_logfile=/path/to/your/application/storage/logs/worker.log
stopwaitsecs=3600
Then reload Supervisor:
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*
Laravel Forge can automatically configure Supervisor for you. Go to your site’s Queue section and enable the queue worker.

Step 6: Configure Laravel Scheduler

The scheduler runs the app:check-custom-domain-setup command periodically to monitor and complete pending domain setups.

For Development

Run the scheduler manually:
php artisan schedule:work

For Production

Add this cron entry (Forge does this automatically):
* * * * * cd /path/to/your/application && php artisan schedule:run >> /dev/null 2>&1

Schedule Configuration

Add to your routes/console.php or app/Console/Kernel.php:
use Illuminate\Support\Facades\Schedule;

Schedule::command('app:check-custom-domain-setup')
    ->everyFiveMinutes()
    ->withoutOverlapping()
    ->runInBackground();

Step 7: Test the Configuration

Create a test to verify your Forge connection:
php artisan tinker
$forge = app(\App\Services\ForgeService::class);
$sites = $forge->sites();
dd($sites);
If you see a list of sites, your configuration is working correctly.

Step 8: Configure Your Main Server

8.1 Wildcard SSL Certificate (Optional)

If you want to support subdomains of your main domain (e.g., *.yourdomain.com), configure a wildcard certificate:
  1. In Laravel Forge, go to your main site
  2. Navigate to SSLLetsEncrypt
  3. Request a wildcard certificate

8.2 Nginx Configuration

Ensure your main site’s Nginx configuration can handle custom domains. The default configuration should work, but you may need to adjust based on your needs.

Step 9: Enable Custom Domains in Team Settings

9.1 Add Domain Field to Team Resource

In your Filament Team resource (app/Filament/Resources/TeamResource.php), add a domain field:
use Filament\Forms;

public static function form(Form $form): Form
{
    return $form
        ->schema([
            Forms\Components\TextInput::make('name')
                ->required()
                ->maxLength(255),

            Forms\Components\Textarea::make('description')
                ->rows(3),

            Forms\Components\TextInput::make('domain')
                ->label('Custom Domain')
                ->placeholder('app.yourdomain.com')
                ->helperText('Enter your custom domain (e.g., portal.company.com)')
                ->maxLength(255)
                ->rule('regex:/^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,}$/i'),

            Forms\Components\Placeholder::make('domain_status')
                ->label('Domain Status')
                ->content(fn ($record) => match(true) {
                    !$record || !$record->domain => 'No domain configured',
                    $record->domain_verified => '✅ Verified and Active',
                    default => '⏳ Pending verification'
                }),
        ]);
}

9.2 Add to Table Columns (Optional)

use Filament\Tables;

public static function table(Table $table): Table
{
    return $table
        ->columns([
            Tables\Columns\TextColumn::make('name'),
            Tables\Columns\TextColumn::make('domain')
                ->label('Custom Domain'),
            Tables\Columns\IconColumn::make('domain_verified')
                ->label('Verified')
                ->boolean(),
        ]);
}

Step 10: Create Team Observer

The team observer triggers domain verification when a domain is added or updated. It should already be in place at app/Observers/TeamObserver.php:
<?php

namespace App\Observers;

use App\Jobs\VerifyDomainDnsRecordsJob;
use App\Models\Team;

class TeamObserver
{
    public function updated(Team $team): void
    {
        if ($team->isDirty('domain') && $team->domain) {
            VerifyDomainDnsRecordsJob::dispatch($team, $team->domain);
        }
    }
}
Ensure the observer is registered on your Team model:
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use App\Observers\TeamObserver;

#[ObservedBy([TeamObserver::class])]
class Team extends Model
{
    // ...
}

Verification

After completing the setup:
  1. Log in to your Filament admin panel
  2. Navigate to Team Settings (or Teams management)
  3. Add a custom domain to a test team
  4. Monitor the logs: tail -f storage/logs/laravel.log
  5. Watch the queue worker process the jobs
  6. Check the domain record in the database:
php artisan tinker
\App\Models\Domain::latest()->first();

Next Steps

Common Issues

Queue Jobs Not Running

Problem: Domain verification is stuck in pending. Solution: Ensure your queue worker is running:
php artisan queue:work

Forge API Errors

Problem: Jobs fail with Forge API errors. Solution: Verify your FORGE_API_KEY and FORGE_SERVER_ID are correct.

DNS Verification Fails

Problem: DNS verification keeps failing. Solution: Ensure DNS records are properly configured (see DNS Configuration guide).

SSL Certificate Issues

Problem: SSL certificates fail to provision. Solution: Verify your FORGE_DIGITALOCEAN_TOKEN is valid and has proper permissions.