fix(domains): sync nginx targets per server block across deployments

This commit is contained in:
KodekaTeam committed 2026-08-22 14:53:36 +07:00
1 parent e097467c95
commit 5c4fc32d80
2 files changed
+159 -10

No files matched your search

+83 -10
View File
@@ -446,6 +446,80 @@ function extractNginxServerNameDomains(content: string): string[] {
return Array.from(domains);
}
export interface NginxDomainTarget {
name: string;
host: string | null;
port: number | null;
}
function extractNginxServerBlocks(content: string): string[] {
const sanitized = content.replace(/^\s*#.*$/gm, "");
const blocks: string[] = [];
const serverPattern = /\bserver\s*\{/gi;
for (const match of sanitized.matchAll(serverPattern)) {
const matchIndex = match.index ?? -1;
const openingBrace = sanitized.indexOf("{", matchIndex);
if (openingBrace < 0) continue;
let depth = 0;
let quote: '"' | "'" | null = null;
let escaped = false;
for (let index = openingBrace; index < sanitized.length; index += 1) {
const character = sanitized[index];
if (escaped) {
escaped = false;
continue;
}
if (character === "\\") {
escaped = true;
continue;
}
if (quote) {
if (character === quote) quote = null;
continue;
}
if (character === '"' || character === "'") {
quote = character;
continue;
}
if (character === "{") {
depth += 1;
} else if (character === "}") {
depth -= 1;
if (depth === 0) {
blocks.push(sanitized.slice(matchIndex, index + 1));
break;
}
}
}
}
return blocks;
}
export function extractNginxDomainTargets(
content: string,
): NginxDomainTarget[] {
const serverBlocks = extractNginxServerBlocks(content);
const scopes = serverBlocks.length > 0 ? serverBlocks : [content];
const targets = new Map<string, NginxDomainTarget>();
for (const scope of scopes) {
const proxyPassTarget = extractProxyPassTarget(scope);
for (const name of extractNginxServerNameDomains(scope)) {
const existing = targets.get(name);
if (!existing || (!existing.host && proxyPassTarget.host)) {
targets.set(name, { name, ...proxyPassTarget });
}
}
}
return Array.from(targets.values());
}
async function listNginxDomains(server: Server): Promise<DiscoveredDomain[]> {
const [configDump, fileDump] = await Promise.all([
exec(
@@ -501,12 +575,11 @@ async function listNginxDomains(server: Server): Promise<DiscoveredDomain[]> {
continue;
}
const proxyPassTarget = extractProxyPassTarget(content);
const managedMetadata = extractManagedNginxMetadata(content);
for (const domainName of extractNginxServerNameDomains(content)) {
for (const target of extractNginxDomainTargets(content)) {
addDiscoveredDomain(
domains,
domainName,
target.name,
"NGINX",
"NGINX",
server.ip,
@@ -517,14 +590,14 @@ async function listNginxDomains(server: Server): Promise<DiscoveredDomain[]> {
managedConfigMode: managedMetadata.managedConfigMode,
managedPrimaryDomain: managedMetadata.managedPrimaryDomain,
containerName:
deriveManagedNginxContainerName(path, domainName) ??
(proxyPassTarget.host &&
!isLocalhostHost(proxyPassTarget.host) &&
!isLikelyIpv4Host(proxyPassTarget.host) &&
!isLikelyIpv6Host(proxyPassTarget.host)
? proxyPassTarget.host
deriveManagedNginxContainerName(path, target.name) ??
(target.host &&
!isLocalhostHost(target.host) &&
!isLikelyIpv4Host(target.host) &&
!isLikelyIpv6Host(target.host)
? target.host
: null),
targetPort: proxyPassTarget.port,
targetPort: target.port,
},
);
}
+76
View File
@@ -0,0 +1,76 @@
import assert from "node:assert/strict";
import test from "node:test";
import { extractNginxDomainTargets } from "../../src/server/services/ssh-services/domains";
test("nginx sync resolves each shared domain from its own server block", () => {
const config = `
# doktainer-managed: true
# doktainer-config-mode: SHARED
# doktainer-primary-domain: example.com
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://frontend:3000;
}
}
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://backend:8080;
}
}
`;
assert.deepEqual(extractNginxDomainTargets(config), [
{ name: "example.com", host: "frontend", port: 3000 },
{ name: "api.example.com", host: "backend", port: 8080 },
]);
});
test("nginx sync prefers the proxied HTTPS block over an HTTP redirect block", () => {
const config = `
server {
listen 80;
server_name example.com api.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name example.com;
location / { proxy_pass http://frontend:3000; }
}
server {
listen 443 ssl;
server_name api.example.com;
location / { proxy_pass http://backend:8080; }
}
`;
assert.deepEqual(extractNginxDomainTargets(config), [
{ name: "example.com", host: "frontend", port: 3000 },
{ name: "api.example.com", host: "backend", port: 8080 },
]);
});
test("nginx sync keeps multiple names in one server block on the same target", () => {
const config = `
server {
listen 80;
server_name example.com www.example.com;
location / { proxy_pass http://frontend:3000; }
}
`;
assert.deepEqual(extractNginxDomainTargets(config), [
{ name: "example.com", host: "frontend", port: 3000 },
{ name: "www.example.com", host: "frontend", port: 3000 },
]);
});