-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathecosystem.config.cjs
More file actions
100 lines (95 loc) · 3.43 KB
/
Copy pathecosystem.config.cjs
File metadata and controls
100 lines (95 loc) · 3.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
* PM2 process definition for a bare-metal / dedicated-server deployment.
*
* pnpm install
* ./scripts/deploy.sh # build, migrate, seed
* pm2 start ecosystem.config.cjs
* pm2 save && pm2 startup # persist across reboots
*
* Secrets live in the repo-root `.env` (copied from `.env.example`). The API process
* loads that file itself (apps/api/src/config/dotenv.ts). For the web process we read a
* couple of values here so PORT/HOSTNAME are honoured no matter how PM2 is invoked.
*/
const fs = require('node:fs');
const path = require('node:path');
const root = __dirname;
/** Minimal, dependency-free .env reader (KEY=VALUE, ignores comments/blank lines). */
function readEnv(file) {
const out = {};
try {
for (const raw of fs.readFileSync(file, 'utf8').split('\n')) {
const line = raw.trim();
if (!line || line.startsWith('#')) continue;
const eq = line.indexOf('=');
if (eq === -1) continue;
let value = line.slice(eq + 1).trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
out[line.slice(0, eq).trim()] = value;
}
} catch {
/* no .env yet — fall back to defaults below */
}
return out;
}
const env = readEnv(path.join(root, '.env'));
// If a project-local PostgreSQL cluster exists (created by scripts/postgres-local.sh on a
// random port), let PM2 supervise it too, ahead of the API.
const hasLocalPostgres = fs.existsSync(path.join(root, '.postgres', 'data', 'PG_VERSION'));
// PostgreSQL refuses to run as root. The cluster is owned by an unprivileged account (set up
// by scripts/setup-wizard.sh); when PM2 itself runs as root we must drop to that account, or
// `postgres-local.sh start` would refuse to launch. Derive the uid/gid from whoever owns the
// cluster so this holds no matter which user it was created under.
let localPostgresOwner = null;
if (hasLocalPostgres) {
try {
const st = fs.statSync(path.join(root, '.postgres'));
if (st.uid !== 0) localPostgresOwner = { uid: st.uid, gid: st.gid };
} catch {
/* fall back to inheriting PM2's own user */
}
}
const localPostgresApp = {
name: 'opencoperlock-postgres',
cwd: root,
script: 'scripts/postgres-local.sh',
args: 'start',
interpreter: 'bash',
autorestart: true,
max_restarts: 10,
// Drop privileges to the cluster owner when PM2 runs as root (no-op if already that user).
...(localPostgresOwner || {}),
};
module.exports = {
apps: [
...(hasLocalPostgres ? [localPostgresApp] : []),
{
name: 'opencoperlock-api',
cwd: path.join(root, 'apps/api'),
script: 'dist/index.js',
instances: 1,
exec_mode: 'fork', // the in-process Remote-Upload worker must be a single instance
env: { NODE_ENV: 'production' },
max_memory_restart: '512M',
kill_timeout: 10000, // give in-flight uploads / graceful shutdown time
},
{
name: 'opencoperlock-web',
cwd: path.join(root, 'apps/web'),
script: '.next/standalone/apps/web/server.js',
instances: 1,
exec_mode: 'fork',
env: {
NODE_ENV: 'production',
PORT: env.WEB_PORT || process.env.WEB_PORT || 3000,
// Bound to localhost when WEB_HOST=127.0.0.1 (set by the wizard when behind nginx).
HOSTNAME: env.WEB_HOST || process.env.WEB_HOST || '0.0.0.0',
},
max_memory_restart: '512M',
},
],
};