feat: Refactor CaddyManager and OneboxReverseProxy to use Docker service for Caddy management

This commit is contained in:
2025-11-26 13:23:56 +00:00
parent c639453e01
commit c03e0e055c
2 changed files with 228 additions and 254 deletions

View File

@@ -1,25 +1,27 @@
/** /**
* Caddy Manager for Onebox * Caddy Manager for Onebox
* *
* Manages Caddy binary download, process lifecycle, and Admin API configuration. * Manages Caddy as a Docker Swarm service instead of a host binary.
* Caddy is used as the reverse proxy with native SNI support. * This allows Caddy to access services on the Docker overlay network.
*/ */
import * as plugins from '../plugins.ts';
import { logger } from '../logging.ts'; import { logger } from '../logging.ts';
import { getErrorMessage } from '../utils/error.ts'; import { getErrorMessage } from '../utils/error.ts';
const CADDY_VERSION = '2.10.2'; const CADDY_SERVICE_NAME = 'onebox-caddy';
const CADDY_DOWNLOAD_URL = `https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/caddy_${CADDY_VERSION}_linux_amd64.tar.gz`; const CADDY_IMAGE = 'caddy:2-alpine';
const DOCKER_GATEWAY_IP = '172.17.0.1'; // Docker bridge gateway for container-to-host communication
export interface ICaddyRoute { export interface ICaddyRoute {
domain: string; domain: string;
upstream: string; // e.g., "10.0.1.40:80" upstream: string; // e.g., "onebox-hello-world:80"
} }
export interface ICaddyCertificate { export interface ICaddyCertificate {
domain: string; domain: string;
certPath: string; certPem: string;
keyPath: string; keyPem: string;
} }
interface ICaddyLoggingConfig { interface ICaddyLoggingConfig {
@@ -64,7 +66,7 @@ interface ICaddyConfig {
policies: Array<{ issuers: never[] }>; policies: Array<{ issuers: never[] }>;
}; };
certificates?: { certificates?: {
load_files?: Array<{ load_pem?: Array<{
certificate: string; certificate: string;
key: string; key: string;
tags?: string[]; tags?: string[];
@@ -85,8 +87,7 @@ interface ICaddyRouteConfig {
} }
export class CaddyManager { export class CaddyManager {
private process: Deno.ChildProcess | null = null; private dockerClient: InstanceType<typeof plugins.docker.Docker> | null = null;
private binaryPath: string;
private certsDir: string; private certsDir: string;
private adminUrl: string; private adminUrl: string;
private httpPort: number; private httpPort: number;
@@ -95,9 +96,10 @@ export class CaddyManager {
private loggingEnabled: boolean; private loggingEnabled: boolean;
private routes: Map<string, ICaddyRoute> = new Map(); private routes: Map<string, ICaddyRoute> = new Map();
private certificates: Map<string, ICaddyCertificate> = new Map(); private certificates: Map<string, ICaddyCertificate> = new Map();
private networkName = 'onebox-network';
private serviceRunning = false;
constructor(options?: { constructor(options?: {
binaryPath?: string;
certsDir?: string; certsDir?: string;
adminPort?: number; adminPort?: number;
httpPort?: number; httpPort?: number;
@@ -105,7 +107,6 @@ export class CaddyManager {
logReceiverPort?: number; logReceiverPort?: number;
loggingEnabled?: boolean; loggingEnabled?: boolean;
}) { }) {
this.binaryPath = options?.binaryPath || './.nogit/caddy';
this.certsDir = options?.certsDir || './.nogit/certs'; this.certsDir = options?.certsDir || './.nogit/certs';
this.adminUrl = `http://localhost:${options?.adminPort || 2019}`; this.adminUrl = `http://localhost:${options?.adminPort || 2019}`;
this.httpPort = options?.httpPort || 8080; this.httpPort = options?.httpPort || 8080;
@@ -114,6 +115,18 @@ export class CaddyManager {
this.loggingEnabled = options?.loggingEnabled ?? true; this.loggingEnabled = options?.loggingEnabled ?? true;
} }
/**
* Initialize Docker client for Caddy service management
*/
private async ensureDockerClient(): Promise<void> {
if (!this.dockerClient) {
this.dockerClient = new plugins.docker.Docker({
socketPath: 'unix:///var/run/docker.sock',
});
await this.dockerClient.start();
}
}
/** /**
* Update listening ports (must call reloadConfig after if running) * Update listening ports (must call reloadConfig after if running)
*/ */
@@ -123,103 +136,98 @@ export class CaddyManager {
} }
/** /**
* Ensure Caddy binary is downloaded and executable * Start Caddy as a Docker Swarm service
*/
async ensureBinary(): Promise<void> {
try {
// Check if binary exists
try {
const stat = await Deno.stat(this.binaryPath);
if (stat.isFile) {
// Verify it's executable by checking version
const cmd = new Deno.Command(this.binaryPath, {
args: ['version'],
stdout: 'piped',
stderr: 'piped',
});
const result = await cmd.output();
if (result.success) {
const version = new TextDecoder().decode(result.stdout).trim();
logger.info(`Caddy binary found: ${version}`);
return;
}
}
} catch {
// Binary doesn't exist, need to download
}
logger.info(`Downloading Caddy v${CADDY_VERSION}...`);
// Create directory if needed
const dir = this.binaryPath.substring(0, this.binaryPath.lastIndexOf('/'));
await Deno.mkdir(dir, { recursive: true });
// Download tar.gz
const response = await fetch(CADDY_DOWNLOAD_URL);
if (!response.ok) {
throw new Error(`Failed to download Caddy: ${response.status} ${response.statusText}`);
}
const tarGzPath = `${this.binaryPath}.tar.gz`;
const data = new Uint8Array(await response.arrayBuffer());
await Deno.writeFile(tarGzPath, data);
// Extract using tar command
const extractCmd = new Deno.Command('tar', {
args: ['-xzf', tarGzPath, '-C', dir, 'caddy'],
stdout: 'piped',
stderr: 'piped',
});
const extractResult = await extractCmd.output();
if (!extractResult.success) {
throw new Error(`Failed to extract Caddy: ${new TextDecoder().decode(extractResult.stderr)}`);
}
// Clean up tar.gz
await Deno.remove(tarGzPath);
// Make executable
await Deno.chmod(this.binaryPath, 0o755);
logger.success(`Caddy v${CADDY_VERSION} downloaded to ${this.binaryPath}`);
} catch (error) {
logger.error(`Failed to ensure Caddy binary: ${getErrorMessage(error)}`);
throw error;
}
}
/**
* Start Caddy process with minimal config, then configure via Admin API
*/ */
async start(): Promise<void> { async start(): Promise<void> {
if (this.process) { if (this.serviceRunning) {
logger.warn('Caddy is already running'); logger.warn('Caddy service is already running');
return; return;
} }
try { try {
// Create certs directory await this.ensureDockerClient();
// Create certs directory for backup/persistence
await Deno.mkdir(this.certsDir, { recursive: true }); await Deno.mkdir(this.certsDir, { recursive: true });
logger.info('Starting Caddy server...'); logger.info('Starting Caddy Docker service...');
// Start Caddy with blank config - Admin API is available immediately // Check if service already exists
// We'll push the full configuration via Admin API after it's ready const existingService = await this.getExistingService();
const cmd = new Deno.Command(this.binaryPath, { if (existingService) {
args: ['run'], logger.info('Caddy service exists, removing old service...');
stdin: 'null', await this.removeService();
stdout: 'piped', // Wait for service to be removed
stderr: 'piped', await new Promise((resolve) => setTimeout(resolve, 2000));
}
// Get network ID
const networkId = await this.getNetworkId();
// Create Caddy Docker service
const response = await this.dockerClient!.request('POST', '/services/create', {
Name: CADDY_SERVICE_NAME,
Labels: {
'managed-by': 'onebox',
'onebox-type': 'caddy',
},
TaskTemplate: {
ContainerSpec: {
Image: CADDY_IMAGE,
// Start Caddy with admin listening on all interfaces so we can reach it from host
// Write minimal config to /tmp and start Caddy with that config
Command: ['sh', '-c', 'printf \'{"admin":{"listen":"0.0.0.0:2019"}}\' > /tmp/caddy.json && caddy run --config /tmp/caddy.json'],
},
Networks: [
{
Target: networkId,
},
],
RestartPolicy: {
Condition: 'any',
MaxAttempts: 0,
},
},
Mode: {
Replicated: {
Replicas: 1,
},
},
EndpointSpec: {
Ports: [
{
Protocol: 'tcp',
TargetPort: 80,
PublishedPort: this.httpPort,
PublishMode: 'host',
},
{
Protocol: 'tcp',
TargetPort: 443,
PublishedPort: this.httpsPort,
PublishMode: 'host',
},
{
Protocol: 'tcp',
TargetPort: 2019,
PublishedPort: 2019,
PublishMode: 'host',
},
],
},
}); });
this.process = cmd.spawn(); if (response.statusCode >= 300) {
throw new Error(`Failed to create Caddy service: HTTP ${response.statusCode} - ${JSON.stringify(response.body)}`);
}
// Start log readers (non-blocking) logger.info(`Caddy service created: ${response.body.ID}`);
this.readProcessOutput();
// Wait for Admin API to be ready // Wait for Admin API to be ready
await this.waitForReady(); await this.waitForReady();
this.serviceRunning = true;
// Now configure via Admin API with current routes and certificates // Now configure via Admin API with current routes and certificates
await this.reloadConfig(); await this.reloadConfig();
@@ -231,42 +239,47 @@ export class CaddyManager {
} }
/** /**
* Read process stdout/stderr and log * Get existing Caddy service if any
*/ */
private async readProcessOutput(): Promise<void> { private async getExistingService(): Promise<any | null> {
if (!this.process) return;
// Read stderr (Caddy logs to stderr by default)
const stderrReader = this.process.stderr.getReader();
(async () => {
try { try {
while (true) { const response = await this.dockerClient!.request('GET', `/services/${CADDY_SERVICE_NAME}`, {});
const { done, value } = await stderrReader.read(); if (response.statusCode === 200) {
if (done) break; return response.body;
const text = new TextDecoder().decode(value).trim();
if (text) {
// Parse Caddy's JSON log format or just log as-is
for (const line of text.split('\n')) {
if (line.includes('"level":"error"')) {
logger.error(`[Caddy] ${line}`);
} else if (line.includes('"level":"warn"')) {
logger.warn(`[Caddy] ${line}`);
} else {
logger.debug(`[Caddy] ${line}`);
}
}
}
} }
return null;
} catch { } catch {
// Process ended return null;
} }
})();
} }
/** /**
* Wait for Caddy to be ready by polling admin API * Remove the Caddy service
*/ */
private async waitForReady(maxAttempts = 50, intervalMs = 100): Promise<void> { private async removeService(): Promise<void> {
try {
await this.dockerClient!.request('DELETE', `/services/${CADDY_SERVICE_NAME}`, {});
} catch {
// Service may not exist
}
}
/**
* Get network ID by name
*/
private async getNetworkId(): Promise<string> {
const networks = await this.dockerClient!.listNetworks();
const network = networks.find((n: any) => n.Name === this.networkName);
if (!network) {
throw new Error(`Network not found: ${this.networkName}`);
}
return network.Id;
}
/**
* Wait for Caddy Admin API to be ready
*/
private async waitForReady(maxAttempts = 60, intervalMs = 500): Promise<void> {
for (let i = 0; i < maxAttempts; i++) { for (let i = 0; i < maxAttempts; i++) {
try { try {
const response = await fetch(`${this.adminUrl}/config/`); const response = await fetch(`${this.adminUrl}/config/`);
@@ -278,48 +291,33 @@ export class CaddyManager {
} }
await new Promise((resolve) => setTimeout(resolve, intervalMs)); await new Promise((resolve) => setTimeout(resolve, intervalMs));
} }
throw new Error('Caddy failed to start within timeout'); throw new Error('Caddy service failed to start within timeout');
} }
/** /**
* Stop Caddy process * Stop Caddy Docker service
*/ */
async stop(): Promise<void> { async stop(): Promise<void> {
if (!this.process) { if (!this.serviceRunning && !(await this.getExistingService())) {
return; return;
} }
try { try {
logger.info('Stopping Caddy...'); await this.ensureDockerClient();
// Try graceful shutdown via API first logger.info('Stopping Caddy service...');
try {
await fetch(`${this.adminUrl}/stop`, { method: 'POST' });
// Wait for process to exit
await Promise.race([
this.process.status,
new Promise((resolve) => setTimeout(resolve, 5000)),
]);
} catch {
// API not available, kill directly
}
// Force kill if still running await this.removeService();
try {
this.process.kill('SIGTERM');
} catch {
// Already dead
}
this.process = null; this.serviceRunning = false;
logger.info('Caddy stopped'); logger.info('Caddy service stopped');
} catch (error) { } catch (error) {
logger.error(`Failed to stop Caddy: ${getErrorMessage(error)}`); logger.error(`Failed to stop Caddy: ${getErrorMessage(error)}`);
} }
} }
/** /**
* Check if Caddy is healthy * Check if Caddy Admin API is healthy
*/ */
async isHealthy(): Promise<boolean> { async isHealthy(): Promise<boolean> {
try { try {
@@ -330,6 +328,31 @@ export class CaddyManager {
} }
} }
/**
* Check if Caddy service is running
*/
async isRunning(): Promise<boolean> {
try {
await this.ensureDockerClient();
const service = await this.getExistingService();
if (!service) return false;
// Check if service has running tasks
const tasksResponse = await this.dockerClient!.request(
'GET',
`/tasks?filters=${encodeURIComponent(JSON.stringify({ service: [CADDY_SERVICE_NAME] }))}`,
{}
);
if (tasksResponse.statusCode !== 200) return false;
const tasks = tasksResponse.body;
return tasks.some((task: any) => task.Status?.State === 'running');
} catch {
return false;
}
}
/** /**
* Build Caddy JSON configuration from current routes and certificates * Build Caddy JSON configuration from current routes and certificates
*/ */
@@ -350,27 +373,27 @@ export class CaddyManager {
}); });
} }
// Build certificate load_files // Build certificate load_pem entries (inline PEM content)
const loadFiles: Array<{ certificate: string; key: string; tags?: string[] }> = []; const loadPem: Array<{ certificate: string; key: string; tags?: string[] }> = [];
for (const [domain, cert] of this.certificates) { for (const [domain, cert] of this.certificates) {
loadFiles.push({ loadPem.push({
certificate: cert.certPath, certificate: cert.certPem,
key: cert.keyPath, key: cert.keyPem,
tags: [domain], tags: [domain],
}); });
} }
const config: ICaddyConfig = { const config: ICaddyConfig = {
admin: { admin: {
listen: this.adminUrl.replace('http://', ''), listen: '0.0.0.0:2019', // Listen on all interfaces inside container
}, },
apps: { apps: {
http: { http: {
servers: { servers: {
main: { main: {
listen: [`:${this.httpPort}`, `:${this.httpsPort}`], listen: [':80', ':443'],
routes, routes,
// Disable automatic HTTPS to prevent Caddy from trying to bind to port 80/443 // Disable automatic HTTPS to prevent Caddy from trying to obtain certs
automatic_https: { automatic_https: {
disable: true, disable: true,
}, },
@@ -387,7 +410,8 @@ export class CaddyManager {
access: { access: {
writer: { writer: {
output: 'net', output: 'net',
address: `tcp/localhost:${this.logReceiverPort}`, // Use Docker bridge gateway IP to reach log receiver on host
address: `tcp/${DOCKER_GATEWAY_IP}:${this.logReceiverPort}`,
dial_timeout: '5s', dial_timeout: '5s',
soft_start: true, // Continue even if log receiver is down soft_start: true, // Continue even if log receiver is down
}, },
@@ -405,14 +429,14 @@ export class CaddyManager {
} }
// Add TLS config if we have certificates // Add TLS config if we have certificates
if (loadFiles.length > 0) { if (loadPem.length > 0) {
config.apps.tls = { config.apps.tls = {
automation: { automation: {
// Disable automatic HTTPS - we manage certs ourselves // Disable automatic HTTPS - we manage certs ourselves
policies: [{ issuers: [] }], policies: [{ issuers: [] }],
}, },
certificates: { certificates: {
load_files: loadFiles, load_pem: loadPem,
}, },
}; };
} }
@@ -424,7 +448,8 @@ export class CaddyManager {
* Reload Caddy configuration via Admin API * Reload Caddy configuration via Admin API
*/ */
async reloadConfig(): Promise<void> { async reloadConfig(): Promise<void> {
if (!this.process) { const isRunning = await this.isRunning();
if (!isRunning) {
logger.warn('Caddy not running, cannot reload config'); logger.warn('Caddy not running, cannot reload config');
return; return;
} }
@@ -456,7 +481,7 @@ export class CaddyManager {
async addRoute(domain: string, upstream: string): Promise<void> { async addRoute(domain: string, upstream: string): Promise<void> {
this.routes.set(domain, { domain, upstream }); this.routes.set(domain, { domain, upstream });
if (this.process) { if (await this.isRunning()) {
await this.reloadConfig(); await this.reloadConfig();
} }
@@ -468,7 +493,7 @@ export class CaddyManager {
*/ */
async removeRoute(domain: string): Promise<void> { async removeRoute(domain: string): Promise<void> {
if (this.routes.delete(domain)) { if (this.routes.delete(domain)) {
if (this.process) { if (await this.isRunning()) {
await this.reloadConfig(); await this.reloadConfig();
} }
logger.success(`Removed Caddy route: ${domain}`); logger.success(`Removed Caddy route: ${domain}`);
@@ -477,28 +502,26 @@ export class CaddyManager {
/** /**
* Add or update a TLS certificate * Add or update a TLS certificate
* Writes PEM files to disk and updates config * Stores PEM content in memory for Admin API, also writes to disk for backup
*/ */
async addCertificate(domain: string, certPem: string, keyPem: string): Promise<void> { async addCertificate(domain: string, certPem: string, keyPem: string): Promise<void> {
// Write PEM files // Store PEM content in memory for buildConfig()
const certPath = `${this.certsDir}/${domain}.crt`;
const keyPath = `${this.certsDir}/${domain}.key`;
await Deno.mkdir(this.certsDir, { recursive: true });
await Deno.writeTextFile(certPath, certPem);
await Deno.writeTextFile(keyPath, keyPem);
// Use absolute paths for Caddy
const absoluteCertPath = await Deno.realPath(certPath);
const absoluteKeyPath = await Deno.realPath(keyPath);
this.certificates.set(domain, { this.certificates.set(domain, {
domain, domain,
certPath: absoluteCertPath, certPem,
keyPath: absoluteKeyPath, keyPem,
}); });
if (this.process) { // Also write to disk for backup/persistence
try {
await Deno.mkdir(this.certsDir, { recursive: true });
await Deno.writeTextFile(`${this.certsDir}/${domain}.crt`, certPem);
await Deno.writeTextFile(`${this.certsDir}/${domain}.key`, keyPem);
} catch (error) {
logger.warn(`Failed to write certificate backup for ${domain}: ${getErrorMessage(error)}`);
}
if (await this.isRunning()) {
await this.reloadConfig(); await this.reloadConfig();
} }
@@ -509,19 +532,16 @@ export class CaddyManager {
* Remove a TLS certificate * Remove a TLS certificate
*/ */
async removeCertificate(domain: string): Promise<void> { async removeCertificate(domain: string): Promise<void> {
const cert = this.certificates.get(domain); if (this.certificates.delete(domain)) {
if (cert) { // Remove backup files
this.certificates.delete(domain);
// Remove files
try { try {
await Deno.remove(cert.certPath); await Deno.remove(`${this.certsDir}/${domain}.crt`);
await Deno.remove(cert.keyPath); await Deno.remove(`${this.certsDir}/${domain}.key`);
} catch { } catch {
// Files may not exist // Files may not exist
} }
if (this.process) { if (await this.isRunning()) {
await this.reloadConfig(); await this.reloadConfig();
} }
@@ -562,7 +582,7 @@ export class CaddyManager {
certificates: number; certificates: number;
} { } {
return { return {
running: this.process !== null, running: this.serviceRunning,
httpPort: this.httpPort, httpPort: this.httpPort,
httpsPort: this.httpsPort, httpsPort: this.httpsPort,
routes: this.routes.size, routes: this.routes.size,

View File

@@ -1,8 +1,11 @@
/** /**
* Reverse Proxy for Onebox * Reverse Proxy for Onebox
* *
* Delegates to Caddy for production-grade reverse proxy with native SNI support, * Delegates to Caddy (running as Docker service) for production-grade reverse proxy
* HTTP/2, WebSocket proxying, and zero-downtime configuration updates. * with native SNI support, HTTP/2, WebSocket proxying, and zero-downtime configuration updates.
*
* Routes use Docker service names (e.g., onebox-hello-world:80) for container-to-container
* communication within the Docker overlay network.
*/ */
import { logger } from '../logging.ts'; import { logger } from '../logging.ts';
@@ -15,7 +18,7 @@ interface IProxyRoute {
targetHost: string; targetHost: string;
targetPort: number; targetPort: number;
serviceId: number; serviceId: number;
containerID?: string; serviceName?: string;
} }
export class OneboxReverseProxy { export class OneboxReverseProxy {
@@ -36,16 +39,10 @@ export class OneboxReverseProxy {
} }
/** /**
* Initialize reverse proxy - ensures Caddy binary is available * Initialize reverse proxy - Caddy runs as Docker service, no setup needed
*/ */
async init(): Promise<void> { async init(): Promise<void> {
try { logger.info('Reverse proxy initialized (Caddy Docker service)');
await this.caddy.ensureBinary();
logger.info('Reverse proxy initialized (Caddy)');
} catch (error) {
logger.error(`Failed to initialize reverse proxy: ${getErrorMessage(error)}`);
throw error;
}
} }
/** /**
@@ -61,7 +58,7 @@ export class OneboxReverseProxy {
try { try {
// Start Caddy (handles both HTTP and HTTPS) // Start Caddy (handles both HTTP and HTTPS)
await this.caddy.start(); await this.caddy.start();
logger.success(`Reverse proxy started on port ${this.httpPort} (Caddy)`); logger.success(`Reverse proxy started on port ${this.httpPort} (Caddy Docker service)`);
} catch (error) { } catch (error) {
logger.error(`Failed to start reverse proxy: ${getErrorMessage(error)}`); logger.error(`Failed to start reverse proxy: ${getErrorMessage(error)}`);
throw error; throw error;
@@ -97,46 +94,32 @@ export class OneboxReverseProxy {
/** /**
* Add a route for a service * Add a route for a service
* Uses Docker service name for upstream (Caddy runs in same Docker network)
*/ */
async addRoute(serviceId: number, domain: string, targetPort: number): Promise<void> { async addRoute(serviceId: number, domain: string, targetPort: number): Promise<void> {
try { try {
// Get container IP from Docker // Get service info from database
const service = this.database.getServiceByID(serviceId); const service = this.database.getServiceByID(serviceId);
if (!service || !service.containerID) { if (!service) {
throw new Error(`Service not found or has no container: ${serviceId}`); throw new Error(`Service not found: ${serviceId}`);
} }
// Get container IP from Docker network // Use Docker service name as upstream target
let targetHost = 'localhost'; // Caddy runs on the same Docker network, so it can resolve service names directly
try { const serviceName = `onebox-${service.name}`;
const containerIP = await this.oneboxRef.docker.getContainerIP(service.containerID); const targetHost = serviceName;
if (containerIP) {
targetHost = containerIP;
} else {
// Caddy runs on host, so we need the actual IP
// Try getting task IP from Swarm
const taskIP = await this.getSwarmTaskIP(service.containerID);
if (taskIP) {
targetHost = taskIP;
} else {
logger.warn(`Could not resolve IP for ${service.name}, using localhost`);
}
}
} catch (error) {
logger.warn(`Could not resolve container IP for ${service.name}: ${getErrorMessage(error)}`);
}
const route: IProxyRoute = { const route: IProxyRoute = {
domain, domain,
targetHost, targetHost,
targetPort, targetPort,
serviceId, serviceId,
containerID: service.containerID, serviceName,
}; };
this.routes.set(domain, route); this.routes.set(domain, route);
// Add route to Caddy // Add route to Caddy using Docker service name
const upstream = `${targetHost}:${targetPort}`; const upstream = `${targetHost}:${targetPort}`;
await this.caddy.addRoute(domain, upstream); await this.caddy.addRoute(domain, upstream);
@@ -147,36 +130,6 @@ export class OneboxReverseProxy {
} }
} }
/**
* Get IP address for a Swarm task
*/
private async getSwarmTaskIP(containerIdOrTaskId: string): Promise<string | null> {
try {
// Try to get task details from Swarm
const docker = this.oneboxRef.docker;
// First, try to find the task by inspecting the container
const containerInfo = await docker.inspectContainer(containerIdOrTaskId);
if (containerInfo?.NetworkSettings?.Networks) {
// Get IP from the overlay network
for (const [networkName, networkInfo] of Object.entries(containerInfo.NetworkSettings.Networks)) {
if (networkName.includes('onebox') && (networkInfo as any).IPAddress) {
return (networkInfo as any).IPAddress;
}
}
// Fall back to any network
for (const networkInfo of Object.values(containerInfo.NetworkSettings.Networks)) {
if ((networkInfo as any).IPAddress) {
return (networkInfo as any).IPAddress;
}
}
}
return null;
} catch {
return null;
}
}
/** /**
* Remove a route * Remove a route
*/ */
@@ -213,6 +166,7 @@ export class OneboxReverseProxy {
const services = this.database.getAllServices(); const services = this.database.getAllServices();
for (const service of services) { for (const service of services) {
// Route by domain if running (containerID is the service ID for Swarm services)
if (service.domain && service.status === 'running' && service.containerID) { if (service.domain && service.status === 'running' && service.containerID) {
await this.addRoute(service.id!, service.domain, service.port); await this.addRoute(service.id!, service.domain, service.port);
} }
@@ -227,7 +181,7 @@ export class OneboxReverseProxy {
/** /**
* Add TLS certificate for a domain * Add TLS certificate for a domain
* Writes PEM files to disk for Caddy to load * Sends PEM content to Caddy via Admin API
*/ */
async addCertificate(domain: string, certPem: string, keyPem: string): Promise<void> { async addCertificate(domain: string, certPem: string, keyPem: string): Promise<void> {
if (!certPem || !keyPem) { if (!certPem || !keyPem) {
@@ -288,7 +242,7 @@ export class OneboxReverseProxy {
certificates: caddyStatus.certificates, certificates: caddyStatus.certificates,
}, },
routes: caddyStatus.routes, routes: caddyStatus.routes,
backend: 'caddy', backend: 'caddy-docker',
}; };
} }
} }