Add subdirectory routing mode for public domain deployments
Apps can now be served at domain.com/appname/ instead of requiring subdomain DNS records (appname.domain.com). Supports three subpath modes per template: native (URL base env var), strip (handle_path), and none (incompatible warning). Tested on Linux with deploy/removal lifecycle verified. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -109,6 +109,18 @@ module.exports = function(ctx) {
|
||||
processed.docker.environment.PLEX_CLAIM = config.plexClaimToken;
|
||||
}
|
||||
|
||||
// Inject URL base env var for subdirectory routing mode
|
||||
if (ctx.siteConfig.routingMode === 'subdirectory' && template.urlBaseEnv) {
|
||||
processed.docker.environment = processed.docker.environment || {};
|
||||
const basePath = `/${config.subdomain}`;
|
||||
// Some apps need the full URL, not just the path
|
||||
if (['GF_SERVER_ROOT_URL', 'GITEA__server__ROOT_URL'].includes(template.urlBaseEnv)) {
|
||||
processed.docker.environment[template.urlBaseEnv] = ctx.buildServiceUrl(config.subdomain) + '/';
|
||||
} else {
|
||||
processed.docker.environment[template.urlBaseEnv] = basePath;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply custom volume overrides
|
||||
if (config.customVolumes?.length && processed.docker?.volumes) {
|
||||
processed.docker.volumes = processed.docker.volumes.map(vol => {
|
||||
@@ -266,6 +278,105 @@ module.exports = function(ctx) {
|
||||
await ctx.caddy.verifySite(domain);
|
||||
}
|
||||
|
||||
// Reserved paths that cannot be used as subpath names in subdirectory mode
|
||||
const RESERVED_SUBPATHS = ['api', 'probe', 'assets', 'health', 'dist', 'js', 'css', 'fonts', 'favicon.ico'];
|
||||
|
||||
/** Ensure the main domain block with route markers exists in the Caddyfile.
|
||||
* Called before the first subdirectory app is deployed. */
|
||||
async function ensureMainDomainBlock() {
|
||||
if (ctx.siteConfig.routingMode !== 'subdirectory' || !ctx.siteConfig.domain) return;
|
||||
|
||||
const content = await ctx.caddy.read();
|
||||
const domain = ctx.siteConfig.domain;
|
||||
const ROUTE_MARKER = '# === DashCaddy App Routes ===';
|
||||
|
||||
// Already has markers — nothing to do
|
||||
if (content.includes(ROUTE_MARKER)) return;
|
||||
|
||||
// Domain block exists but lacks markers — inject them
|
||||
if (content.includes(`${domain} {`)) {
|
||||
const result = await ctx.caddy.modify(c => {
|
||||
// Insert markers before the final catch-all handle block inside the domain block
|
||||
const domainStart = c.indexOf(`${domain} {`);
|
||||
// Find standalone "handle {" (catch-all SPA fallback) — match tabs or spaces
|
||||
const searchFrom = domainStart + domain.length;
|
||||
const handleMatch = c.slice(searchFrom).match(/^([ \t]+)handle\s*\{/m);
|
||||
if (!handleMatch) return null;
|
||||
const handleIdx = searchFrom + handleMatch.index;
|
||||
const indent = handleMatch[1];
|
||||
const markerBlock = `${indent}${ROUTE_MARKER}\n${indent}# === End App Routes ===\n\n`;
|
||||
return c.slice(0, handleIdx) + markerBlock + c.slice(handleIdx);
|
||||
});
|
||||
if (result.success) {
|
||||
ctx.log.info('caddy', 'Injected route markers into existing domain block', { domain });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// No domain block at all — create one with dashboard + markers
|
||||
const dashboardRoot = platformPaths.sitePath('status');
|
||||
const apiPort = process.env.PORT || 3001;
|
||||
const block = `\n${domain} {\n root * ${dashboardRoot}\n encode gzip\n\n handle /api/* {\n reverse_proxy localhost:${apiPort}\n }\n\n handle /probe/* {\n reverse_proxy localhost:${apiPort}\n }\n\n ${ROUTE_MARKER}\n # === End App Routes ===\n\n handle {\n @notFile not file {path}\n rewrite @notFile /index.html\n file_server\n }\n}\n`;
|
||||
|
||||
const result = await ctx.caddy.modify(c => c + block);
|
||||
if (result.success) {
|
||||
ctx.log.info('caddy', 'Created main domain block with route markers', { domain });
|
||||
} else {
|
||||
throw new Error(`[DC-303] Failed to create main domain block for ${domain}: ${result.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Inject a subpath config block between route markers in the Caddyfile. */
|
||||
async function addSubpathConfig(subdomain, configBlock) {
|
||||
const marker = `# --- DashCaddy: ${subdomain} ---`;
|
||||
const endMarker = `# --- End: ${subdomain} ---`;
|
||||
const END_ROUTE_MARKER = '# === End App Routes ===';
|
||||
|
||||
const result = await ctx.caddy.modify(content => {
|
||||
if (content.includes(marker)) {
|
||||
ctx.log.info('caddy', 'Subpath config already exists, skipping', { subdomain });
|
||||
return null;
|
||||
}
|
||||
|
||||
const endIdx = content.indexOf(END_ROUTE_MARKER);
|
||||
if (endIdx === -1) {
|
||||
throw new Error(`Route marker "${END_ROUTE_MARKER}" not found in Caddyfile`);
|
||||
}
|
||||
|
||||
// Detect indentation from the end marker line
|
||||
const lineStart = content.lastIndexOf('\n', endIdx) + 1;
|
||||
const indent = content.slice(lineStart, endIdx).match(/^([ \t]*)/)?.[1] || '\t';
|
||||
|
||||
const injection = `${indent}${marker}\n${configBlock}\n${indent}${endMarker}\n`;
|
||||
return content.slice(0, endIdx) + injection + content.slice(endIdx);
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(`[DC-303] Failed to add subpath config for ${subdomain}: ${result.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Remove a subpath config block from between its markers in the Caddyfile. */
|
||||
async function removeSubpathConfig(subdomain) {
|
||||
const marker = `# --- DashCaddy: ${subdomain} ---`;
|
||||
const endMarker = `# --- End: ${subdomain} ---`;
|
||||
|
||||
return await ctx.caddy.modify(content => {
|
||||
const startIdx = content.indexOf(marker);
|
||||
if (startIdx === -1) return null;
|
||||
|
||||
const endIdx = content.indexOf(endMarker);
|
||||
if (endIdx === -1) return null;
|
||||
|
||||
// Remove from the line start of the marker to the line end of the end marker
|
||||
const lineStart = content.lastIndexOf('\n', startIdx);
|
||||
const lineEnd = content.indexOf('\n', endIdx + endMarker.length);
|
||||
|
||||
const modified = content.slice(0, lineStart) + content.slice(lineEnd);
|
||||
return modified.replace(/\n{3,}/g, '\n\n');
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
checkPortConflicts,
|
||||
findExistingContainerByImage,
|
||||
@@ -273,6 +384,10 @@ module.exports = function(ctx) {
|
||||
processTemplateVariables,
|
||||
waitForHealthCheck,
|
||||
addCaddyConfig,
|
||||
addSubpathConfig,
|
||||
removeSubpathConfig,
|
||||
ensureMainDomainBlock,
|
||||
RESERVED_SUBPATHS,
|
||||
generateStaticSiteConfig
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user