Run the Caddy TLS ingress as a Docker container

Caddy previously ran as a host binary in /opt/caddy with a wgui-caddy
systemd/OpenRC unit. Only Caddy is containerized; the app still runs on the
host as root because it shells out to wg, ip and iptables.

- Add caddy-container-commands.ts as the single source of truth for the
  run/exec/validate/inspect argument arrays. Every value stays a separate
  argv element, the image is pinned to caddy:2.11.4-alpine, and the
  entrypoint is set explicitly because some mirrors publish the image with
  a null Entrypoint (which made Docker exec "run" as a binary).
- Rewrite install-caddy.sh: require_docker with distro-specific hints,
  pull + recreate the container, assert it is running and echo the daemon
  error instead of discarding it, normalize the admin socket path in
  existing Caddyfiles, and clean up a legacy native install only after the
  container is up so the ingress port is never unbound. Add
  --purge-legacy / --purge for hosts that want to drop the old install
  without installing the container.
- Move the admin socket to /var/lib/wgui/caddy/admin.sock so it lives
  inside the mounted Caddy dir and reloads work over docker exec.
- Validate candidate configs in a throwaway container, mounting the TLS
  dir read-only as well: provisioning the tls app loads every certificate
  referenced by a tls directive, so without that mount a route with a
  certificate failed validation and its Caddyfile was never committed.
- Report docker-based installed/version/service_active, skip
  validate/reload with a warning when the image is absent, and update the
  system setup tool check to look for docker.
- Issue certificates with a docker exec reload command and chown files to
  root:root, since the container reads them through a read-only mount.
- Update the Setup UI copy and docs (README, AGENTS.md).

Also drops the unused EmptyContent import and a stray debug log in the
client route form, and stops passing the unsupported cancel_href prop to
ClientRouteForm from the route add/edit pages.
This commit is contained in:
2026-10-21 14:34:16 +01:00
parent 2cc876b062
commit cbd5451ab7
22 changed files with 652 additions and 318 deletions
+2 -2
View File
@@ -26,7 +26,7 @@ When building, refactoring, styling, or auditing UI components and layouts, acti
## Entrypoint
`src/server.ts` — runs `bunextInit()`, starts `Bun.serve` on `0.0.0.0` (port from `SiteData.ServerPort`), serves `/media/*` via `handleMediaServer`, then runs `syncWireguardHosts()` to bring host tunnels in line with the DB.
`src/server.ts` — runs `bunextInit()`, starts `Bun.serve` on `0.0.0.0` (port from `SiteData.ServerPort`), serves `/media/*` via `handleMediaServer`, then runs `syncWireguardHosts()` to bring host tunnels in line with the DB. Caddy's data plane runs as the `wgui-caddy` Docker container (host networking, `/var/lib/wgui/caddy` and `/var/lib/wgui/tls` bind-mounted); reloads go through `docker exec`, and the admin socket lives at `/var/lib/wgui/caddy/admin.sock`.
On-disk state: WireGuard keys/configs/iptables scripts live under `/var/lib/wgui` (`grabDirNames()` in `src/utils/grab-dir-names.ts`); uploaded media under `DATA_DIR` (default `./.data`).
@@ -82,7 +82,7 @@ Server functions use `index.server.ts` alongside `index.tsx` (e.g. `single-docum
- **`src/hooks/`** — `use-admin-crud-get` (with `refetch`), `use-client-rules-map`, `use-confirm`, `use-toast`, `use-theme`, `use-fetch-get`, `use-form-init`, `use-users-get`.
- **`src/utils/`** — Cookies, crypto, dir names, IP/CIDR/port/interface validators, WireGuard interface naming. Most have `.test.ts` files.
- **`src/dict/`**, **`src/data/`** — Constants (`app-data.ts`, `site-data.ts`), user types, client rules, links.
- **`src/scripts/`** — Shell scripts: `install-wg-ui.sh`, `install-caddy.sh` (idempotent Caddy/acme ingress installer used by the UI and the full installer), `setup-wireguard.sh`, `wg-quick-manage.sh`.
- **`src/scripts/`** — Shell scripts: `install-wg-ui.sh`, `install-caddy.sh` (idempotent installer that pulls the `caddy:<version>-alpine` image and (re)creates the `wgui-caddy` Docker container plus acme.sh renewal; used by the UI and the full installer), `setup-wireguard.sh`, `wg-quick-manage.sh`.
- **`src/components/general/`** — Shared app components, all built from twui. Reach for these before writing page markup:
- Page frame: `page-header` (title, meta, primary action + overflow menu), `page-body`, `panel` (the one card style), `form-section`, `form-actions` (submit is always rendered).
- Text & status: `muted-text`, `mono-text` (IPs, keys, paths), `section-label`, `status-dot`, `inline-alert`, `empty-state`, `stat-tile`, `key-value-list`, `copy-button`, `copyable-value`.
+33 -7
View File
@@ -49,9 +49,9 @@ The install script will:
5. Install app dependencies (`bun install`)
6. Generate `.env` with encryption secrets
7. Run WireGuard setup (if available)
8. Install Caddy (`/opt/caddy`)
8. Install Docker (if missing) and run Caddy as the `wgui-caddy` container
9. Install the `wg-quick` helper script
10. Set up systemd/OpenRC services for `wgui`, `wgui-caddy`, and certificate renewal
10. Set up systemd/OpenRC services for `wgui` and certificate renewal
The installer **never modifies your firewall** — it will print the commands to
temporarily allow the dashboard port so you can reach the first login, and then
@@ -86,13 +86,16 @@ Firewall rules are managed per host, automatically:
### TLS for Tunnel Services
Client routes expose internal services to WireGuard clients over HTTPS. The
data plane is a dedicated Caddy service (`wgui-caddy`) on `8443`; wg-ui owns
the route and certificate records and regenerates the Caddyfile. Caddy never
binds ports 80 or 443, and the host's existing proxy is left alone.
data plane is a dedicated Caddy container (`wgui-caddy`, host networking) on
`8443`; wg-ui owns the route and certificate records and regenerates the
Caddyfile. Caddy never binds ports 80 or 443, and the host's existing proxy is
left alone. Docker is required — the installer starts the daemon if it is not
running.
1. If Caddy isn't installed yet, open **Setup → TLS ingress** and click
**Install Caddy** (or use the Caddy action on **Setup → Maintenance**).
The installer sets up the binary, the `wgui-caddy` service and certificate support.
The installer pulls `caddy:2.11.4-alpine`, (re)creates the `wgui-caddy`
container, and sets up certificate support.
2. Open a route form from **Routes** (or a host's **Routes** panel) and set
the ACME email and staging toggle. No DNS provider token is needed:
@@ -117,7 +120,29 @@ binds ports 80 or 443, and the host's existing proxy is left alone.
Manual DNS-01 certificates are renewed from the route page because validation
needs your registrar. The route list shows expiry warnings. A successful
issuance reloads Caddy without restarting wg-ui. Check
`journalctl -u wgui-caddy` for reload failures.
`docker logs wgui-caddy` for reload failures.
#### Upgrading from the old native Caddy install
Earlier versions ran Caddy as a host binary in `/opt/caddy` with a `wgui-caddy`
systemd/OpenRC service. Re-running the installer migrates the host in place: it
pulls the image, starts the container, and only then removes the old service
and binary. The Caddyfile is rewritten to use the new admin socket path
(`/var/lib/wgui/caddy/admin.sock`); certificates in `/var/lib/wgui/tls` are
untouched.
To remove a leftover native install without installing the container:
```bash
sudo bash src/scripts/install-caddy.sh --purge-legacy
```
Add `--purge` to also remove the container, the image and the certificate
renewal job (the Caddyfile and certificates are always kept):
```bash
sudo bash src/scripts/install-caddy.sh --purge-legacy --purge
```
### Manual (Development)
@@ -142,6 +167,7 @@ The `.env` file (gitignored) controls runtime behavior. It is auto-generated dur
| `ENCRYPTION_KEY` | Encryption key for sensitive data | Auto-generated |
| `ENCRYPTION_SALT` | Encryption salt for sensitive data | Auto-generated |
| `DATA_DIR` | Application data directory | `$INSTALL_DIR/.data` |
| `CADDY_VERSION` | Caddy image tag (`caddy:<version>-alpine`)| `2.11.4` |
| `CADDY_HTTPS_PORT`| Caddy ingress HTTPS port (never 80/443) | `8443` |
| `CADDY_HTTP_PORT` | Caddy plain HTTP port (never 80) | `8081` |
| `CADDY_INGRESS_ENABLED` | Set to `0` to skip Caddy config sync | `1` |
@@ -20,7 +20,6 @@ import { AppContext } from "@/src/pages/__root";
import { useAdminCrudGet } from "@/src/hooks/use-admin-crud-get";
import Select from "../../twui/form/Select";
import adminCrudHandler from "@/src/functions/frontend/admin-crud-handler";
import EmptyContent from "../../twui/elements/EmptyContent";
import twuiNumberfy from "../../twui/utils/numberfy";
type Props = {
@@ -36,7 +35,9 @@ export default function ClientRouteForm({
const { user, pageProps } = useContext(AppContext);
const [host_id, set_host_id] = useState(
passed_host_id || pageProps?.host?.id || pageProps?.hosts?.[0]?.id,
typeof passed_host_id == "number"
? passed_host_id
: pageProps?.host?.id || pageProps?.hosts?.[0]?.id,
);
const is_host_id_present = typeof host_id == "number";
@@ -65,12 +66,6 @@ export default function ClientRouteForm({
return <LoadingBlock className="h-[420px]" />;
}
if (!pageProps?.hosts?.[0]) {
return (
<EmptyContent title="No hosts added. Please Add a host to continue" />
);
}
const cancel_href = is_host_id_present
? `/admin/hosts/${host_id}/routes`
: `/admin/routes`;
@@ -101,20 +96,25 @@ export default function ClientRouteForm({
/>
) : null}
<FormSection title="Host" description="The host for this route">
<Select
options={
pageProps.hosts.map((h) => ({
value: String(h.id),
default: h.id == host_id,
title: h.name,
})) || []
}
changeHandler={(value) => {
set_host_id(twuiNumberfy(value));
}}
/>
</FormSection>
{pageProps?.hosts ? (
<FormSection
title="Host"
description="The host for this route"
>
<Select
options={
pageProps?.hosts?.map((h) => ({
value: String(h.id),
default: h.id == host_id,
title: h.name,
})) || []
}
changeHandler={(value) => {
set_host_id(twuiNumberfy(value));
}}
/>
</FormSection>
) : null}
<FormSection
title="Route"
description="The public hostname and path Caddy matches."
@@ -7,6 +7,10 @@ type Params = {
key_file: string;
};
/**
* Certificate files are consumed by the Caddy container through a read-only
* bind mount, so they only need to be readable by root.
*/
export default function fixCertificatePermissions({
cert_dir,
cert_file,
@@ -18,12 +22,12 @@ export default function fixCertificatePermissions({
chmodSync(key_file, 0o640);
for (const target of [cert_dir, cert_file, key_file]) {
execFileSync("chown", ["root:wgui-caddy", target], {
execFileSync("chown", ["root:root", target], {
stdio: "pipe",
timeout: 5000,
});
}
} catch {
// The group may not exist in development; Caddy runs as root there.
// Permissions are best-effort; the container only needs to read them.
}
}
@@ -5,8 +5,8 @@ import type { BUN_SQLITE_WGUI_CLIENT_ROUTES } from "@/db/types/db";
import BunSQLite from "@moduletrace/bun-sqlite";
import grabDirNames from "@/src/utils/grab-dir-names";
import type { TableType } from "@/src/types";
import grabSystemSetupStatus from "@/src/functions/backend/setup/grab-system-setup-status";
import grabCertDirNames from "../caddy/grab-cert-dir-names";
import { grabCaddyReloadCommandString } from "../caddy/caddy-container-commands";
import syncCaddyIngress from "../caddy/sync-caddy-ingress";
import validateRouteHostname from "../caddy/validate-route-hostname";
import grabCertificateByHostname from "./grab-certificate-by-hostname";
@@ -46,13 +46,13 @@ function fixPermissions({ cert_dir, cert_file, key_file }: {
chmodSync(key_file, 0o640);
for (const target of [cert_dir, cert_file, key_file]) {
execFileSync("chown", ["root:wgui-caddy", target], {
execFileSync("chown", ["root:root", target], {
stdio: "pipe",
timeout: 5000,
});
}
} catch {
// The group may not exist in development; Caddy runs as root there.
// Permissions are best-effort; the container only needs to read them.
}
}
@@ -148,11 +148,7 @@ export default async function issueCertificate({
mkdirSync(CERT_DIR, { recursive: true, mode: 0o750 });
const setup_status = grabSystemSetupStatus();
const reload_command =
setup_status.init_system == "openrc"
? `rc-service wgui-caddy reload`
: `systemctl reload wgui-caddy`;
const reload_command = grabCaddyReloadCommandString();
const child_env = {
...process.env,
@@ -8,7 +8,7 @@ import buildCaddyfile from "./build-caddyfile";
const base = {
https_port: 8443,
http_port: 8081,
admin_socket: `/run/wgui-caddy/admin.sock`,
admin_socket: `/var/lib/wgui/caddy/admin.sock`,
storage_dir: `/var/lib/wgui/caddy/data`,
};
@@ -0,0 +1,116 @@
import { describe, expect, test } from "bun:test";
import grabDirNames from "@/src/utils/grab-dir-names";
import {
CADDY_CONTAINER_NAME,
CADDY_IMAGE,
grabCaddyInspectArgs,
grabCaddyReloadArgs,
grabCaddyReloadCommandString,
grabCaddyRestartArgs,
grabCaddyRunArgs,
grabCaddyValidateArgs,
} from "./caddy-container-commands";
const { WGUI_LIB_CADDY_DIR, WGUI_LIB_CADDY_CONFIG_FILE, WGUI_LIB_TLS_DIR } =
grabDirNames();
describe("caddy container commands", () => {
test("pins the container name and image", () => {
expect(CADDY_CONTAINER_NAME).toBe("wgui-caddy");
expect(CADDY_IMAGE).toBe(
`caddy:${process.env.CADDY_VERSION || "2.11.4"}-alpine`,
);
});
test("runs with host networking and mounts the caddy dir and certs", () => {
const args = grabCaddyRunArgs();
expect(args.slice(0, 3)).toEqual(["run", "-d", "--name"]);
expect(args).toContain("--network");
expect(args[args.indexOf("--network") + 1]).toBe("host");
expect(args).toContain(`${WGUI_LIB_CADDY_DIR}:${WGUI_LIB_CADDY_DIR}`);
expect(args).toContain(`${WGUI_LIB_TLS_DIR}:${WGUI_LIB_TLS_DIR}:ro`);
expect(args).toContain("--restart");
expect(args[args.indexOf("--restart") + 1]).toBe("unless-stopped");
expect(args).toContain(CADDY_IMAGE);
// The entrypoint must be explicit: mirrors exist that publish the image
// with a null Entrypoint, which would exec "run" as a binary.
expect(args).toContain("--entrypoint");
expect(args[args.indexOf("--entrypoint") + 1]).toBe("caddy");
expect(args[args.indexOf("--entrypoint") + 2]).toBe(CADDY_IMAGE);
expect(args.slice(-5)).toEqual([
"run",
"--config",
WGUI_LIB_CADDY_CONFIG_FILE,
"--adapter",
"caddyfile",
]);
});
test("reloads through docker exec with the unix admin socket", () => {
const args = grabCaddyReloadArgs();
expect(args.slice(0, 4)).toEqual([
"exec",
CADDY_CONTAINER_NAME,
"caddy",
"reload",
]);
expect(args).toContain("--config");
expect(args[args.indexOf("--config") + 1]).toBe(
WGUI_LIB_CADDY_CONFIG_FILE,
);
expect(args[args.indexOf("--address") + 1]).toBe(
`unix/${WGUI_LIB_CADDY_DIR}/admin.sock`,
);
});
test("restart fallback targets the container", () => {
expect(grabCaddyRestartArgs()).toEqual([
"restart",
CADDY_CONTAINER_NAME,
]);
});
test("validates in a throwaway container without networking", () => {
const config_path = `${WGUI_LIB_CADDY_DIR}/Caddyfile.tmp`;
const args = grabCaddyValidateArgs({ config_path });
expect(args.slice(0, 2)).toEqual(["run", "--rm"]);
expect(args).toContain("--network");
expect(args[args.indexOf("--network") + 1]).toBe("none");
expect(args).toContain("--entrypoint");
expect(args[args.indexOf("--entrypoint") + 1]).toBe("caddy");
expect(args).toContain(`${WGUI_LIB_CADDY_DIR}:${WGUI_LIB_CADDY_DIR}`);
// validate provisions the tls app, so route certificates must be
// visible or every config with a `tls` directive fails validation.
expect(args).toContain(`${WGUI_LIB_TLS_DIR}:${WGUI_LIB_TLS_DIR}:ro`);
expect(args).toContain(CADDY_IMAGE);
expect(args.slice(-5)).toEqual([
"validate",
"--config",
config_path,
"--adapter",
"caddyfile",
]);
});
test("inspect reads the running flag", () => {
expect(grabCaddyInspectArgs()).toEqual([
"inspect",
"--format",
"{{.State.Running}}",
CADDY_CONTAINER_NAME,
]);
});
test("the acme reload command mirrors the exec args", () => {
const command = grabCaddyReloadCommandString();
expect(command).toBe(
["docker", ...grabCaddyReloadArgs()].join(" "),
);
expect(command).not.toContain("systemctl");
expect(command).not.toContain("rc-service");
});
});
@@ -0,0 +1,141 @@
import grabDirNames from "@/src/utils/grab-dir-names";
/**
* Caddy runs as a Docker container instead of a host binary. Every argument is
* kept as its own array element — nothing here is interpolated into a shell
* string, except `grabCaddyReloadCommandString()` which is handed to acme.sh.
*
* The container uses host networking and mounts the Caddy state directory at
* the exact same path as the host, so the paths below are valid on both sides.
*/
const {
WGUI_LIB_CADDY_DIR,
WGUI_LIB_CADDY_CONFIG_FILE,
WGUI_LIB_CADDY_ADMIN_SOCKET,
WGUI_LIB_TLS_DIR,
} = grabDirNames();
export const CADDY_CONTAINER_NAME = `wgui-caddy`;
export const CADDY_VERSION = process.env.CADDY_VERSION || `2.11.4`;
export const CADDY_IMAGE = `caddy:${CADDY_VERSION}-alpine`;
export const CADDY_CONFIG_FILE_IN_CONTAINER = WGUI_LIB_CADDY_CONFIG_FILE;
export const CADDY_ADMIN_SOCKET_IN_CONTAINER = WGUI_LIB_CADDY_ADMIN_SOCKET;
function volumeArgs() {
return [
`-v`,
`${WGUI_LIB_CADDY_DIR}:${WGUI_LIB_CADDY_DIR}`,
`-v`,
`${WGUI_LIB_TLS_DIR}:${WGUI_LIB_TLS_DIR}:ro`,
];
}
export function grabCaddyRunArgs() {
return [
`run`,
`-d`,
`--name`,
CADDY_CONTAINER_NAME,
`--restart`,
`unless-stopped`,
`--network`,
`host`,
...volumeArgs(),
// Set the entrypoint explicitly instead of trusting the image metadata:
// some mirrors publish the image with a null Entrypoint, which would
// make Docker exec the first arg ("run") as a binary.
`--entrypoint`,
`caddy`,
CADDY_IMAGE,
`run`,
`--config`,
CADDY_CONFIG_FILE_IN_CONTAINER,
`--adapter`,
`caddyfile`,
];
}
export function grabCaddyReloadArgs() {
return [
`exec`,
CADDY_CONTAINER_NAME,
`caddy`,
`reload`,
`--config`,
CADDY_CONFIG_FILE_IN_CONTAINER,
`--adapter`,
`caddyfile`,
`--address`,
`unix/${CADDY_ADMIN_SOCKET_IN_CONTAINER}`,
];
}
export function grabCaddyRestartArgs() {
return [`restart`, CADDY_CONTAINER_NAME];
}
/**
* Validate a candidate Caddyfile in a throwaway container so a bad config can
* never take the running ingress down. The config must live inside the mounted
* Caddy directory for the container to see it.
*
* The TLS directory is mounted too: `caddy validate` provisions the tls app,
* which loads every certificate referenced by a `tls` directive, and a route
* whose certificate file is missing makes validation fail.
*/
export function grabCaddyValidateArgs({ config_path }: { config_path: string }) {
return [
`run`,
`--rm`,
`--network`,
`none`,
`--entrypoint`,
`caddy`,
...volumeArgs(),
CADDY_IMAGE,
`validate`,
`--config`,
config_path,
`--adapter`,
`caddyfile`,
];
}
export function grabCaddyInspectArgs() {
return [
`inspect`,
`--format`,
`{{.State.Running}}`,
CADDY_CONTAINER_NAME,
];
}
export function grabCaddyImageInspectArgs() {
return [`image`, `inspect`, CADDY_IMAGE];
}
export function grabCaddyExecVersionArgs() {
return [`exec`, CADDY_CONTAINER_NAME, `caddy`, `version`];
}
/**
* acme.sh runs `--reloadcmd` through a shell, so this one is a string. All
* values come from the constants above (no user input).
*/
export function grabCaddyReloadCommandString() {
return [
`docker`,
`exec`,
CADDY_CONTAINER_NAME,
`caddy`,
`reload`,
`--config`,
CADDY_CONFIG_FILE_IN_CONTAINER,
`--adapter`,
`caddyfile`,
`--address`,
`unix/${CADDY_ADMIN_SOCKET_IN_CONTAINER}`,
].join(` `);
}
@@ -0,0 +1,20 @@
import { execFileSync } from "node:child_process";
import { grabCaddyImageInspectArgs } from "./caddy-container-commands";
/**
* True when the Caddy image has been pulled. Until then the generated config
* cannot be validated or served (e.g. development machines without Docker).
*/
export default function caddyImageInstalled() {
try {
execFileSync("docker", grabCaddyImageInspectArgs(), {
encoding: "utf-8",
stdio: "pipe",
timeout: 5000,
});
return true;
} catch {
return false;
}
}
@@ -2,10 +2,15 @@ import { execFileSync } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import grabDirNames from "@/src/utils/grab-dir-names";
import { TlsData } from "@/src/data/tls-data";
import grabSystemSetupStatus from "@/src/functions/backend/setup/grab-system-setup-status";
import caddyImageInstalled from "./caddy-image-installed";
import {
CADDY_CONTAINER_NAME,
CADDY_IMAGE,
grabCaddyExecVersionArgs,
grabCaddyInspectArgs,
} from "./caddy-container-commands";
const {
WGUI_CADDY_BINARY,
WGUI_LIB_CADDY_CONFIG_FILE,
WGUI_LIB_CADDY_STATE_FILE,
WGUI_ACME_SCRIPT,
@@ -30,9 +35,9 @@ function readState() {
}
}
function version() {
function docker({ args }: { args: string[] }) {
try {
return execFileSync(WGUI_CADDY_BINARY, ["version"], {
return execFileSync("docker", args, {
encoding: "utf-8",
stdio: "pipe",
timeout: 5000,
@@ -42,38 +47,28 @@ function version() {
}
}
function serviceActive() {
const setup_status = grabSystemSetupStatus();
function version() {
const running = docker({ args: grabCaddyExecVersionArgs() });
try {
if (setup_status.init_system == "systemd") {
return (
execFileSync(
"systemctl",
["is-active", "wgui-caddy"],
{
encoding: "utf-8",
stdio: "pipe",
timeout: 5000,
},
).trim() == "active"
);
}
if (setup_status.init_system == "openrc") {
execFileSync("rc-service", ["wgui-caddy", "status"], {
encoding: "utf-8",
stdio: "pipe",
timeout: 5000,
});
return true;
}
} catch {
return false;
if (running) {
return running;
}
return null;
if (!caddyImageInstalled()) {
return null;
}
return CADDY_IMAGE;
}
function serviceActive() {
const running = docker({ args: grabCaddyInspectArgs() });
if (running == null) {
return null;
}
return running == "true";
}
type Settings = {
@@ -85,7 +80,10 @@ export default function grabCaddyStatus(settings?: Settings) {
const state = readState();
return {
installed: existsSync(WGUI_CADDY_BINARY),
runtime: "docker" as const,
container_name: CADDY_CONTAINER_NAME,
image: CADDY_IMAGE,
installed: caddyImageInstalled(),
service_active: serviceActive(),
acme_installed: existsSync(WGUI_ACME_SCRIPT),
acme_env_exists: existsSync(WGUI_ACME_ENV_FILE),
@@ -1,11 +1,12 @@
import { execFileSync } from "node:child_process";
import grabSystemSetupStatus from "@/src/functions/backend/setup/grab-system-setup-status";
import {
grabCaddyReloadArgs,
grabCaddyRestartArgs,
} from "./caddy-container-commands";
const CADDY_SERVICE_NAME = "wgui-caddy";
function run({ command, args }: { command: string; args: string[] }) {
function run({ args }: { args: string[] }) {
try {
const output = execFileSync(command, args, {
const output = execFileSync("docker", args, {
encoding: "utf-8",
stdio: "pipe",
timeout: 30000,
@@ -29,58 +30,24 @@ function run({ command, args }: { command: string; args: string[] }) {
}
}
function caddyServiceIsActive({
init_system,
}: {
init_system: ReturnType<typeof grabSystemSetupStatus>["init_system"];
}) {
if (init_system == "systemd") {
const status = run({
command: "systemctl",
args: ["is-active", CADDY_SERVICE_NAME],
});
return status.success && status.msg == "active";
}
if (init_system == "openrc") {
return run({
command: "rc-service",
args: [CADDY_SERVICE_NAME, "status"],
}).success;
}
return false;
}
/**
* Reload the Caddy container's config over the admin socket. If the container
* is missing (e.g. it was recreated or removed by hand), fall back to a restart
* so a fresh container picks up the new Caddyfile.
*/
export default function reloadCaddyService() {
const setup_status = grabSystemSetupStatus();
const service_active = caddyServiceIsActive({
init_system: setup_status.init_system,
});
const reload = run({ args: grabCaddyReloadArgs() });
if (setup_status.is_dev && !service_active) {
return {
success: true as const,
msg: `Development mode — Caddy service is not running, reload skipped`,
};
if (reload.success) {
return reload;
}
switch (setup_status.init_system) {
case "systemd":
return run({
command: "systemctl",
args: ["reload", CADDY_SERVICE_NAME],
});
case "openrc":
return run({
command: "rc-service",
args: [CADDY_SERVICE_NAME, "reload"],
});
default:
return {
success: false as const,
msg: `No supported init system found for wgui-caddy`,
};
}
const restart = run({ args: grabCaddyRestartArgs() });
return restart.success
? {
success: true as const,
msg: `Caddy container restarted`,
}
: reload;
}
@@ -18,6 +18,8 @@ import type { TableType } from "@/src/types";
import grabDirNames from "@/src/utils/grab-dir-names";
import { TlsData } from "@/src/data/tls-data";
import buildCaddyfile from "./build-caddyfile";
import caddyImageInstalled from "./caddy-image-installed";
import { CADDY_IMAGE, grabCaddyValidateArgs } from "./caddy-container-commands";
import reloadCaddyService from "./reload-caddy-service";
import applyCaddyIngressFirewallRule from "./apply-caddy-ingress-firewall-rule";
import grabCaddySettings from "./grab-caddy-settings";
@@ -33,7 +35,6 @@ type Params = {
};
const {
WGUI_CADDY_BINARY,
WGUI_LIB_CADDY_DIR,
WGUI_LIB_CADDY_CONFIG_FILE,
WGUI_LIB_CADDY_STATE_FILE,
@@ -155,29 +156,23 @@ async function doSync({ reason }: Params): Promise<CaddySyncResult> {
writeFileSync(tmp_path, built.config, { mode: 0o644 });
if (existsSync(WGUI_CADDY_BINARY)) {
// Without the image the Caddyfile can be neither validated nor served —
// write it and let the installer/Setup page take over.
if (caddyImageInstalled()) {
try {
execFileSync(
WGUI_CADDY_BINARY,
[
"validate",
"--config",
tmp_path,
"--adapter",
"caddyfile",
],
"docker",
grabCaddyValidateArgs({ config_path: tmp_path }),
{
encoding: "utf-8",
stdio: "pipe",
timeout: 30000,
timeout: 60000,
},
);
} catch (error: any) {
rmSync(tmp_path, { force: true });
const msg = `Caddy rejected the generated config: ${
error?.stderr
? String(error.stderr).trim()
: error.message
error?.stderr ? String(error.stderr).trim() : error.message
}`;
writeState({ warnings: built.warnings, last_error: msg });
@@ -188,6 +183,10 @@ async function doSync({ reason }: Params): Promise<CaddySyncResult> {
warnings: built.warnings,
};
}
} else {
built.warnings.push(
`Caddy image ${CADDY_IMAGE} is not installed — config not validated or reloaded`,
);
}
if (existsSync(WGUI_LIB_CADDY_CONFIG_FILE)) {
@@ -196,7 +195,9 @@ async function doSync({ reason }: Params): Promise<CaddySyncResult> {
renameSync(tmp_path, WGUI_LIB_CADDY_CONFIG_FILE);
const reload = reloadCaddyService();
const reload = caddyImageInstalled()
? reloadCaddyService()
: { success: true as const, msg: `skipped, image not installed` };
const warnings = [...built.warnings];
if (!reload.success) {
@@ -85,9 +85,9 @@ const TOOL_CHECKS: {
version_command: "bun --version",
},
{
name: "caddy",
command: "/opt/caddy/caddy",
version_command: "/opt/caddy/caddy version",
name: "caddy (docker)",
command: "docker",
version_command: "docker --version",
},
];
@@ -22,10 +22,7 @@ export default function AddClientRoutePage() {
}}
/>
<PageBody>
<ClientRouteForm
host_id={host_id}
cancel_href={`/admin/hosts/${host_id}/routes`}
/>
<ClientRouteForm host_id={host_id} />
</PageBody>
</>
);
@@ -28,7 +28,6 @@ export default function EditClientRoutePage() {
<ClientRouteForm
host_id={host_id}
existing_route={route}
cancel_href={`/admin/hosts/${host_id}/routes`}
/>
) : (
<EmptyState
@@ -13,13 +13,13 @@ export default function SetupCaddyAction({ installed, on_done }: Props) {
return (
<SetupActionRow
title="Caddy TLS ingress"
description="Installs or repairs the Caddy binary and the wgui-caddy service. Certificate issuance is handled by wg-ui with manual DNS-01. Safe to re-run."
description="Pulls the Caddy image and (re)creates the wgui-caddy container with host networking. Requires Docker. Certificate issuance is handled by wg-ui with manual DNS-01. Safe to re-run."
icon={<ServerCog size={15} />}
button_title={installed ? "Repair Caddy" : "Install Caddy"}
confirm_params={{
title: installed ? "Repair the Caddy ingress?" : "Install Caddy?",
message:
"This installs Caddy and the wgui-caddy service. Existing routes and certificates are preserved.",
"This pulls the Caddy image and restarts the wgui-caddy container. Existing routes and certificates are preserved.",
confirm_label: installed ? "Repair Caddy" : "Install Caddy",
}}
on_run={async () => {
@@ -26,7 +26,7 @@ export default function SetupActionsSection({ setup }: Props) {
.every((tool) => tool.installed);
const have_caddy = Boolean(
setup_status.tools.find((tool) => tool.command == "/opt/caddy/caddy")
setup_status.tools.find((tool) => tool.name.startsWith("caddy"))
?.installed,
);
@@ -18,7 +18,7 @@ export default function TlsStatusSection({ tls }: Props) {
return (
<Panel
title="Ingress"
description="Caddy runs as its own service and terminates TLS on the configured high port."
description="Caddy runs as a Docker container (host networking) and terminates TLS on the configured high port."
actions={
<>
<StatusDot
@@ -33,7 +33,7 @@ export default function TlsStatusSection({ tls }: Props) {
}
label={
!caddy?.installed
? "Caddy not installed"
? "Caddy image missing"
: caddy.service_active === false
? "wgui-caddy stopped"
: caddy.version || "Caddy installed"
@@ -70,7 +70,7 @@ export default function TlsStatusSection({ tls }: Props) {
{!caddy?.installed ? (
<InlineAlert
tone="warning"
msg="Caddy is not installed yet. Run the installer/update script, or install it to /opt/caddy/caddy."
msg="The Caddy image is not present yet. Click “Install Caddy” below — Docker is required."
/>
) : null}
{caddy?.state?.last_error ? (
@@ -94,6 +94,14 @@ export default function TlsStatusSection({ tls }: Props) {
? `${caddy.https_port}/tcp`
: "—",
},
{
key: "runtime",
label: "Runtime",
value: caddy
? `docker · ${caddy.container_name} · ${caddy.image}`
: "—",
mono: true,
},
{
key: "config",
label: "Caddyfile",
+1 -7
View File
@@ -11,12 +11,7 @@ import type {
const INSTALL_CADDY_SCRIPT = `install-caddy.sh`;
const INSTALL_CADDY_TIMEOUT_MS = 20 * 60 * 1000;
const {
WGUI_LIB_DIR,
WGUI_CADDY_BIN_DIR,
WGUI_ACME_HOME,
WGUI_ACME_ENV_FILE,
} = grabDirNames();
const { WGUI_LIB_DIR, WGUI_ACME_HOME, WGUI_ACME_ENV_FILE } = grabDirNames();
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
req,
@@ -37,7 +32,6 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
env: {
WGUI_LIB_DIR,
CADDY_VERSION: process.env.CADDY_VERSION,
CADDY_BIN_DIR: WGUI_CADDY_BIN_DIR,
CADDY_HTTPS_PORT: String(TlsData.CaddyHttpsPort),
CADDY_HTTP_PORT: String(TlsData.CaddyHttpPort),
ACME_HOME: WGUI_ACME_HOME,
+210 -139
View File
@@ -1,6 +1,7 @@
#!/bin/bash
# Install or repair the Caddy TLS ingress used by client routes.
# Caddy runs as a Docker container (host networking) instead of a host binary.
# Safe to re-run: it preserves the existing Caddyfile and certificate data.
set -euo pipefail
@@ -10,14 +11,15 @@ fail() { echo "error: $*" >&2; exit 1; }
WGUI_LIB_DIR="${WGUI_LIB_DIR:-/var/lib/wgui}"
CADDY_VERSION="${CADDY_VERSION:-2.11.4}"
CADDY_BIN_DIR="${CADDY_BIN_DIR:-/opt/caddy}"
CADDY_BIN="$CADDY_BIN_DIR/caddy"
CADDY_USER="${CADDY_USER:-wgui-caddy}"
CADDY_IMAGE="caddy:${CADDY_VERSION}-alpine"
CADDY_CONTAINER_NAME="wgui-caddy"
CADDY_DIR="$WGUI_LIB_DIR/caddy"
CADDY_CONFIG_FILE="$CADDY_DIR/Caddyfile"
CADDY_ADMIN_SOCKET="$CADDY_DIR/admin.sock"
CADDY_HTTPS_PORT="${CADDY_HTTPS_PORT:-8443}"
CADDY_HTTP_PORT="${CADDY_HTTP_PORT:-8081}"
ACME_HOME="${ACME_HOME:-/opt/acme.sh}"
ACME_ENV_FILE="${ACME_ENV_FILE:-/etc/wgui/acme.env}"
SERVICE_NAME="wgui-caddy"
require_root() {
if [ "$(id -u)" -ne 0 ]; then
@@ -45,19 +47,32 @@ detect_init_system() {
fi
}
caddy_arch() {
case "$(uname -m)" in
x86_64 | amd64) echo "amd64" ;;
aarch64 | arm64 | armv8l | armv8) echo "arm64" ;;
armv7l | armv7) echo "armv7" ;;
armv6l | armv6) echo "armv6" ;;
*) fail "unsupported architecture for Caddy: $(uname -m)" ;;
docker_install_hint() {
local distro
distro="$(detect_distro)"
case "$distro" in
debian | ubuntu | linuxmint | raspbian)
echo "apt-get install -y docker.io"
;;
fedora | rhel | centos | rocky | almalinux | ol)
echo "dnf install -y docker"
;;
arch | manjaro | endeavouros)
echo "pacman -S --noconfirm docker"
;;
alpine)
echo "apk add docker"
;;
*)
echo "install Docker from https://docs.docker.com/engine/install/"
;;
esac
}
install_deps() {
local missing=false
local tool
local missing=false
for tool in curl tar git openssl; do
if ! command -v "$tool" >/dev/null 2>&1; then
@@ -97,50 +112,36 @@ install_deps() {
esac
}
install_caddy() {
if [ -x "$CADDY_BIN" ]; then
if [ ! -e /usr/local/bin/caddy ]; then
ln -sf "$CADDY_BIN" /usr/local/bin/caddy
fi
log "caddy: $($CADDY_BIN version)"
return
require_docker() {
if ! command -v docker >/dev/null 2>&1; then
fail "docker is required for the Caddy ingress — install it with: $(docker_install_hint)"
fi
local arch
arch="$(caddy_arch)"
local archive="caddy_${CADDY_VERSION}_linux_${arch}.tar.gz"
local base_url="https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}"
local tmp_dir
tmp_dir="$(mktemp -d)"
local init_system
init_system="$(detect_init_system)"
log "installing Caddy ${CADDY_VERSION} (${arch}) to ${CADDY_BIN_DIR} ..."
mkdir -p "$CADDY_BIN_DIR"
if ! docker info >/dev/null 2>&1; then
log "starting the docker daemon ..."
curl -fsSL -o "$tmp_dir/$archive" "$base_url/$archive"
curl -fsSL -o "$tmp_dir/checksums.txt" "$base_url/caddy_${CADDY_VERSION}_checksums.txt"
if command -v sha512sum >/dev/null 2>&1; then
(cd "$tmp_dir" && grep " $archive\$" checksums.txt | sha512sum -c -)
elif command -v openssl >/dev/null 2>&1; then
local expected actual
expected="$(grep " $archive\$" "$tmp_dir/checksums.txt" | awk '{print $1}')"
actual="$(openssl dgst -sha512 "$tmp_dir/$archive" | awk '{print $NF}')"
if [ -z "$expected" ] || [ "$expected" != "$actual" ]; then
fail "Caddy checksum verification failed for $archive"
fi
else
fail "no sha512 verification tool available (install coreutils or openssl)"
case "$init_system" in
systemd)
systemctl enable --now docker
;;
openrc)
rc-update add docker default
rc-service docker start
;;
*)
fail "docker is installed but not running — start it manually and re-run this script"
;;
esac
fi
tar -xzf "$tmp_dir/$archive" -C "$CADDY_BIN_DIR" caddy
chmod 755 "$CADDY_BIN"
if [ ! -e /usr/local/bin/caddy ]; then
ln -sf "$CADDY_BIN" /usr/local/bin/caddy
if ! docker info >/dev/null 2>&1; then
fail "docker is not running — start it and re-run this script"
fi
rm -rf "$tmp_dir"
log "caddy: $($CADDY_BIN version)"
log "docker: $(docker --version)"
}
install_acme_sh() {
@@ -156,29 +157,15 @@ install_acme_sh() {
chmod 700 "$ACME_HOME/acme.sh"
}
setup_caddy() {
if ! id -u "$CADDY_USER" >/dev/null 2>&1; then
log "creating service user $CADDY_USER ..."
if command -v useradd >/dev/null 2>&1; then
useradd --system --user-group --home-dir "$WGUI_LIB_DIR/caddy" --shell /usr/sbin/nologin "$CADDY_USER"
else
adduser -S -H -h "$WGUI_LIB_DIR/caddy" -s /sbin/nologin "$CADDY_USER"
fi
fi
if ! id -g "$CADDY_USER" >/dev/null 2>&1 && command -v groupadd >/dev/null 2>&1; then
groupadd --system "$CADDY_USER"
fi
mkdir -p "$WGUI_LIB_DIR/caddy/data" "$WGUI_LIB_DIR/tls"
setup_caddy_dirs() {
# The official Caddy image runs as root, so the mounted state directory is
# root-owned; nothing else on the host needs access to it.
mkdir -p "$CADDY_DIR/data" "$WGUI_LIB_DIR/tls" /etc/wgui
chmod 755 "$WGUI_LIB_DIR"
chown root:"$CADDY_USER" "$WGUI_LIB_DIR/tls"
chmod 750 "$CADDY_DIR"
chmod 750 "$CADDY_DIR/data"
chmod 750 "$WGUI_LIB_DIR/tls"
chown -R "$CADDY_USER:$CADDY_USER" "$WGUI_LIB_DIR/caddy"
chmod 750 "$WGUI_LIB_DIR/caddy"
chmod 750 "$WGUI_LIB_DIR/caddy/data"
mkdir -p /etc/wgui
chown -R root:root "$CADDY_DIR" "$WGUI_LIB_DIR/tls"
chmod 700 /etc/wgui
if [ ! -f "$ACME_ENV_FILE" ]; then
@@ -194,89 +181,134 @@ EOF
chown root:root "$ACME_ENV_FILE"
fi
if [ ! -f "$WGUI_LIB_DIR/caddy/Caddyfile" ]; then
if [ ! -f "$CADDY_CONFIG_FILE" ]; then
log "writing initial Caddyfile ..."
cat > "$WGUI_LIB_DIR/caddy/Caddyfile" <<EOF
cat > "$CADDY_CONFIG_FILE" <<EOF
{
https_port $CADDY_HTTPS_PORT
http_port $CADDY_HTTP_PORT
auto_https disable_redirects
admin unix//run/wgui-caddy/admin.sock
storage file_system $WGUI_LIB_DIR/caddy/data
admin unix/$CADDY_ADMIN_SOCKET
storage file_system $CADDY_DIR/data
}
EOF
chown "$CADDY_USER:$CADDY_USER" "$WGUI_LIB_DIR/caddy/Caddyfile"
chmod 640 "$WGUI_LIB_DIR/caddy/Caddyfile"
chmod 640 "$CADDY_CONFIG_FILE"
chown root:root "$CADDY_CONFIG_FILE"
fi
}
install_systemd_unit() {
local unit="/etc/systemd/system/$SERVICE_NAME.service"
log "writing systemd unit $unit ..."
cat > "$unit" <<EOF
[Unit]
Description=wg-ui TLS ingress (Caddy)
After=network-online.target
Wants=network-online.target
# Older installs used an admin socket under /run, which does not exist inside
# the container. Point an existing Caddyfile at the mounted socket path.
normalize_caddyfile() {
[ -f "$CADDY_CONFIG_FILE" ] || return
[Service]
Type=simple
User=$CADDY_USER
Group=$CADDY_USER
Environment=HOME=$WGUI_LIB_DIR/caddy
ExecStart=$CADDY_BIN run --config $WGUI_LIB_DIR/caddy/Caddyfile --adapter caddyfile
ExecReload=$CADDY_BIN reload --config $WGUI_LIB_DIR/caddy/Caddyfile --adapter caddyfile --address unix//run/wgui-caddy/admin.sock
Restart=on-failure
RestartSec=5
RuntimeDirectory=wgui-caddy
RuntimeDirectoryMode=0750
PrivateTmp=true
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=$WGUI_LIB_DIR/caddy
UMask=0077
if grep -qx " admin unix/$CADDY_ADMIN_SOCKET" "$CADDY_CONFIG_FILE"; then
return
fi
[Install]
WantedBy=multi-user.target
EOF
log "updating the admin socket path in the existing Caddyfile ..."
local tmp
tmp="$(mktemp)"
systemctl daemon-reload
systemctl enable "$SERVICE_NAME.service"
systemctl restart "$SERVICE_NAME.service"
awk -v admin=" admin unix/$CADDY_ADMIN_SOCKET" '
BEGIN { inserted = 0 }
/^[[:space:]]*admin / { next }
{
print
if (!inserted && $0 ~ /^[[:space:]]*\{/) {
print admin
inserted = 1
}
}
END {
if (!inserted) print admin
}
' "$CADDY_CONFIG_FILE" > "$tmp"
chmod 640 "$tmp"
chown root:root "$tmp"
mv "$tmp" "$CADDY_CONFIG_FILE"
}
install_openrc_unit() {
local init_script="/etc/init.d/$SERVICE_NAME"
log "writing openrc init script $init_script ..."
cat > "$init_script" <<EOF
#!/sbin/openrc-run
name="$SERVICE_NAME"
description="wg-ui TLS ingress (Caddy)"
command="$CADDY_BIN"
command_args="run --config $WGUI_LIB_DIR/caddy/Caddyfile --adapter caddyfile"
command_user="$CADDY_USER:$CADDY_USER"
directory="$WGUI_LIB_DIR/caddy"
pidfile="/run/\${RC_SVCNAME}.pid"
install_container() {
log "pulling $CADDY_IMAGE ..."
docker pull "$CADDY_IMAGE"
start_pre() {
mkdir -p /run/wgui-caddy
chown "$CADDY_USER:$CADDY_USER" /run/wgui-caddy
if docker inspect "$CADDY_CONTAINER_NAME" >/dev/null 2>&1; then
log "removing the existing $CADDY_CONTAINER_NAME container ..."
docker rm -f "$CADDY_CONTAINER_NAME" >/dev/null
fi
log "starting the $CADDY_CONTAINER_NAME container ..."
local run_output=""
# The entrypoint is set explicitly: some mirrors publish the image with a
# null Entrypoint, which would make Docker exec the first arg as a binary.
if ! run_output="$(docker run -d \
--name "$CADDY_CONTAINER_NAME" \
--restart unless-stopped \
--network host \
-v "$CADDY_DIR:$CADDY_DIR" \
-v "$WGUI_LIB_DIR/tls:$WGUI_LIB_DIR/tls:ro" \
--entrypoint caddy \
"$CADDY_IMAGE" \
run --config "$CADDY_CONFIG_FILE" --adapter caddyfile 2>&1)"; then
fail "could not start the $CADDY_CONTAINER_NAME container: $run_output"
fi
local running
running="$(docker inspect --format '{{.State.Running}}' "$CADDY_CONTAINER_NAME" 2>/dev/null || echo false)"
if [ "$running" != "true" ]; then
docker logs --tail 50 "$CADDY_CONTAINER_NAME" >&2 || true
fail "the $CADDY_CONTAINER_NAME container failed to start"
fi
log "caddy: $(docker exec "$CADDY_CONTAINER_NAME" caddy version 2>/dev/null || echo "$CADDY_IMAGE")"
}
reload() {
ebegin "Reloading Caddy"
"$CADDY_BIN" reload --config "$WGUI_LIB_DIR/caddy/Caddyfile" --adapter caddyfile --address unix//run/wgui-caddy/admin.sock
eend \$?
}
# Remove a native Caddy install left over from earlier versions so it does not
# keep holding the ingress ports.
cleanup_legacy_local_install() {
local init_system
init_system="$(detect_init_system)"
local legacy_unit="/etc/systemd/system/wgui-caddy.service"
local legacy_openrc="/etc/init.d/wgui-caddy"
depend() {
need net
}
EOF
chmod +x "$init_script"
rc-update add "$SERVICE_NAME" default
rc-service "$SERVICE_NAME" restart
if [ -f "$legacy_unit" ]; then
log "removing the legacy wgui-caddy systemd unit ..."
systemctl disable --now wgui-caddy >/dev/null 2>&1 || true
rm -f "$legacy_unit"
systemctl daemon-reload >/dev/null 2>&1 || true
fi
if [ -f "$legacy_openrc" ]; then
log "removing the legacy wgui-caddy init script ..."
rc-service wgui-caddy stop >/dev/null 2>&1 || true
rc-update del wgui-caddy default >/dev/null 2>&1 || true
rm -f "$legacy_openrc"
fi
if [ -d /opt/caddy ]; then
log "removing the legacy Caddy binary from /opt/caddy ..."
rm -rf /opt/caddy
fi
if [ -L /usr/local/bin/caddy ]; then
local target
target="$(readlink /usr/local/bin/caddy || echo '')"
if [ "$target" = "/opt/caddy/caddy" ]; then
rm -f /usr/local/bin/caddy
fi
fi
# The legacy admin socket lived in /run and would otherwise be stale.
rm -rf /run/wgui-caddy
if [ "$init_system" = "unknown" ]; then
log "warning: no supported init system found — the container still starts on boot via docker"
fi
}
install_systemd_timer() {
@@ -341,27 +373,66 @@ EOF
chmod 755 "$script"
}
PURGE_LEGACY=false
PURGE_ALL=false
for arg in "$@"; do
case "$arg" in
--purge-legacy) PURGE_LEGACY=true ;;
--purge) PURGE_ALL=true ;;
*) fail "unknown argument: $arg" ;;
esac
done
require_root
# --purge-legacy: remove a leftover native install (binary, service, socket)
# without touching Docker, the Caddyfile or any certificate.
if [ "$PURGE_LEGACY" = true ]; then
log "purging the legacy native Caddy install ..."
cleanup_legacy_local_install
if [ "$PURGE_ALL" = true ]; then
log "purging the Caddy container, images and renewal job ..."
docker rm -f "$CADDY_CONTAINER_NAME" >/dev/null 2>&1 || true
docker rmi "$CADDY_IMAGE" >/dev/null 2>&1 || true
rm -f /etc/systemd/system/wgui-acme-renew.service \
/etc/systemd/system/wgui-acme-renew.timer \
/etc/periodic/daily/wgui-acme-renew \
/etc/cron.daily/wgui-acme-renew
systemctl daemon-reload >/dev/null 2>&1 || true
log "kept: $CADDY_DIR (Caddyfile + certificates) and $WGUI_LIB_DIR/tls"
fi
log "legacy Caddy cleanup complete."
exit 0
fi
install_deps
install_caddy
require_docker
install_acme_sh
setup_caddy
setup_caddy_dirs
normalize_caddyfile
INIT_SYSTEM="$(detect_init_system)"
case "$INIT_SYSTEM" in
systemd)
install_systemd_unit
install_systemd_timer
;;
openrc)
install_openrc_unit
install_openrc_timer
;;
*)
fail "no supported init system found — start Caddy manually with: $CADDY_BIN run --config $WGUI_LIB_DIR/caddy/Caddyfile --adapter caddyfile"
log "warning: no supported init system found — set up certificate renewal manually"
;;
esac
log "Caddy TLS ingress is installed and running on port $CADDY_HTTPS_PORT."
# Start the container before removing the legacy service so the ingress port is
# never left unbound.
install_container
cleanup_legacy_local_install
log "Caddy TLS ingress container '$CADDY_CONTAINER_NAME' is running on port $CADDY_HTTPS_PORT."
log "Inspect it with: docker logs $CADDY_CONTAINER_NAME"
log "Set the ACME email on each route form; certificate issuance uses manual DNS-01 from the route page."
+2 -7
View File
@@ -21,9 +21,6 @@ BUN_INSTALL_DIR="${BUN_INSTALL_DIR:-/opt/bun}"
BUN_VERSION="${BUN_VERSION:-1.4.3}"
BUN_BIN="/usr/local/bin/bun"
CADDY_VERSION="${CADDY_VERSION:-2.11.4}"
CADDY_BIN_DIR="${CADDY_BIN_DIR:-/opt/caddy}"
CADDY_BIN="$CADDY_BIN_DIR/caddy"
CADDY_USER="${CADDY_USER:-wgui-caddy}"
CADDY_HTTPS_PORT="${CADDY_HTTPS_PORT:-8443}"
CADDY_HTTP_PORT="${CADDY_HTTP_PORT:-8081}"
ACME_HOME="${ACME_HOME:-/opt/acme.sh}"
@@ -132,11 +129,9 @@ install_caddy_stack() {
fail "missing $install_script — cannot install the Caddy TLS ingress"
fi
log "installing/repairing the Caddy TLS ingress ..."
log "installing/repairing the Caddy TLS ingress container ..."
WGUI_LIB_DIR="$WGUI_LIB_DIR" \
CADDY_VERSION="$CADDY_VERSION" \
CADDY_BIN_DIR="$CADDY_BIN_DIR" \
CADDY_USER="$CADDY_USER" \
CADDY_HTTPS_PORT="$CADDY_HTTPS_PORT" \
CADDY_HTTP_PORT="$CADDY_HTTP_PORT" \
ACME_HOME="$ACME_HOME" \
@@ -240,6 +235,6 @@ else
fi
log "open http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo localhost):$PORT in your browser"
if [ "$DEV_MODE" = false ]; then
log "tls ingress: wgui-caddy on https://<hostname>:$CADDY_HTTPS_PORT (never binds 80/443)"
log "tls ingress: wgui-caddy container on https://<hostname>:$CADDY_HTTPS_PORT (never binds 80/443); inspect with: docker logs wgui-caddy"
log "open a route page to issue certificates with manual DNS-01"
fi
+6 -5
View File
@@ -45,9 +45,12 @@ export default function grabDirNames(params?: Params) {
`state.json`,
);
const WGUI_LIB_CADDY_DATA_DIR = path.join(WGUI_LIB_CADDY_DIR, `data`);
const WGUI_LIB_CADDY_ADMIN_SOCKET = `/run/wgui-caddy/admin.sock`;
const WGUI_CADDY_BIN_DIR = `/opt/caddy`;
const WGUI_CADDY_BINARY = path.join(WGUI_CADDY_BIN_DIR, `caddy`);
// Caddy runs in a container that mounts the Caddy dir at the same path, so
// the admin socket lives next to the Caddyfile.
const WGUI_LIB_CADDY_ADMIN_SOCKET = path.join(
WGUI_LIB_CADDY_DIR,
`admin.sock`,
);
const WGUI_ACME_HOME = `/opt/acme.sh`;
const WGUI_ACME_SCRIPT = path.join(WGUI_ACME_HOME, `acme.sh`);
const WGUI_ACME_ENV_FILE = `/etc/wgui/acme.env`;
@@ -87,8 +90,6 @@ export default function grabDirNames(params?: Params) {
WGUI_LIB_CADDY_STATE_FILE,
WGUI_LIB_CADDY_DATA_DIR,
WGUI_LIB_CADDY_ADMIN_SOCKET,
WGUI_CADDY_BIN_DIR,
WGUI_CADDY_BINARY,
WGUI_ACME_HOME,
WGUI_ACME_SCRIPT,
WGUI_ACME_ENV_FILE,