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:
@@ -26,7 +26,7 @@ When building, refactoring, styling, or auditing UI components and layouts, acti
|
|||||||
|
|
||||||
## Entrypoint
|
## 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`).
|
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/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/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/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:
|
- **`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).
|
- 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`.
|
- 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`.
|
||||||
|
|||||||
@@ -49,9 +49,9 @@ The install script will:
|
|||||||
5. Install app dependencies (`bun install`)
|
5. Install app dependencies (`bun install`)
|
||||||
6. Generate `.env` with encryption secrets
|
6. Generate `.env` with encryption secrets
|
||||||
7. Run WireGuard setup (if available)
|
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
|
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
|
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
|
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
|
### TLS for Tunnel Services
|
||||||
|
|
||||||
Client routes expose internal services to WireGuard clients over HTTPS. The
|
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
|
data plane is a dedicated Caddy container (`wgui-caddy`, host networking) on
|
||||||
the route and certificate records and regenerates the Caddyfile. Caddy never
|
`8443`; wg-ui owns the route and certificate records and regenerates the
|
||||||
binds ports 80 or 443, and the host's existing proxy is left alone.
|
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
|
1. If Caddy isn't installed yet, open **Setup → TLS ingress** and click
|
||||||
**Install Caddy** (or use the Caddy action on **Setup → Maintenance**).
|
**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
|
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:
|
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
|
Manual DNS-01 certificates are renewed from the route page because validation
|
||||||
needs your registrar. The route list shows expiry warnings. A successful
|
needs your registrar. The route list shows expiry warnings. A successful
|
||||||
issuance reloads Caddy without restarting wg-ui. Check
|
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)
|
### 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_KEY` | Encryption key for sensitive data | Auto-generated |
|
||||||
| `ENCRYPTION_SALT` | Encryption salt for sensitive data | Auto-generated |
|
| `ENCRYPTION_SALT` | Encryption salt for sensitive data | Auto-generated |
|
||||||
| `DATA_DIR` | Application data directory | `$INSTALL_DIR/.data` |
|
| `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_HTTPS_PORT`| Caddy ingress HTTPS port (never 80/443) | `8443` |
|
||||||
| `CADDY_HTTP_PORT` | Caddy plain HTTP port (never 80) | `8081` |
|
| `CADDY_HTTP_PORT` | Caddy plain HTTP port (never 80) | `8081` |
|
||||||
| `CADDY_INGRESS_ENABLED` | Set to `0` to skip Caddy config sync | `1` |
|
| `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 { useAdminCrudGet } from "@/src/hooks/use-admin-crud-get";
|
||||||
import Select from "../../twui/form/Select";
|
import Select from "../../twui/form/Select";
|
||||||
import adminCrudHandler from "@/src/functions/frontend/admin-crud-handler";
|
import adminCrudHandler from "@/src/functions/frontend/admin-crud-handler";
|
||||||
import EmptyContent from "../../twui/elements/EmptyContent";
|
|
||||||
import twuiNumberfy from "../../twui/utils/numberfy";
|
import twuiNumberfy from "../../twui/utils/numberfy";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -36,7 +35,9 @@ export default function ClientRouteForm({
|
|||||||
const { user, pageProps } = useContext(AppContext);
|
const { user, pageProps } = useContext(AppContext);
|
||||||
|
|
||||||
const [host_id, set_host_id] = useState(
|
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";
|
const is_host_id_present = typeof host_id == "number";
|
||||||
@@ -65,12 +66,6 @@ export default function ClientRouteForm({
|
|||||||
return <LoadingBlock className="h-[420px]" />;
|
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
|
const cancel_href = is_host_id_present
|
||||||
? `/admin/hosts/${host_id}/routes`
|
? `/admin/hosts/${host_id}/routes`
|
||||||
: `/admin/routes`;
|
: `/admin/routes`;
|
||||||
@@ -101,10 +96,14 @@ export default function ClientRouteForm({
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<FormSection title="Host" description="The host for this route">
|
{pageProps?.hosts ? (
|
||||||
|
<FormSection
|
||||||
|
title="Host"
|
||||||
|
description="The host for this route"
|
||||||
|
>
|
||||||
<Select
|
<Select
|
||||||
options={
|
options={
|
||||||
pageProps.hosts.map((h) => ({
|
pageProps?.hosts?.map((h) => ({
|
||||||
value: String(h.id),
|
value: String(h.id),
|
||||||
default: h.id == host_id,
|
default: h.id == host_id,
|
||||||
title: h.name,
|
title: h.name,
|
||||||
@@ -115,6 +114,7 @@ export default function ClientRouteForm({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
) : null}
|
||||||
<FormSection
|
<FormSection
|
||||||
title="Route"
|
title="Route"
|
||||||
description="The public hostname and path Caddy matches."
|
description="The public hostname and path Caddy matches."
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ type Params = {
|
|||||||
key_file: string;
|
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({
|
export default function fixCertificatePermissions({
|
||||||
cert_dir,
|
cert_dir,
|
||||||
cert_file,
|
cert_file,
|
||||||
@@ -18,12 +22,12 @@ export default function fixCertificatePermissions({
|
|||||||
chmodSync(key_file, 0o640);
|
chmodSync(key_file, 0o640);
|
||||||
|
|
||||||
for (const target of [cert_dir, cert_file, key_file]) {
|
for (const target of [cert_dir, cert_file, key_file]) {
|
||||||
execFileSync("chown", ["root:wgui-caddy", target], {
|
execFileSync("chown", ["root:root", target], {
|
||||||
stdio: "pipe",
|
stdio: "pipe",
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} 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 BunSQLite from "@moduletrace/bun-sqlite";
|
||||||
import grabDirNames from "@/src/utils/grab-dir-names";
|
import grabDirNames from "@/src/utils/grab-dir-names";
|
||||||
import type { TableType } from "@/src/types";
|
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 grabCertDirNames from "../caddy/grab-cert-dir-names";
|
||||||
|
import { grabCaddyReloadCommandString } from "../caddy/caddy-container-commands";
|
||||||
import syncCaddyIngress from "../caddy/sync-caddy-ingress";
|
import syncCaddyIngress from "../caddy/sync-caddy-ingress";
|
||||||
import validateRouteHostname from "../caddy/validate-route-hostname";
|
import validateRouteHostname from "../caddy/validate-route-hostname";
|
||||||
import grabCertificateByHostname from "./grab-certificate-by-hostname";
|
import grabCertificateByHostname from "./grab-certificate-by-hostname";
|
||||||
@@ -46,13 +46,13 @@ function fixPermissions({ cert_dir, cert_file, key_file }: {
|
|||||||
chmodSync(key_file, 0o640);
|
chmodSync(key_file, 0o640);
|
||||||
|
|
||||||
for (const target of [cert_dir, cert_file, key_file]) {
|
for (const target of [cert_dir, cert_file, key_file]) {
|
||||||
execFileSync("chown", ["root:wgui-caddy", target], {
|
execFileSync("chown", ["root:root", target], {
|
||||||
stdio: "pipe",
|
stdio: "pipe",
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} 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 });
|
mkdirSync(CERT_DIR, { recursive: true, mode: 0o750 });
|
||||||
|
|
||||||
const setup_status = grabSystemSetupStatus();
|
const reload_command = grabCaddyReloadCommandString();
|
||||||
const reload_command =
|
|
||||||
setup_status.init_system == "openrc"
|
|
||||||
? `rc-service wgui-caddy reload`
|
|
||||||
: `systemctl reload wgui-caddy`;
|
|
||||||
|
|
||||||
const child_env = {
|
const child_env = {
|
||||||
...process.env,
|
...process.env,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import buildCaddyfile from "./build-caddyfile";
|
|||||||
const base = {
|
const base = {
|
||||||
https_port: 8443,
|
https_port: 8443,
|
||||||
http_port: 8081,
|
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`,
|
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 { existsSync, readFileSync } from "node:fs";
|
||||||
import grabDirNames from "@/src/utils/grab-dir-names";
|
import grabDirNames from "@/src/utils/grab-dir-names";
|
||||||
import { TlsData } from "@/src/data/tls-data";
|
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 {
|
const {
|
||||||
WGUI_CADDY_BINARY,
|
|
||||||
WGUI_LIB_CADDY_CONFIG_FILE,
|
WGUI_LIB_CADDY_CONFIG_FILE,
|
||||||
WGUI_LIB_CADDY_STATE_FILE,
|
WGUI_LIB_CADDY_STATE_FILE,
|
||||||
WGUI_ACME_SCRIPT,
|
WGUI_ACME_SCRIPT,
|
||||||
@@ -30,9 +35,9 @@ function readState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function version() {
|
function docker({ args }: { args: string[] }) {
|
||||||
try {
|
try {
|
||||||
return execFileSync(WGUI_CADDY_BINARY, ["version"], {
|
return execFileSync("docker", args, {
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
stdio: "pipe",
|
stdio: "pipe",
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
@@ -42,38 +47,28 @@ function version() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function serviceActive() {
|
function version() {
|
||||||
const setup_status = grabSystemSetupStatus();
|
const running = docker({ args: grabCaddyExecVersionArgs() });
|
||||||
|
|
||||||
try {
|
if (running) {
|
||||||
if (setup_status.init_system == "systemd") {
|
return running;
|
||||||
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 (!caddyImageInstalled()) {
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CADDY_IMAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function serviceActive() {
|
||||||
|
const running = docker({ args: grabCaddyInspectArgs() });
|
||||||
|
|
||||||
|
if (running == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return running == "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
type Settings = {
|
type Settings = {
|
||||||
@@ -85,7 +80,10 @@ export default function grabCaddyStatus(settings?: Settings) {
|
|||||||
const state = readState();
|
const state = readState();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
installed: existsSync(WGUI_CADDY_BINARY),
|
runtime: "docker" as const,
|
||||||
|
container_name: CADDY_CONTAINER_NAME,
|
||||||
|
image: CADDY_IMAGE,
|
||||||
|
installed: caddyImageInstalled(),
|
||||||
service_active: serviceActive(),
|
service_active: serviceActive(),
|
||||||
acme_installed: existsSync(WGUI_ACME_SCRIPT),
|
acme_installed: existsSync(WGUI_ACME_SCRIPT),
|
||||||
acme_env_exists: existsSync(WGUI_ACME_ENV_FILE),
|
acme_env_exists: existsSync(WGUI_ACME_ENV_FILE),
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { execFileSync } from "node:child_process";
|
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({ args }: { args: string[] }) {
|
||||||
|
|
||||||
function run({ command, args }: { command: string; args: string[] }) {
|
|
||||||
try {
|
try {
|
||||||
const output = execFileSync(command, args, {
|
const output = execFileSync("docker", args, {
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
stdio: "pipe",
|
stdio: "pipe",
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
@@ -29,58 +30,24 @@ function run({ command, args }: { command: string; args: string[] }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function caddyServiceIsActive({
|
/**
|
||||||
init_system,
|
* 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
|
||||||
init_system: ReturnType<typeof grabSystemSetupStatus>["init_system"];
|
* so a fresh container picks up the new Caddyfile.
|
||||||
}) {
|
*/
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function reloadCaddyService() {
|
export default function reloadCaddyService() {
|
||||||
const setup_status = grabSystemSetupStatus();
|
const reload = run({ args: grabCaddyReloadArgs() });
|
||||||
const service_active = caddyServiceIsActive({
|
|
||||||
init_system: setup_status.init_system,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (setup_status.is_dev && !service_active) {
|
if (reload.success) {
|
||||||
return {
|
return reload;
|
||||||
|
}
|
||||||
|
|
||||||
|
const restart = run({ args: grabCaddyRestartArgs() });
|
||||||
|
|
||||||
|
return restart.success
|
||||||
|
? {
|
||||||
success: true as const,
|
success: true as const,
|
||||||
msg: `Development mode — Caddy service is not running, reload skipped`,
|
msg: `Caddy container restarted`,
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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`,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
: reload;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import type { TableType } from "@/src/types";
|
|||||||
import grabDirNames from "@/src/utils/grab-dir-names";
|
import grabDirNames from "@/src/utils/grab-dir-names";
|
||||||
import { TlsData } from "@/src/data/tls-data";
|
import { TlsData } from "@/src/data/tls-data";
|
||||||
import buildCaddyfile from "./build-caddyfile";
|
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 reloadCaddyService from "./reload-caddy-service";
|
||||||
import applyCaddyIngressFirewallRule from "./apply-caddy-ingress-firewall-rule";
|
import applyCaddyIngressFirewallRule from "./apply-caddy-ingress-firewall-rule";
|
||||||
import grabCaddySettings from "./grab-caddy-settings";
|
import grabCaddySettings from "./grab-caddy-settings";
|
||||||
@@ -33,7 +35,6 @@ type Params = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
WGUI_CADDY_BINARY,
|
|
||||||
WGUI_LIB_CADDY_DIR,
|
WGUI_LIB_CADDY_DIR,
|
||||||
WGUI_LIB_CADDY_CONFIG_FILE,
|
WGUI_LIB_CADDY_CONFIG_FILE,
|
||||||
WGUI_LIB_CADDY_STATE_FILE,
|
WGUI_LIB_CADDY_STATE_FILE,
|
||||||
@@ -155,29 +156,23 @@ async function doSync({ reason }: Params): Promise<CaddySyncResult> {
|
|||||||
|
|
||||||
writeFileSync(tmp_path, built.config, { mode: 0o644 });
|
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 {
|
try {
|
||||||
execFileSync(
|
execFileSync(
|
||||||
WGUI_CADDY_BINARY,
|
"docker",
|
||||||
[
|
grabCaddyValidateArgs({ config_path: tmp_path }),
|
||||||
"validate",
|
|
||||||
"--config",
|
|
||||||
tmp_path,
|
|
||||||
"--adapter",
|
|
||||||
"caddyfile",
|
|
||||||
],
|
|
||||||
{
|
{
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
stdio: "pipe",
|
stdio: "pipe",
|
||||||
timeout: 30000,
|
timeout: 60000,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
rmSync(tmp_path, { force: true });
|
rmSync(tmp_path, { force: true });
|
||||||
const msg = `Caddy rejected the generated config: ${
|
const msg = `Caddy rejected the generated config: ${
|
||||||
error?.stderr
|
error?.stderr ? String(error.stderr).trim() : error.message
|
||||||
? String(error.stderr).trim()
|
|
||||||
: error.message
|
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
writeState({ warnings: built.warnings, last_error: msg });
|
writeState({ warnings: built.warnings, last_error: msg });
|
||||||
@@ -188,6 +183,10 @@ async function doSync({ reason }: Params): Promise<CaddySyncResult> {
|
|||||||
warnings: built.warnings,
|
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)) {
|
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);
|
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];
|
const warnings = [...built.warnings];
|
||||||
|
|
||||||
if (!reload.success) {
|
if (!reload.success) {
|
||||||
|
|||||||
@@ -85,9 +85,9 @@ const TOOL_CHECKS: {
|
|||||||
version_command: "bun --version",
|
version_command: "bun --version",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "caddy",
|
name: "caddy (docker)",
|
||||||
command: "/opt/caddy/caddy",
|
command: "docker",
|
||||||
version_command: "/opt/caddy/caddy version",
|
version_command: "docker --version",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -22,10 +22,7 @@ export default function AddClientRoutePage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<ClientRouteForm
|
<ClientRouteForm host_id={host_id} />
|
||||||
host_id={host_id}
|
|
||||||
cancel_href={`/admin/hosts/${host_id}/routes`}
|
|
||||||
/>
|
|
||||||
</PageBody>
|
</PageBody>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export default function EditClientRoutePage() {
|
|||||||
<ClientRouteForm
|
<ClientRouteForm
|
||||||
host_id={host_id}
|
host_id={host_id}
|
||||||
existing_route={route}
|
existing_route={route}
|
||||||
cancel_href={`/admin/hosts/${host_id}/routes`}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ export default function SetupCaddyAction({ installed, on_done }: Props) {
|
|||||||
return (
|
return (
|
||||||
<SetupActionRow
|
<SetupActionRow
|
||||||
title="Caddy TLS ingress"
|
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} />}
|
icon={<ServerCog size={15} />}
|
||||||
button_title={installed ? "Repair Caddy" : "Install Caddy"}
|
button_title={installed ? "Repair Caddy" : "Install Caddy"}
|
||||||
confirm_params={{
|
confirm_params={{
|
||||||
title: installed ? "Repair the Caddy ingress?" : "Install Caddy?",
|
title: installed ? "Repair the Caddy ingress?" : "Install Caddy?",
|
||||||
message:
|
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",
|
confirm_label: installed ? "Repair Caddy" : "Install Caddy",
|
||||||
}}
|
}}
|
||||||
on_run={async () => {
|
on_run={async () => {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function SetupActionsSection({ setup }: Props) {
|
|||||||
.every((tool) => tool.installed);
|
.every((tool) => tool.installed);
|
||||||
|
|
||||||
const have_caddy = Boolean(
|
const have_caddy = Boolean(
|
||||||
setup_status.tools.find((tool) => tool.command == "/opt/caddy/caddy")
|
setup_status.tools.find((tool) => tool.name.startsWith("caddy"))
|
||||||
?.installed,
|
?.installed,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default function TlsStatusSection({ tls }: Props) {
|
|||||||
return (
|
return (
|
||||||
<Panel
|
<Panel
|
||||||
title="Ingress"
|
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={
|
actions={
|
||||||
<>
|
<>
|
||||||
<StatusDot
|
<StatusDot
|
||||||
@@ -33,7 +33,7 @@ export default function TlsStatusSection({ tls }: Props) {
|
|||||||
}
|
}
|
||||||
label={
|
label={
|
||||||
!caddy?.installed
|
!caddy?.installed
|
||||||
? "Caddy not installed"
|
? "Caddy image missing"
|
||||||
: caddy.service_active === false
|
: caddy.service_active === false
|
||||||
? "wgui-caddy stopped"
|
? "wgui-caddy stopped"
|
||||||
: caddy.version || "Caddy installed"
|
: caddy.version || "Caddy installed"
|
||||||
@@ -70,7 +70,7 @@ export default function TlsStatusSection({ tls }: Props) {
|
|||||||
{!caddy?.installed ? (
|
{!caddy?.installed ? (
|
||||||
<InlineAlert
|
<InlineAlert
|
||||||
tone="warning"
|
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}
|
) : null}
|
||||||
{caddy?.state?.last_error ? (
|
{caddy?.state?.last_error ? (
|
||||||
@@ -94,6 +94,14 @@ export default function TlsStatusSection({ tls }: Props) {
|
|||||||
? `${caddy.https_port}/tcp`
|
? `${caddy.https_port}/tcp`
|
||||||
: "—",
|
: "—",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "runtime",
|
||||||
|
label: "Runtime",
|
||||||
|
value: caddy
|
||||||
|
? `docker · ${caddy.container_name} · ${caddy.image}`
|
||||||
|
: "—",
|
||||||
|
mono: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "config",
|
key: "config",
|
||||||
label: "Caddyfile",
|
label: "Caddyfile",
|
||||||
|
|||||||
@@ -11,12 +11,7 @@ import type {
|
|||||||
const INSTALL_CADDY_SCRIPT = `install-caddy.sh`;
|
const INSTALL_CADDY_SCRIPT = `install-caddy.sh`;
|
||||||
const INSTALL_CADDY_TIMEOUT_MS = 20 * 60 * 1000;
|
const INSTALL_CADDY_TIMEOUT_MS = 20 * 60 * 1000;
|
||||||
|
|
||||||
const {
|
const { WGUI_LIB_DIR, WGUI_ACME_HOME, WGUI_ACME_ENV_FILE } = grabDirNames();
|
||||||
WGUI_LIB_DIR,
|
|
||||||
WGUI_CADDY_BIN_DIR,
|
|
||||||
WGUI_ACME_HOME,
|
|
||||||
WGUI_ACME_ENV_FILE,
|
|
||||||
} = grabDirNames();
|
|
||||||
|
|
||||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
||||||
req,
|
req,
|
||||||
@@ -37,7 +32,6 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
|||||||
env: {
|
env: {
|
||||||
WGUI_LIB_DIR,
|
WGUI_LIB_DIR,
|
||||||
CADDY_VERSION: process.env.CADDY_VERSION,
|
CADDY_VERSION: process.env.CADDY_VERSION,
|
||||||
CADDY_BIN_DIR: WGUI_CADDY_BIN_DIR,
|
|
||||||
CADDY_HTTPS_PORT: String(TlsData.CaddyHttpsPort),
|
CADDY_HTTPS_PORT: String(TlsData.CaddyHttpsPort),
|
||||||
CADDY_HTTP_PORT: String(TlsData.CaddyHttpPort),
|
CADDY_HTTP_PORT: String(TlsData.CaddyHttpPort),
|
||||||
ACME_HOME: WGUI_ACME_HOME,
|
ACME_HOME: WGUI_ACME_HOME,
|
||||||
|
|||||||
+210
-139
@@ -1,6 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Install or repair the Caddy TLS ingress used by client routes.
|
# 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.
|
# Safe to re-run: it preserves the existing Caddyfile and certificate data.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -10,14 +11,15 @@ fail() { echo "error: $*" >&2; exit 1; }
|
|||||||
|
|
||||||
WGUI_LIB_DIR="${WGUI_LIB_DIR:-/var/lib/wgui}"
|
WGUI_LIB_DIR="${WGUI_LIB_DIR:-/var/lib/wgui}"
|
||||||
CADDY_VERSION="${CADDY_VERSION:-2.11.4}"
|
CADDY_VERSION="${CADDY_VERSION:-2.11.4}"
|
||||||
CADDY_BIN_DIR="${CADDY_BIN_DIR:-/opt/caddy}"
|
CADDY_IMAGE="caddy:${CADDY_VERSION}-alpine"
|
||||||
CADDY_BIN="$CADDY_BIN_DIR/caddy"
|
CADDY_CONTAINER_NAME="wgui-caddy"
|
||||||
CADDY_USER="${CADDY_USER:-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_HTTPS_PORT="${CADDY_HTTPS_PORT:-8443}"
|
||||||
CADDY_HTTP_PORT="${CADDY_HTTP_PORT:-8081}"
|
CADDY_HTTP_PORT="${CADDY_HTTP_PORT:-8081}"
|
||||||
ACME_HOME="${ACME_HOME:-/opt/acme.sh}"
|
ACME_HOME="${ACME_HOME:-/opt/acme.sh}"
|
||||||
ACME_ENV_FILE="${ACME_ENV_FILE:-/etc/wgui/acme.env}"
|
ACME_ENV_FILE="${ACME_ENV_FILE:-/etc/wgui/acme.env}"
|
||||||
SERVICE_NAME="wgui-caddy"
|
|
||||||
|
|
||||||
require_root() {
|
require_root() {
|
||||||
if [ "$(id -u)" -ne 0 ]; then
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
@@ -45,19 +47,32 @@ detect_init_system() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
caddy_arch() {
|
docker_install_hint() {
|
||||||
case "$(uname -m)" in
|
local distro
|
||||||
x86_64 | amd64) echo "amd64" ;;
|
distro="$(detect_distro)"
|
||||||
aarch64 | arm64 | armv8l | armv8) echo "arm64" ;;
|
|
||||||
armv7l | armv7) echo "armv7" ;;
|
case "$distro" in
|
||||||
armv6l | armv6) echo "armv6" ;;
|
debian | ubuntu | linuxmint | raspbian)
|
||||||
*) fail "unsupported architecture for Caddy: $(uname -m)" ;;
|
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
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
install_deps() {
|
install_deps() {
|
||||||
local missing=false
|
|
||||||
local tool
|
local tool
|
||||||
|
local missing=false
|
||||||
|
|
||||||
for tool in curl tar git openssl; do
|
for tool in curl tar git openssl; do
|
||||||
if ! command -v "$tool" >/dev/null 2>&1; then
|
if ! command -v "$tool" >/dev/null 2>&1; then
|
||||||
@@ -97,50 +112,36 @@ install_deps() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
install_caddy() {
|
require_docker() {
|
||||||
if [ -x "$CADDY_BIN" ]; then
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
if [ ! -e /usr/local/bin/caddy ]; then
|
fail "docker is required for the Caddy ingress — install it with: $(docker_install_hint)"
|
||||||
ln -sf "$CADDY_BIN" /usr/local/bin/caddy
|
|
||||||
fi
|
|
||||||
log "caddy: $($CADDY_BIN version)"
|
|
||||||
return
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local arch
|
local init_system
|
||||||
arch="$(caddy_arch)"
|
init_system="$(detect_init_system)"
|
||||||
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)"
|
|
||||||
|
|
||||||
log "installing Caddy ${CADDY_VERSION} (${arch}) to ${CADDY_BIN_DIR} ..."
|
if ! docker info >/dev/null 2>&1; then
|
||||||
mkdir -p "$CADDY_BIN_DIR"
|
log "starting the docker daemon ..."
|
||||||
|
|
||||||
curl -fsSL -o "$tmp_dir/$archive" "$base_url/$archive"
|
case "$init_system" in
|
||||||
curl -fsSL -o "$tmp_dir/checksums.txt" "$base_url/caddy_${CADDY_VERSION}_checksums.txt"
|
systemd)
|
||||||
|
systemctl enable --now docker
|
||||||
if command -v sha512sum >/dev/null 2>&1; then
|
;;
|
||||||
(cd "$tmp_dir" && grep " $archive\$" checksums.txt | sha512sum -c -)
|
openrc)
|
||||||
elif command -v openssl >/dev/null 2>&1; then
|
rc-update add docker default
|
||||||
local expected actual
|
rc-service docker start
|
||||||
expected="$(grep " $archive\$" "$tmp_dir/checksums.txt" | awk '{print $1}')"
|
;;
|
||||||
actual="$(openssl dgst -sha512 "$tmp_dir/$archive" | awk '{print $NF}')"
|
*)
|
||||||
|
fail "docker is installed but not running — start it manually and re-run this script"
|
||||||
if [ -z "$expected" ] || [ "$expected" != "$actual" ]; then
|
;;
|
||||||
fail "Caddy checksum verification failed for $archive"
|
esac
|
||||||
fi
|
|
||||||
else
|
|
||||||
fail "no sha512 verification tool available (install coreutils or openssl)"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
tar -xzf "$tmp_dir/$archive" -C "$CADDY_BIN_DIR" caddy
|
if ! docker info >/dev/null 2>&1; then
|
||||||
chmod 755 "$CADDY_BIN"
|
fail "docker is not running — start it and re-run this script"
|
||||||
if [ ! -e /usr/local/bin/caddy ]; then
|
|
||||||
ln -sf "$CADDY_BIN" /usr/local/bin/caddy
|
|
||||||
fi
|
fi
|
||||||
rm -rf "$tmp_dir"
|
|
||||||
|
|
||||||
log "caddy: $($CADDY_BIN version)"
|
log "docker: $(docker --version)"
|
||||||
}
|
}
|
||||||
|
|
||||||
install_acme_sh() {
|
install_acme_sh() {
|
||||||
@@ -156,29 +157,15 @@ install_acme_sh() {
|
|||||||
chmod 700 "$ACME_HOME/acme.sh"
|
chmod 700 "$ACME_HOME/acme.sh"
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_caddy() {
|
setup_caddy_dirs() {
|
||||||
if ! id -u "$CADDY_USER" >/dev/null 2>&1; then
|
# The official Caddy image runs as root, so the mounted state directory is
|
||||||
log "creating service user $CADDY_USER ..."
|
# root-owned; nothing else on the host needs access to it.
|
||||||
if command -v useradd >/dev/null 2>&1; then
|
mkdir -p "$CADDY_DIR/data" "$WGUI_LIB_DIR/tls" /etc/wgui
|
||||||
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"
|
|
||||||
chmod 755 "$WGUI_LIB_DIR"
|
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"
|
chmod 750 "$WGUI_LIB_DIR/tls"
|
||||||
chown -R "$CADDY_USER:$CADDY_USER" "$WGUI_LIB_DIR/caddy"
|
chown -R root:root "$CADDY_DIR" "$WGUI_LIB_DIR/tls"
|
||||||
chmod 750 "$WGUI_LIB_DIR/caddy"
|
|
||||||
chmod 750 "$WGUI_LIB_DIR/caddy/data"
|
|
||||||
|
|
||||||
mkdir -p /etc/wgui
|
|
||||||
chmod 700 /etc/wgui
|
chmod 700 /etc/wgui
|
||||||
|
|
||||||
if [ ! -f "$ACME_ENV_FILE" ]; then
|
if [ ! -f "$ACME_ENV_FILE" ]; then
|
||||||
@@ -194,89 +181,134 @@ EOF
|
|||||||
chown root:root "$ACME_ENV_FILE"
|
chown root:root "$ACME_ENV_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f "$WGUI_LIB_DIR/caddy/Caddyfile" ]; then
|
if [ ! -f "$CADDY_CONFIG_FILE" ]; then
|
||||||
log "writing initial Caddyfile ..."
|
log "writing initial Caddyfile ..."
|
||||||
cat > "$WGUI_LIB_DIR/caddy/Caddyfile" <<EOF
|
cat > "$CADDY_CONFIG_FILE" <<EOF
|
||||||
{
|
{
|
||||||
https_port $CADDY_HTTPS_PORT
|
https_port $CADDY_HTTPS_PORT
|
||||||
http_port $CADDY_HTTP_PORT
|
http_port $CADDY_HTTP_PORT
|
||||||
auto_https disable_redirects
|
auto_https disable_redirects
|
||||||
admin unix//run/wgui-caddy/admin.sock
|
admin unix/$CADDY_ADMIN_SOCKET
|
||||||
storage file_system $WGUI_LIB_DIR/caddy/data
|
storage file_system $CADDY_DIR/data
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
chown "$CADDY_USER:$CADDY_USER" "$WGUI_LIB_DIR/caddy/Caddyfile"
|
chmod 640 "$CADDY_CONFIG_FILE"
|
||||||
chmod 640 "$WGUI_LIB_DIR/caddy/Caddyfile"
|
chown root:root "$CADDY_CONFIG_FILE"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
install_systemd_unit() {
|
# Older installs used an admin socket under /run, which does not exist inside
|
||||||
local unit="/etc/systemd/system/$SERVICE_NAME.service"
|
# the container. Point an existing Caddyfile at the mounted socket path.
|
||||||
log "writing systemd unit $unit ..."
|
normalize_caddyfile() {
|
||||||
cat > "$unit" <<EOF
|
[ -f "$CADDY_CONFIG_FILE" ] || return
|
||||||
[Unit]
|
|
||||||
Description=wg-ui TLS ingress (Caddy)
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
if grep -qx " admin unix/$CADDY_ADMIN_SOCKET" "$CADDY_CONFIG_FILE"; then
|
||||||
Type=simple
|
return
|
||||||
User=$CADDY_USER
|
fi
|
||||||
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
|
|
||||||
|
|
||||||
[Install]
|
log "updating the admin socket path in the existing Caddyfile ..."
|
||||||
WantedBy=multi-user.target
|
local tmp
|
||||||
EOF
|
tmp="$(mktemp)"
|
||||||
|
|
||||||
systemctl daemon-reload
|
awk -v admin=" admin unix/$CADDY_ADMIN_SOCKET" '
|
||||||
systemctl enable "$SERVICE_NAME.service"
|
BEGIN { inserted = 0 }
|
||||||
systemctl restart "$SERVICE_NAME.service"
|
/^[[: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() {
|
install_container() {
|
||||||
local init_script="/etc/init.d/$SERVICE_NAME"
|
log "pulling $CADDY_IMAGE ..."
|
||||||
log "writing openrc init script $init_script ..."
|
docker pull "$CADDY_IMAGE"
|
||||||
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"
|
|
||||||
|
|
||||||
start_pre() {
|
if docker inspect "$CADDY_CONTAINER_NAME" >/dev/null 2>&1; then
|
||||||
mkdir -p /run/wgui-caddy
|
log "removing the existing $CADDY_CONTAINER_NAME container ..."
|
||||||
chown "$CADDY_USER:$CADDY_USER" /run/wgui-caddy
|
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() {
|
# Remove a native Caddy install left over from earlier versions so it does not
|
||||||
ebegin "Reloading Caddy"
|
# keep holding the ingress ports.
|
||||||
"$CADDY_BIN" reload --config "$WGUI_LIB_DIR/caddy/Caddyfile" --adapter caddyfile --address unix//run/wgui-caddy/admin.sock
|
cleanup_legacy_local_install() {
|
||||||
eend \$?
|
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() {
|
if [ -f "$legacy_unit" ]; then
|
||||||
need net
|
log "removing the legacy wgui-caddy systemd unit ..."
|
||||||
}
|
systemctl disable --now wgui-caddy >/dev/null 2>&1 || true
|
||||||
EOF
|
rm -f "$legacy_unit"
|
||||||
chmod +x "$init_script"
|
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||||
rc-update add "$SERVICE_NAME" default
|
fi
|
||||||
rc-service "$SERVICE_NAME" restart
|
|
||||||
|
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() {
|
install_systemd_timer() {
|
||||||
@@ -341,27 +373,66 @@ EOF
|
|||||||
chmod 755 "$script"
|
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
|
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_deps
|
||||||
install_caddy
|
require_docker
|
||||||
install_acme_sh
|
install_acme_sh
|
||||||
setup_caddy
|
setup_caddy_dirs
|
||||||
|
normalize_caddyfile
|
||||||
|
|
||||||
INIT_SYSTEM="$(detect_init_system)"
|
INIT_SYSTEM="$(detect_init_system)"
|
||||||
|
|
||||||
case "$INIT_SYSTEM" in
|
case "$INIT_SYSTEM" in
|
||||||
systemd)
|
systemd)
|
||||||
install_systemd_unit
|
|
||||||
install_systemd_timer
|
install_systemd_timer
|
||||||
;;
|
;;
|
||||||
openrc)
|
openrc)
|
||||||
install_openrc_unit
|
|
||||||
install_openrc_timer
|
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
|
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."
|
log "Set the ACME email on each route form; certificate issuance uses manual DNS-01 from the route page."
|
||||||
|
|||||||
@@ -21,9 +21,6 @@ BUN_INSTALL_DIR="${BUN_INSTALL_DIR:-/opt/bun}"
|
|||||||
BUN_VERSION="${BUN_VERSION:-1.4.3}"
|
BUN_VERSION="${BUN_VERSION:-1.4.3}"
|
||||||
BUN_BIN="/usr/local/bin/bun"
|
BUN_BIN="/usr/local/bin/bun"
|
||||||
CADDY_VERSION="${CADDY_VERSION:-2.11.4}"
|
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_HTTPS_PORT="${CADDY_HTTPS_PORT:-8443}"
|
||||||
CADDY_HTTP_PORT="${CADDY_HTTP_PORT:-8081}"
|
CADDY_HTTP_PORT="${CADDY_HTTP_PORT:-8081}"
|
||||||
ACME_HOME="${ACME_HOME:-/opt/acme.sh}"
|
ACME_HOME="${ACME_HOME:-/opt/acme.sh}"
|
||||||
@@ -132,11 +129,9 @@ install_caddy_stack() {
|
|||||||
fail "missing $install_script — cannot install the Caddy TLS ingress"
|
fail "missing $install_script — cannot install the Caddy TLS ingress"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "installing/repairing the Caddy TLS ingress ..."
|
log "installing/repairing the Caddy TLS ingress container ..."
|
||||||
WGUI_LIB_DIR="$WGUI_LIB_DIR" \
|
WGUI_LIB_DIR="$WGUI_LIB_DIR" \
|
||||||
CADDY_VERSION="$CADDY_VERSION" \
|
CADDY_VERSION="$CADDY_VERSION" \
|
||||||
CADDY_BIN_DIR="$CADDY_BIN_DIR" \
|
|
||||||
CADDY_USER="$CADDY_USER" \
|
|
||||||
CADDY_HTTPS_PORT="$CADDY_HTTPS_PORT" \
|
CADDY_HTTPS_PORT="$CADDY_HTTPS_PORT" \
|
||||||
CADDY_HTTP_PORT="$CADDY_HTTP_PORT" \
|
CADDY_HTTP_PORT="$CADDY_HTTP_PORT" \
|
||||||
ACME_HOME="$ACME_HOME" \
|
ACME_HOME="$ACME_HOME" \
|
||||||
@@ -240,6 +235,6 @@ else
|
|||||||
fi
|
fi
|
||||||
log "open http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo localhost):$PORT in your browser"
|
log "open http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo localhost):$PORT in your browser"
|
||||||
if [ "$DEV_MODE" = false ]; then
|
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"
|
log "open a route page to issue certificates with manual DNS-01"
|
||||||
fi
|
fi
|
||||||
@@ -45,9 +45,12 @@ export default function grabDirNames(params?: Params) {
|
|||||||
`state.json`,
|
`state.json`,
|
||||||
);
|
);
|
||||||
const WGUI_LIB_CADDY_DATA_DIR = path.join(WGUI_LIB_CADDY_DIR, `data`);
|
const WGUI_LIB_CADDY_DATA_DIR = path.join(WGUI_LIB_CADDY_DIR, `data`);
|
||||||
const WGUI_LIB_CADDY_ADMIN_SOCKET = `/run/wgui-caddy/admin.sock`;
|
// Caddy runs in a container that mounts the Caddy dir at the same path, so
|
||||||
const WGUI_CADDY_BIN_DIR = `/opt/caddy`;
|
// the admin socket lives next to the Caddyfile.
|
||||||
const WGUI_CADDY_BINARY = path.join(WGUI_CADDY_BIN_DIR, `caddy`);
|
const WGUI_LIB_CADDY_ADMIN_SOCKET = path.join(
|
||||||
|
WGUI_LIB_CADDY_DIR,
|
||||||
|
`admin.sock`,
|
||||||
|
);
|
||||||
const WGUI_ACME_HOME = `/opt/acme.sh`;
|
const WGUI_ACME_HOME = `/opt/acme.sh`;
|
||||||
const WGUI_ACME_SCRIPT = path.join(WGUI_ACME_HOME, `acme.sh`);
|
const WGUI_ACME_SCRIPT = path.join(WGUI_ACME_HOME, `acme.sh`);
|
||||||
const WGUI_ACME_ENV_FILE = `/etc/wgui/acme.env`;
|
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_STATE_FILE,
|
||||||
WGUI_LIB_CADDY_DATA_DIR,
|
WGUI_LIB_CADDY_DATA_DIR,
|
||||||
WGUI_LIB_CADDY_ADMIN_SOCKET,
|
WGUI_LIB_CADDY_ADMIN_SOCKET,
|
||||||
WGUI_CADDY_BIN_DIR,
|
|
||||||
WGUI_CADDY_BINARY,
|
|
||||||
WGUI_ACME_HOME,
|
WGUI_ACME_HOME,
|
||||||
WGUI_ACME_SCRIPT,
|
WGUI_ACME_SCRIPT,
|
||||||
WGUI_ACME_ENV_FILE,
|
WGUI_ACME_ENV_FILE,
|
||||||
|
|||||||
Reference in New Issue
Block a user