Serve preview URLs over Traefik+TLS, stabilize them, and speed up builds.

Ingress / preview URLs:
- Default the app Ingress class and ACME HTTP-01 solver to Traefik
  (k3s default) via a new INGRESS_CLASS env, instead of hardcoding nginx —
  fixes 404s on clusters without ingress-nginx.
- Only put public, real-TLD hosts (custom domain + preview) in the TLS
  block; the internal *.apps.cloudhost.local host no longer poisons the
  Let's Encrypt order, so certs actually issue.
- Make the per-app preview number stable across redeploys so URLs stop
  breaking, and let PREVIEW_BASE_DOMAIN configure the base domain.

Registry pulls:
- Point the k3s registries.yaml mirror endpoint at the registry NodePort on
  loopback so node containerd never depends on cluster DNS (image pulls
  survive node restarts).

Builds:
- Pin the Kaniko image, use IfNotPresent pull policy, drop the dead build
  queue/processor, and retry transient Kubernetes API errors while polling
  build jobs.

Logs & apps list:
- fluent-bit reads log files from head so startup output reaches
  Elasticsearch.
- Order joined deployments newest-first so the apps list shows the latest
  deployment status.

Allocation:
- Reserve in-flight (pending/building) capacity and stop globally degrading
  the cluster on a single allocation failure, so concurrent deploys don't
  starve or wrongly report "no healthy cluster".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
keyhan
2026-06-02 15:44:08 +03:30
parent 786689e0fd
commit 4301277b48
13 changed files with 244 additions and 109 deletions
+24 -8
View File
@@ -30,13 +30,29 @@ export class DeploymentsService {
) {}
/**
* Stable "number" part for preview host, derived from deployment id.
* We keep it numeric to match the "<number>" requirement.
* Random 7-digit suffix for the preview host: <userId>-<7-digit>.<baseDomain>.
* Generated once per application (see resolvePreviewNumber) and persisted.
*/
private computePreviewNumberFromDeploymentId(deploymentId: string): string {
const hashHex = crypto.createHash('sha256').update(deploymentId).digest('hex');
const num = parseInt(hashHex.slice(0, 8), 16) % 1_000_000; // 0..999999
return String(num).padStart(6, '0');
private generatePreviewNumber(): string {
return String(crypto.randomInt(1_000_000, 10_000_000));
}
/**
* Returns a STABLE preview number for an application: reuse the one already
* assigned to a previous deployment so the public preview URL never changes
* across redeploys (otherwise old links 404). Only generates a new number the
* first time the app is deployed. Apps with a custom domain get no preview host.
*/
private async resolvePreviewNumber(applicationId: string): Promise<string> {
const existing = await this.deploymentsRepository
.createQueryBuilder('d')
.select('d.previewSubdomain', 'previewSubdomain')
.where('d.applicationId = :applicationId', { applicationId })
.andWhere('d.previewSubdomain IS NOT NULL')
.orderBy('d.createdAt', 'DESC')
.limit(1)
.getRawOne<{ previewSubdomain: string }>();
return existing?.previewSubdomain || this.generatePreviewNumber();
}
async triggerDeployment(applicationId: string, userId: string): Promise<Deployment> {
@@ -56,7 +72,7 @@ export class DeploymentsService {
// Fill deterministic preview number after we have the deployment id.
let previewSubdomain: string | null = null;
if (!app.customDomain) {
previewSubdomain = this.computePreviewNumberFromDeploymentId(saved.id);
previewSubdomain = await this.resolvePreviewNumber(app.id);
await this.deploymentsRepository.update(saved.id, { previewSubdomain });
saved.previewSubdomain = previewSubdomain;
}
@@ -657,7 +673,7 @@ export class DeploymentsService {
let previewSubdomain: string | null = null;
if (!app.customDomain) {
previewSubdomain = this.computePreviewNumberFromDeploymentId(saved.id);
previewSubdomain = await this.resolvePreviewNumber(app.id);
await this.deploymentsRepository.update(saved.id, { previewSubdomain });
saved.previewSubdomain = previewSubdomain;
}