Skip to content

Commit a8cde34

Browse files
committed
fix(deploy): sudo caddy writes and use per-app pm2 namespace in health check
- CaddyService wrote /etc/caddy/conf.d/<app>.caddy with a bare redirect and reloaded caddy without sudo. Worked when the SSH user was root but Permission denied under the deploy user setup now bootstraps. Wrap the write via \`| sudo tee\` and the reload with \`sudo systemctl\`. - HealthCheckService.performPm2StatusCheck built pm2 names from the workspace-level deploymentName (always apps[0]'s namespace), so in a multi-app workspace it looked for e.g. \`biormin-biormin-frontend\` when pm2 actually runs \`biormin-frontend\` under its own namespace. Take the namespace from the app being checked instead. Health check now correctly matches every app's processes.
1 parent 4830e1c commit a8cde34

2 files changed

Lines changed: 26 additions & 15 deletions

File tree

src/services/caddy.service.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ export class CaddyService {
2828
const caddyConfig = this.generateBackendCaddyfile(app, webApp.port!);
2929

3030
const escaped = caddyConfig.replace(/'/g, "'\"'\"'");
31-
await this.executor.execOrThrow(`echo '${escaped}' > /etc/caddy/conf.d/${app.name}.caddy`);
31+
await this.executor.execOrThrow(
32+
`SUDO=""; [ "$EUID" -ne 0 ] && SUDO="sudo"; ` +
33+
`echo '${escaped}' | $SUDO tee /etc/caddy/conf.d/${app.name}.caddy > /dev/null`,
34+
);
3235
}
3336

3437
async configureFrontend(app: ShipnodeApp): Promise<void> {
@@ -38,11 +41,16 @@ export class CaddyService {
3841
const caddyConfig = this.generateFrontendCaddyfile(app, servePath);
3942

4043
const escaped = caddyConfig.replace(/'/g, "'\"'\"'");
41-
await this.executor.execOrThrow(`echo '${escaped}' > /etc/caddy/conf.d/${app.name}.caddy`);
44+
await this.executor.execOrThrow(
45+
`SUDO=""; [ "$EUID" -ne 0 ] && SUDO="sudo"; ` +
46+
`echo '${escaped}' | $SUDO tee /etc/caddy/conf.d/${app.name}.caddy > /dev/null`,
47+
);
4248
}
4349

4450
async reload(): Promise<void> {
45-
await this.executor.execOrThrow('systemctl reload caddy');
51+
await this.executor.execOrThrow(
52+
`SUDO=""; [ "$EUID" -ne 0 ] && SUDO="sudo"; $SUDO systemctl reload caddy`,
53+
);
4654
}
4755

4856
private generateBackendCaddyfile(app: ShipnodeApp, port: number): string {

src/services/health.service.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ShipnodeConfig, ShipnodeApp, Pm2App } from '../shared/types.js';
22
import type { RemoteExecutor } from '../domain/remote/executor.js';
33
import { HealthCheckError } from '../shared/errors.js';
4-
import { getDeploymentName, getPm2Name } from '../domain/pm2/apps.js';
4+
import { getPm2Name } from '../domain/pm2/apps.js';
55

66
interface Pm2JlistEntry {
77
name: string;
@@ -14,7 +14,7 @@ interface Pm2JlistEntry {
1414
export class HealthCheckService {
1515
constructor(
1616
private executor: RemoteExecutor,
17-
private config: ShipnodeConfig,
17+
_config: ShipnodeConfig,
1818
) {}
1919

2020
async perform(app: ShipnodeApp): Promise<{ attempts: number; responseMs: number }> {
@@ -37,7 +37,7 @@ export class HealthCheckService {
3737
}
3838

3939
if (app.pm2?.apps.length) {
40-
await this.performPm2StatusCheck(app.pm2.apps);
40+
await this.performPm2StatusCheck(app);
4141
}
4242

4343
return { attempts, responseMs };
@@ -79,7 +79,7 @@ export class HealthCheckService {
7979
);
8080
}
8181

82-
private async performPm2StatusCheck(apps: Pm2App[]): Promise<void> {
82+
private async performPm2StatusCheck(app: ShipnodeApp): Promise<void> {
8383
const mise = `export PATH="$HOME/.local/bin:$HOME/.local/share/mise/shims:$PATH"`;
8484
const result = await this.executor.exec(`${mise} && mise exec -- pm2 jlist`);
8585

@@ -94,33 +94,36 @@ export class HealthCheckService {
9494
);
9595
}
9696

97-
const namespace = getDeploymentName(this.config) ?? '';
97+
// The PM2 namespace is per-app (equal to that app's first pm2 entry),
98+
// not workspace-wide. Using the workspace's deploymentName would build the
99+
// wrong prefix for every non-first app in a multi-app workspace.
100+
const namespace = app.pm2?.apps[0]?.name ?? '';
98101
const byName = new Map(parsed.map((e) => [e.name, e]));
99102
const failures: string[] = [];
100103

101-
for (const app of apps) {
102-
const pm2Name = getPm2Name(namespace, app.name);
104+
for (const pm2App of app.pm2?.apps ?? []) {
105+
const pm2Name = getPm2Name(namespace, pm2App.name);
103106
const entry = byName.get(pm2Name);
104107
if (!entry) {
105-
failures.push(`${app.name}: not running (no PM2 entry found)`);
108+
failures.push(`${pm2Name}: not running (no PM2 entry found)`);
106109
continue;
107110
}
108111
const status = entry.pm2_env?.status;
109112
const restarts = entry.pm2_env?.restart_time ?? 0;
110113
if (status !== 'online') {
111-
failures.push(`${app.name}: status=${status ?? 'unknown'}`);
114+
failures.push(`${pm2Name}: status=${status ?? 'unknown'}`);
112115
continue;
113116
}
114117
if (restarts > 0) {
115-
failures.push(`${app.name}: crashed during startup (restart_time=${restarts})`);
118+
failures.push(`${pm2Name}: crashed during startup (restart_time=${restarts})`);
116119
}
117120
}
118121

119122
if (failures.length === 0) return;
120123

121124
let diagnostics = '';
122-
for (const app of apps) {
123-
const pm2Name = getPm2Name(namespace, app.name);
125+
for (const pm2App of app.pm2?.apps ?? []) {
126+
const pm2Name = getPm2Name(namespace, pm2App.name);
124127
const entry = byName.get(pm2Name);
125128
if (entry && entry.pm2_env?.status === 'online' && (entry.pm2_env?.restart_time ?? 0) === 0) continue;
126129
diagnostics += await this.collectPm2Logs(pm2Name);

0 commit comments

Comments
 (0)