feat(proxy): make the Traefik certificate resolver configurable per server

Coolify always added tls.certresolver=letsencrypt to generated HTTPS
routers, so Traefik logged errors when the proxy used its own
certificates (for example Cloudflare Origin CA) without that resolver.
Add a server setting (default letsencrypt) in the proxy page and API.
An empty value omits the resolver from resource labels, the Coolify
dashboard routes, and the catch-all router.

Closes #12138
This commit is contained in:
Andras Bacsai
2026-10-11 10:19:40 +02:00
parent d4502bc514
commit af589f6da4
13 changed files with 339 additions and 28 deletions
@@ -7,6 +7,7 @@ use App\Enums\ProxyTypes;
use App\Http\Controllers\Controller;
use App\Jobs\RestartProxyJob;
use App\Models\Server;
use App\Models\ServerSetting;
use App\Rules\SafeExternalUrl;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -41,6 +42,7 @@ class ServerProxyController extends Controller
* redirect_enabled: bool,
* redirect_url: string|null,
* generate_exact_labels: bool,
* traefik_certificate_resolver: string|null,
* configuration?: string|null
* }
*/
@@ -52,6 +54,7 @@ class ServerProxyController extends Controller
'redirect_enabled' => (bool) data_get($server->proxy, 'redirect_enabled', true),
'redirect_url' => data_get($server->proxy, 'redirect_url'),
'generate_exact_labels' => (bool) ($server->settings->generate_exact_labels ?? false),
'traefik_certificate_resolver' => $server->traefikCertificateResolver(),
];
// Proxy compose can contain secrets; only expose with read:sensitive (and admin) like other APIs.
@@ -86,6 +89,7 @@ class ServerProxyController extends Controller
new OA\Property(property: 'redirect_enabled', type: 'boolean', example: true),
new OA\Property(property: 'redirect_url', type: 'string', nullable: true, example: 'https://example.com'),
new OA\Property(property: 'generate_exact_labels', type: 'boolean', example: false),
new OA\Property(property: 'traefik_certificate_resolver', type: 'string', nullable: true, example: 'letsencrypt', description: 'Traefik certificate resolver for generated HTTPS routers. Null when the proxy uses its own certificates.'),
new OA\Property(property: 'configuration', type: 'string', nullable: true, description: 'Docker Compose proxy configuration when stored in the database. Only present with read:sensitive.'),
]
)
@@ -114,7 +118,7 @@ class ServerProxyController extends Controller
#[OA\Patch(
summary: 'Update server proxy',
description: 'Update proxy redirect settings, exact labels generation, and optionally the proxy type for a team-owned server.',
description: 'Update proxy redirect settings, exact labels generation, the Traefik certificate resolver, and optionally the proxy type for a team-owned server.',
path: '/servers/{uuid}/proxy',
operationId: 'update-server-proxy',
security: [['bearerAuth' => []]],
@@ -130,6 +134,7 @@ class ServerProxyController extends Controller
new OA\Property(property: 'redirect_enabled', type: 'boolean'),
new OA\Property(property: 'redirect_url', type: 'string', nullable: true, description: 'Public http(s) redirect URL, or null to clear.'),
new OA\Property(property: 'generate_exact_labels', type: 'boolean'),
new OA\Property(property: 'traefik_certificate_resolver', type: 'string', nullable: true, description: 'Traefik certificate resolver for generated HTTPS routers (letters, digits, _ and -). Null or empty to use your own certificates. Redeploy resources to apply.'),
new OA\Property(property: 'proxy_type', type: 'string', enum: ['traefik', 'caddy', 'nginx', 'none'], description: 'Proxy type (case-insensitive).'),
]
)
@@ -146,6 +151,7 @@ class ServerProxyController extends Controller
new OA\Property(property: 'redirect_enabled', type: 'boolean'),
new OA\Property(property: 'redirect_url', type: 'string', nullable: true),
new OA\Property(property: 'generate_exact_labels', type: 'boolean'),
new OA\Property(property: 'traefik_certificate_resolver', type: 'string', nullable: true),
new OA\Property(property: 'configuration', type: 'string', nullable: true),
]
)
@@ -168,11 +174,12 @@ class ServerProxyController extends Controller
return $return;
}
$allowedFields = ['redirect_enabled', 'redirect_url', 'generate_exact_labels', 'proxy_type'];
$allowedFields = ['redirect_enabled', 'redirect_url', 'generate_exact_labels', 'traefik_certificate_resolver', 'proxy_type'];
$validator = customApiValidator($request->all(), [
'redirect_enabled' => 'boolean',
'redirect_url' => ['nullable', 'string', new SafeExternalUrl],
'generate_exact_labels' => 'boolean',
'traefik_certificate_resolver' => ['nullable', 'string', 'max:255', 'regex:'.ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_PATTERN],
'proxy_type' => 'string|nullable',
]);
@@ -228,16 +235,29 @@ class ServerProxyController extends Controller
$server->settings->save();
}
$certificateResolverChanged = false;
if ($request->exists('traefik_certificate_resolver')) {
$server->settings->traefik_certificate_resolver = $request->input('traefik_certificate_resolver') ?: null;
$certificateResolverChanged = $server->settings->isDirty('traefik_certificate_resolver');
$server->settings->save();
}
if ($request->has('proxy_type') && filled($request->proxy_type)) {
$server->changeProxy($request->proxy_type, async: true);
$server->refresh();
}
// Apply redirect file on the server only when reachable (DB settings always saved above).
if ($redirectChanged && $server->isFunctional()) {
// The catch-all router in that file uses the certificate resolver too.
if (($redirectChanged || $certificateResolverChanged) && $server->isFunctional()) {
$server->setupDefaultRedirect();
}
// Coolify's own dashboard routes use the resolver too.
if ($certificateResolverChanged && $server->isFunctional()) {
$server->setupDynamicProxyConfiguration();
}
auditLog('api.server.proxy.updated', [
'team_id' => $teamId,
'server_uuid' => $server->uuid,