diff --git a/app/(dashboard)/settings/SettingsClient.tsx b/app/(dashboard)/settings/SettingsClient.tsx
index 668e60e0..8a36fae1 100644
--- a/app/(dashboard)/settings/SettingsClient.tsx
+++ b/app/(dashboard)/settings/SettingsClient.tsx
@@ -1,13 +1,14 @@
"use client";
import { useFormState } from "react-dom";
-import { Alert, Box, Button, Card, CardContent, Checkbox, FormControlLabel, Stack, TextField, Typography } from "@mui/material";
-import type { GeneralSettings, AuthentikSettings, MetricsSettings } from "@/src/lib/settings";
+import { Alert, Box, Button, Card, CardContent, Checkbox, FormControlLabel, MenuItem, Stack, TextField, Typography } from "@mui/material";
+import type { GeneralSettings, AuthentikSettings, MetricsSettings, LoggingSettings } from "@/src/lib/settings";
import {
updateCloudflareSettingsAction,
updateGeneralSettingsAction,
updateAuthentikSettingsAction,
- updateMetricsSettingsAction
+ updateMetricsSettingsAction,
+ updateLoggingSettingsAction
} from "./actions";
type Props = {
@@ -19,13 +20,15 @@ type Props = {
};
authentik: AuthentikSettings | null;
metrics: MetricsSettings | null;
+ logging: LoggingSettings | null;
};
-export default function SettingsClient({ general, cloudflare, authentik, metrics }: Props) {
+export default function SettingsClient({ general, cloudflare, authentik, metrics, logging }: Props) {
const [generalState, generalFormAction] = useFormState(updateGeneralSettingsAction, null);
const [cloudflareState, cloudflareFormAction] = useFormState(updateCloudflareSettingsAction, null);
const [authentikState, authentikFormAction] = useFormState(updateAuthentikSettingsAction, null);
const [metricsState, metricsFormAction] = useFormState(updateMetricsSettingsAction, null);
+ const [loggingState, loggingFormAction] = useFormState(updateLoggingSettingsAction, null);
return (
@@ -201,6 +204,49 @@ export default function SettingsClient({ general, cloudflare, authentik, metrics
+
+
+
+
+ Access Logging
+
+
+ Enable HTTP access logging to track all requests going through your proxy hosts.
+ Logs will be stored in the caddy-logs directory and mounted at /logs/access.log inside the container.
+
+
+ {loggingState?.message && (
+
+ {loggingState.message}
+
+ )}
+ }
+ label="Enable access logging"
+ />
+
+
+
+
+
+ Access logs will be available at ./caddy-logs/access.log on the host machine.
+ You can tail them with: docker exec caddy-proxy-manager-caddy tail -f /logs/access.log
+
+
+
+
+
+
+
);
}
diff --git a/app/(dashboard)/settings/actions.ts b/app/(dashboard)/settings/actions.ts
index 6eee178f..5c5d9af6 100644
--- a/app/(dashboard)/settings/actions.ts
+++ b/app/(dashboard)/settings/actions.ts
@@ -3,7 +3,7 @@
import { revalidatePath } from "next/cache";
import { requireAdmin } from "@/src/lib/auth";
import { applyCaddyConfig } from "@/src/lib/caddy";
-import { getCloudflareSettings, saveCloudflareSettings, saveGeneralSettings, saveAuthentikSettings, saveMetricsSettings } from "@/src/lib/settings";
+import { getCloudflareSettings, saveCloudflareSettings, saveGeneralSettings, saveAuthentikSettings, saveMetricsSettings, saveLoggingSettings } from "@/src/lib/settings";
type ActionResult = {
success: boolean;
@@ -118,3 +118,39 @@ export async function updateMetricsSettingsAction(_prevState: ActionResult | nul
return { success: false, message: error instanceof Error ? error.message : "Failed to save metrics settings" };
}
}
+
+export async function updateLoggingSettingsAction(_prevState: ActionResult | null, formData: FormData): Promise {
+ try {
+ await requireAdmin();
+ const enabled = formData.get("enabled") === "on";
+ const format = formData.get("format") ? String(formData.get("format")).trim() : "json";
+
+ // Validate format
+ if (format !== "json" && format !== "console") {
+ return { success: false, message: "Invalid log format. Must be 'json' or 'console'" };
+ }
+
+ await saveLoggingSettings({
+ enabled,
+ format: format as "json" | "console"
+ });
+
+ // Apply config to enable/disable logging
+ try {
+ await applyCaddyConfig();
+ revalidatePath("/settings");
+ return { success: true, message: "Logging settings saved and applied successfully" };
+ } catch (error) {
+ console.error("Failed to apply Caddy config:", error);
+ revalidatePath("/settings");
+ const errorMsg = error instanceof Error ? error.message : "Unknown error";
+ return {
+ success: true,
+ message: `Settings saved, but could not apply to Caddy: ${errorMsg}`
+ };
+ }
+ } catch (error) {
+ console.error("Failed to save logging settings:", error);
+ return { success: false, message: error instanceof Error ? error.message : "Failed to save logging settings" };
+ }
+}
diff --git a/app/(dashboard)/settings/page.tsx b/app/(dashboard)/settings/page.tsx
index 9e3e8b75..82ba89dc 100644
--- a/app/(dashboard)/settings/page.tsx
+++ b/app/(dashboard)/settings/page.tsx
@@ -1,15 +1,16 @@
import SettingsClient from "./SettingsClient";
-import { getCloudflareSettings, getGeneralSettings, getAuthentikSettings, getMetricsSettings } from "@/src/lib/settings";
+import { getCloudflareSettings, getGeneralSettings, getAuthentikSettings, getMetricsSettings, getLoggingSettings } from "@/src/lib/settings";
import { requireAdmin } from "@/src/lib/auth";
export default async function SettingsPage() {
await requireAdmin();
- const [general, cloudflare, authentik, metrics] = await Promise.all([
+ const [general, cloudflare, authentik, metrics, logging] = await Promise.all([
getGeneralSettings(),
getCloudflareSettings(),
getAuthentikSettings(),
- getMetricsSettings()
+ getMetricsSettings(),
+ getLoggingSettings()
]);
return (
@@ -22,6 +23,7 @@ export default async function SettingsPage() {
}}
authentik={authentik}
metrics={metrics}
+ logging={logging}
/>
);
}
diff --git a/docker-compose.yml b/docker-compose.yml
index ac223999..9618376e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -69,6 +69,7 @@ services:
volumes:
- ./caddy-data:/data
- ./caddy-config:/config
+ - ./caddy-logs:/logs
networks:
- caddy-network
healthcheck:
diff --git a/src/lib/caddy.ts b/src/lib/caddy.ts
index 1f7b9cca..b3c2e9c1 100644
--- a/src/lib/caddy.ts
+++ b/src/lib/caddy.ts
@@ -3,7 +3,7 @@ import { join } from "node:path";
import crypto from "node:crypto";
import db, { nowIso } from "./db";
import { config } from "./config";
-import { getCloudflareSettings, getGeneralSettings, getMetricsSettings, setSetting } from "./settings";
+import { getCloudflareSettings, getGeneralSettings, getMetricsSettings, getLoggingSettings, setSetting } from "./settings";
import {
accessListEntries,
certificates,
@@ -930,6 +930,11 @@ async function buildCaddyDocument() {
const metricsEnabled = metricsSettings?.enabled ?? false;
const metricsPort = metricsSettings?.port ?? 9090;
+ // Check if access logging should be enabled
+ const loggingSettings = await getLoggingSettings();
+ const loggingEnabled = loggingSettings?.enabled ?? false;
+ const loggingFormat = loggingSettings?.format ?? "json";
+
const servers: Record = {};
// Main HTTP/HTTPS server for proxy hosts
@@ -940,7 +945,9 @@ async function buildCaddyDocument() {
// Only disable automatic HTTPS if we have TLS automation policies
// This allows Caddy to handle HTTP-01 challenges for managed certificates
...(tlsApp ? {} : { automatic_https: { disable: true } }),
- ...(hasTls ? { tls_connection_policies: tlsConnectionPolicies } : {})
+ ...(hasTls ? { tls_connection_policies: tlsConnectionPolicies } : {}),
+ // Enable access logging if configured
+ ...(loggingEnabled ? { logs: { default_logger_name: "http_access" } } : {})
};
}
@@ -966,10 +973,31 @@ async function buildCaddyDocument() {
const httpApp = Object.keys(servers).length > 0 ? { http: { servers } } : {};
+ // Build logging configuration if enabled
+ const loggingApp = loggingEnabled
+ ? {
+ logging: {
+ logs: {
+ http_access: {
+ writer: {
+ output: "file",
+ filename: "/logs/access.log"
+ },
+ encoder: {
+ format: loggingFormat
+ },
+ include: ["http.log.access"]
+ }
+ }
+ }
+ }
+ : {};
+
return {
admin: {
listen: "0.0.0.0:2019"
},
+ ...loggingApp,
apps: {
...httpApp,
...(tlsApp ? { tls: tlsApp } : {})
diff --git a/src/lib/settings.ts b/src/lib/settings.ts
index 4c271d2e..b1d971a6 100644
--- a/src/lib/settings.ts
+++ b/src/lib/settings.ts
@@ -26,6 +26,11 @@ export type MetricsSettings = {
port?: number; // Port to expose metrics on (default: 9090, separate from admin API)
};
+export type LoggingSettings = {
+ enabled: boolean;
+ format?: "json" | "console"; // Log format (default: json)
+};
+
export async function getSetting(key: string): Promise> {
const setting = await db.query.settings.findFirst({
where: (table, { eq }) => eq(table.key, key)
@@ -94,3 +99,11 @@ export async function getMetricsSettings(): Promise {
export async function saveMetricsSettings(settings: MetricsSettings): Promise {
await setSetting("metrics", settings);
}
+
+export async function getLoggingSettings(): Promise {
+ return await getSetting("logging");
+}
+
+export async function saveLoggingSettings(settings: LoggingSettings): Promise {
+ await setSetting("logging", settings);
+}