create-nextblock 0.12.3 → 0.12.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docker-template/.env.docker.example +13 -0
- package/docker-template/docker-compose.yml +7 -2
- package/docker-template/scripts/docker-setup.mjs +104 -10
- package/package.json +1 -1
- package/templates/nextblock-template/docker-compose.yml +7 -2
- package/templates/nextblock-template/lib/onboarding/status.ts +14 -5
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/scripts/docker-setup.mjs +104 -10
|
@@ -15,6 +15,19 @@ CRON_SECRET=replace-me-generated
|
|
|
15
15
|
DRAFT_MODE_SECRET=replace-me-generated
|
|
16
16
|
REVALIDATE_SECRET_TOKEN=replace-me-generated
|
|
17
17
|
|
|
18
|
+
# --- Host port mappings ----------------------------------------------------------------------
|
|
19
|
+
# The published host ports for each service. docker:setup auto-picks a FREE port when a default
|
|
20
|
+
# is taken or reserved by the OS (common on Windows, e.g. Postgres 54322), then keeps the URLs
|
|
21
|
+
# below and LOOPBACK_PROXIES in lockstep. Change a port here only if you also update its URL.
|
|
22
|
+
APP_PORT=3000
|
|
23
|
+
KONG_HTTP_PORT=8000
|
|
24
|
+
MINIO_S3_PORT=9000
|
|
25
|
+
MINIO_CONSOLE_PORT=9001
|
|
26
|
+
POSTGRES_PORT_EXTERNAL=54322
|
|
27
|
+
# In-container loopback (socat): maps the localhost:<port> server code derives from the public
|
|
28
|
+
# URLs to the real service. Left-hand ports MUST match NEXT_PUBLIC_SUPABASE_URL / R2_S3_PUBLIC_ENDPOINT.
|
|
29
|
+
LOOPBACK_PROXIES=8000:kong:8000 9000:minio:9000
|
|
30
|
+
|
|
18
31
|
# --- Supabase wiring (internal container network vs browser) ---------------------------------
|
|
19
32
|
NEXT_PUBLIC_SUPABASE_URL=http://localhost:8000
|
|
20
33
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=${ANON_KEY}
|
|
@@ -182,8 +182,13 @@ services:
|
|
|
182
182
|
NODE_ENV: production
|
|
183
183
|
PORT: 3000
|
|
184
184
|
HOSTNAME: 0.0.0.0
|
|
185
|
-
#
|
|
186
|
-
#
|
|
185
|
+
# In-container loopback proxies (see Dockerfile entrypoint): map the localhost:<port> that
|
|
186
|
+
# server-side code derives from the public URLs to the real service. docker:setup rewrites
|
|
187
|
+
# this when a default host port is taken/reserved and it remaps Kong/MinIO to a free port,
|
|
188
|
+
# so the left-hand ports here always match NEXT_PUBLIC_SUPABASE_URL / R2_S3_PUBLIC_ENDPOINT.
|
|
189
|
+
LOOPBACK_PROXIES: ${LOOPBACK_PROXIES:-8000:kong:8000 9000:minio:9000}
|
|
190
|
+
# Browser AND server both use the same Supabase URL; the loopback proxy above forwards it to
|
|
191
|
+
# Kong, keeping the host-derived Supabase auth cookie key consistent for SSR.
|
|
187
192
|
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL:-http://localhost:8000}
|
|
188
193
|
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${ANON_KEY}
|
|
189
194
|
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
|
|
@@ -12,6 +12,7 @@ import { readFile, writeFile, access } from 'node:fs/promises';
|
|
|
12
12
|
import { resolve, dirname } from 'node:path';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
14
|
import { spawn, spawnSync } from 'node:child_process';
|
|
15
|
+
import { createServer } from 'node:net';
|
|
15
16
|
|
|
16
17
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
18
|
const PROJECT_ROOT = resolve(__dirname, '..');
|
|
@@ -79,6 +80,41 @@ const pathExists = async (p) => {
|
|
|
79
80
|
|
|
80
81
|
const commandWorks = (cmd, args) => spawnSync(cmd, args, { stdio: 'ignore' }).status === 0;
|
|
81
82
|
|
|
83
|
+
// Can the host PUBLISH (bind) this TCP port? Docker publishes on 0.0.0.0, and on Windows the
|
|
84
|
+
// Hyper-V/WinNAT excluded port ranges (which sit in the 49152+ ephemeral band — e.g. the default
|
|
85
|
+
// Postgres port 54322) or any process already holding the port make the bind fail with
|
|
86
|
+
// EACCES ("access permissions") / EADDRINUSE — exactly what `docker compose up` fails on. Probing
|
|
87
|
+
// here lets us pick a working host port BEFORE compose tries and aborts the whole stack.
|
|
88
|
+
function canBindPort(port) {
|
|
89
|
+
return new Promise((res) => {
|
|
90
|
+
const srv = createServer();
|
|
91
|
+
srv.once('error', () => res(false));
|
|
92
|
+
srv.once('listening', () => srv.close(() => res(true)));
|
|
93
|
+
try {
|
|
94
|
+
srv.listen(port, '0.0.0.0');
|
|
95
|
+
} catch {
|
|
96
|
+
res(false);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Prefer the conventional port; if it's unavailable, fall back into the registered-port band
|
|
102
|
+
// (1024–49151, below the ephemeral range WinNAT reserves) rather than just incrementing — a
|
|
103
|
+
// reserved/used port often sits inside a contiguous block, so +1 tends to be taken too.
|
|
104
|
+
async function findAvailablePort(preferred, fallbackBase, taken) {
|
|
105
|
+
const candidates = [preferred];
|
|
106
|
+
for (let i = 0; i < 100; i++) candidates.push(fallbackBase + i);
|
|
107
|
+
for (const p of candidates) {
|
|
108
|
+
if (taken.has(p)) continue;
|
|
109
|
+
if (await canBindPort(p)) {
|
|
110
|
+
taken.add(p);
|
|
111
|
+
return p;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
taken.add(preferred);
|
|
115
|
+
return preferred; // give up gracefully — let compose surface the real error
|
|
116
|
+
}
|
|
117
|
+
|
|
82
118
|
function detectCompose() {
|
|
83
119
|
if (commandWorks('docker', ['compose', 'version'])) return { cmd: 'docker', args: ['compose'] };
|
|
84
120
|
if (commandWorks('docker-compose', ['version'])) return { cmd: 'docker-compose', args: [] };
|
|
@@ -138,6 +174,41 @@ async function main() {
|
|
|
138
174
|
const minioPassword = reuse('MINIO_ROOT_PASSWORD', generateSecret);
|
|
139
175
|
const bucket = readEnvValue(existing, 'STORAGE_BUCKET') || 'nextblock';
|
|
140
176
|
|
|
177
|
+
// Resolve host ports up front so a port that's in use or reserved by the OS (very common on
|
|
178
|
+
// Windows for the default Postgres port 54322) never aborts `docker compose up`. Reuse a port
|
|
179
|
+
// already encoded in .env — the explicit *_PORT* var, else the port parsed from a coupled URL —
|
|
180
|
+
// so re-runs stay stable and never disturb an already-running stack; only a FRESH install probes
|
|
181
|
+
// for free ports. Coupled ports (Kong/MinIO/app) also drive the URLs + loopback proxy below.
|
|
182
|
+
const takenPorts = new Set();
|
|
183
|
+
const reusedPort = (key, urlKey) => {
|
|
184
|
+
const direct = parseInt(readEnvValue(existing, key), 10);
|
|
185
|
+
if (Number.isInteger(direct) && direct > 0) return direct;
|
|
186
|
+
if (urlKey) {
|
|
187
|
+
const m = readEnvValue(existing, urlKey).match(/:(\d{2,5})(?:\/|$)/);
|
|
188
|
+
if (m) return parseInt(m[1], 10);
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
};
|
|
192
|
+
const resolvePort = async (key, preferred, fallbackBase, urlKey) => {
|
|
193
|
+
const prev = reusedPort(key, urlKey);
|
|
194
|
+
if (prev) {
|
|
195
|
+
takenPorts.add(prev);
|
|
196
|
+
return prev;
|
|
197
|
+
}
|
|
198
|
+
if (existing) {
|
|
199
|
+
// An existing .env with no recorded port: keep the default rather than reshuffle a setup
|
|
200
|
+
// the user may already be running.
|
|
201
|
+
takenPorts.add(preferred);
|
|
202
|
+
return preferred;
|
|
203
|
+
}
|
|
204
|
+
return findAvailablePort(preferred, fallbackBase, takenPorts);
|
|
205
|
+
};
|
|
206
|
+
const appPort = await resolvePort('APP_PORT', 3000, 13000, 'NEXT_PUBLIC_URL');
|
|
207
|
+
const kongPort = await resolvePort('KONG_HTTP_PORT', 8000, 18000, 'NEXT_PUBLIC_SUPABASE_URL');
|
|
208
|
+
const minioS3Port = await resolvePort('MINIO_S3_PORT', 9000, 19000, 'R2_S3_PUBLIC_ENDPOINT');
|
|
209
|
+
const minioConsolePort = await resolvePort('MINIO_CONSOLE_PORT', 9001, 19001, null);
|
|
210
|
+
const dbPort = await resolvePort('POSTGRES_PORT_EXTERNAL', 54322, 15432, null);
|
|
211
|
+
|
|
141
212
|
const replacements = {
|
|
142
213
|
POSTGRES_PASSWORD: `POSTGRES_PASSWORD=${postgresPassword}`,
|
|
143
214
|
POSTGRES_DB: 'POSTGRES_DB=postgres',
|
|
@@ -145,12 +216,22 @@ async function main() {
|
|
|
145
216
|
JWT_EXP: 'JWT_EXP=3600',
|
|
146
217
|
ANON_KEY: `ANON_KEY=${anonKey}`,
|
|
147
218
|
SERVICE_ROLE_KEY: `SERVICE_ROLE_KEY=${serviceRoleKey}`,
|
|
148
|
-
|
|
219
|
+
// Host port mappings (compose reads these) + the coupled public URLs, kept in lockstep so a
|
|
220
|
+
// remapped port stays consistent everywhere it's referenced.
|
|
221
|
+
APP_PORT: `APP_PORT=${appPort}`,
|
|
222
|
+
KONG_HTTP_PORT: `KONG_HTTP_PORT=${kongPort}`,
|
|
223
|
+
MINIO_S3_PORT: `MINIO_S3_PORT=${minioS3Port}`,
|
|
224
|
+
MINIO_CONSOLE_PORT: `MINIO_CONSOLE_PORT=${minioConsolePort}`,
|
|
225
|
+
POSTGRES_PORT_EXTERNAL: `POSTGRES_PORT_EXTERNAL=${dbPort}`,
|
|
226
|
+
// In-container loopback: server code hits localhost:<kongPort> / 127.0.0.1:<minioS3Port> and
|
|
227
|
+
// socat forwards to the real service. Left ports MUST match the URLs below.
|
|
228
|
+
LOOPBACK_PROXIES: `LOOPBACK_PROXIES=${kongPort}:kong:8000 ${minioS3Port}:minio:9000`,
|
|
229
|
+
NEXT_PUBLIC_SUPABASE_URL: `NEXT_PUBLIC_SUPABASE_URL=http://localhost:${kongPort}`,
|
|
149
230
|
NEXT_PUBLIC_SUPABASE_ANON_KEY: `NEXT_PUBLIC_SUPABASE_ANON_KEY=${anonKey}`,
|
|
150
231
|
SUPABASE_SERVICE_ROLE_KEY: `SUPABASE_SERVICE_ROLE_KEY=${serviceRoleKey}`,
|
|
151
|
-
API_EXTERNAL_URL:
|
|
152
|
-
SITE_URL:
|
|
153
|
-
NEXT_PUBLIC_URL:
|
|
232
|
+
API_EXTERNAL_URL: `API_EXTERNAL_URL=http://localhost:${kongPort}`,
|
|
233
|
+
SITE_URL: `SITE_URL=http://localhost:${appPort}`,
|
|
234
|
+
NEXT_PUBLIC_URL: `NEXT_PUBLIC_URL=http://localhost:${appPort}`,
|
|
154
235
|
NEXT_PUBLIC_IS_SANDBOX: 'NEXT_PUBLIC_IS_SANDBOX=true',
|
|
155
236
|
CRON_SECRET: `CRON_SECRET=${cronSecret}`,
|
|
156
237
|
DRAFT_MODE_SECRET: `DRAFT_MODE_SECRET=${draftSecret}`,
|
|
@@ -165,10 +246,10 @@ async function main() {
|
|
|
165
246
|
// port-scoped, so the browser would send the app's Supabase auth cookies to MinIO too — and
|
|
166
247
|
// MinIO rejects oversized header sets (MetadataTooLarge), breaking image display once cookies
|
|
167
248
|
// grow. 127.0.0.1 is a different cookie host, so the browser never sends them there.
|
|
168
|
-
R2_S3_PUBLIC_ENDPOINT:
|
|
249
|
+
R2_S3_PUBLIC_ENDPOINT: `R2_S3_PUBLIC_ENDPOINT=http://127.0.0.1:${minioS3Port}`,
|
|
169
250
|
R2_FORCE_PATH_STYLE: 'R2_FORCE_PATH_STYLE=true',
|
|
170
|
-
NEXT_PUBLIC_R2_BASE_URL: `NEXT_PUBLIC_R2_BASE_URL=http://127.0.0.1
|
|
171
|
-
NEXT_PUBLIC_R2_PUBLIC_URL: `NEXT_PUBLIC_R2_PUBLIC_URL=http://127.0.0.1
|
|
251
|
+
NEXT_PUBLIC_R2_BASE_URL: `NEXT_PUBLIC_R2_BASE_URL=http://127.0.0.1:${minioS3Port}/${bucket}`,
|
|
252
|
+
NEXT_PUBLIC_R2_PUBLIC_URL: `NEXT_PUBLIC_R2_PUBLIC_URL=http://127.0.0.1:${minioS3Port}/${bucket}`,
|
|
172
253
|
NEXT_PUBLIC_TURNSTILE_SITE_KEY: `NEXT_PUBLIC_TURNSTILE_SITE_KEY=${turnstileSiteKey}`,
|
|
173
254
|
TURNSTILE_SECRET_KEY: `TURNSTILE_SECRET_KEY=${turnstileSecretKey}`,
|
|
174
255
|
GOTRUE_MAILER_AUTOCONFIRM: `GOTRUE_MAILER_AUTOCONFIRM=${mailerAutoconfirm}`,
|
|
@@ -186,6 +267,19 @@ async function main() {
|
|
|
186
267
|
await writeFile(ENV_PATH, nextEnv, 'utf8');
|
|
187
268
|
console.log('✓ Wrote .env (Postgres, JWT secret + signed anon/service keys, MinIO, app secrets).\n');
|
|
188
269
|
|
|
270
|
+
const remapped = [
|
|
271
|
+
appPort !== 3000 && `app ${appPort} (default 3000)`,
|
|
272
|
+
kongPort !== 8000 && `Supabase API ${kongPort} (default 8000)`,
|
|
273
|
+
minioS3Port !== 9000 && `MinIO S3 ${minioS3Port} (default 9000)`,
|
|
274
|
+
minioConsolePort !== 9001 && `MinIO console ${minioConsolePort} (default 9001)`,
|
|
275
|
+
dbPort !== 54322 && `Postgres ${dbPort} (default 54322)`,
|
|
276
|
+
].filter(Boolean);
|
|
277
|
+
if (remapped.length) {
|
|
278
|
+
console.log(
|
|
279
|
+
`↔ Some default host ports were unavailable (in use, or reserved by the OS — common on Windows).\n Remapped to free ports: ${remapped.join(', ')}.\n`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
189
283
|
// A brand-new .env means brand-new secrets. Postgres only runs its init scripts (which set role
|
|
190
284
|
// passwords) on an EMPTY volume, so a leftover volume from a previous install would keep the old
|
|
191
285
|
// credentials and GoTrue/PostgREST could not log in. Reset volumes when the config is fresh.
|
|
@@ -202,10 +296,10 @@ async function main() {
|
|
|
202
296
|
await run(compose.cmd, [...compose.args, 'up', '-d', '--build'], { cwd: PROJECT_ROOT });
|
|
203
297
|
|
|
204
298
|
console.log('\n🎉 Stack is up!');
|
|
205
|
-
console.log(
|
|
206
|
-
console.log(
|
|
299
|
+
console.log(` 1. Open the app: http://localhost:${appPort}`);
|
|
300
|
+
console.log(` 2. Finish setup: complete the browser wizard at http://localhost:${appPort}/setup`);
|
|
207
301
|
console.log(' (creates your first admin — auto-confirmed, no email needed).');
|
|
208
|
-
console.log(
|
|
302
|
+
console.log(` 3. Supabase API: http://localhost:${kongPort} MinIO console: http://localhost:${minioConsolePort}`);
|
|
209
303
|
const composeStr = `${compose.cmd} ${compose.args.join(' ')}`.trim();
|
|
210
304
|
console.log(`\n Logs: ${composeStr} logs -f nextblock-cms | Stop: ${composeStr} down (add -v to wipe data)`);
|
|
211
305
|
}
|
package/package.json
CHANGED
|
@@ -182,8 +182,13 @@ services:
|
|
|
182
182
|
NODE_ENV: production
|
|
183
183
|
PORT: 3000
|
|
184
184
|
HOSTNAME: 0.0.0.0
|
|
185
|
-
#
|
|
186
|
-
#
|
|
185
|
+
# In-container loopback proxies (see Dockerfile entrypoint): map the localhost:<port> that
|
|
186
|
+
# server-side code derives from the public URLs to the real service. docker:setup rewrites
|
|
187
|
+
# this when a default host port is taken/reserved and it remaps Kong/MinIO to a free port,
|
|
188
|
+
# so the left-hand ports here always match NEXT_PUBLIC_SUPABASE_URL / R2_S3_PUBLIC_ENDPOINT.
|
|
189
|
+
LOOPBACK_PROXIES: ${LOOPBACK_PROXIES:-8000:kong:8000 9000:minio:9000}
|
|
190
|
+
# Browser AND server both use the same Supabase URL; the loopback proxy above forwards it to
|
|
191
|
+
# Kong, keeping the host-derived Supabase auth cookie key consistent for SSR.
|
|
187
192
|
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL:-http://localhost:8000}
|
|
188
193
|
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${ANON_KEY}
|
|
189
194
|
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
|
|
@@ -30,11 +30,20 @@ export type OnboardingStatus = {
|
|
|
30
30
|
dismissed: boolean;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
// Seeded defaults (libs/db
|
|
34
|
-
// "done" only when the value has been customized away from
|
|
35
|
-
// with the seeds, so a plain presence check would mark every
|
|
33
|
+
// Seeded defaults (libs/db baseline seed 00000000000003 + migration 00000000000004). The
|
|
34
|
+
// branding/copyright steps count as "done" only when the value has been customized away from
|
|
35
|
+
// these — a fresh install ships with the seeds, so a plain presence check would mark every
|
|
36
|
+
// step complete immediately.
|
|
36
37
|
const SEEDED_SITE_TITLE = 'NextBlock™ CMS';
|
|
37
|
-
|
|
38
|
+
// Every object_key a fresh install may ship as the DEFAULT logo — the original baseline WebP
|
|
39
|
+
// and the migration-00000000000004 email-safe PNG (which repoints the seeded default). A logo
|
|
40
|
+
// counts as customized only when it is none of these; a new default swap must be added here or
|
|
41
|
+
// branding falsely reads as done out of the box. Mirrors BUNDLED_PUBLIC_MEDIA_KEYS in
|
|
42
|
+
// lib/media/resolveMediaUrl.ts.
|
|
43
|
+
const SEEDED_LOGO_OBJECT_KEYS = new Set<string>([
|
|
44
|
+
'images/nextblock-logo-small.webp',
|
|
45
|
+
'images/nextblock-logo-button-tiny.png',
|
|
46
|
+
]);
|
|
38
47
|
const SEEDED_COPYRIGHT: Record<string, string> = {
|
|
39
48
|
en: '© {year} Nextblock CMS. All rights reserved.',
|
|
40
49
|
fr: '© {year} Nextblock CMS. Tous droits réservés.',
|
|
@@ -88,7 +97,7 @@ export async function getOnboardingStatus(opts: {
|
|
|
88
97
|
const siteTitle = typeof siteTitleRaw === 'string' ? siteTitleRaw.trim() : '';
|
|
89
98
|
const siteTitleCustomized = siteTitle.length > 0 && siteTitle !== SEEDED_SITE_TITLE;
|
|
90
99
|
const logoObjectKey = extractLogoObjectKey(logoRow);
|
|
91
|
-
const logoCustomized = Boolean(logoObjectKey) && logoObjectKey
|
|
100
|
+
const logoCustomized = Boolean(logoObjectKey) && !SEEDED_LOGO_OBJECT_KEYS.has(logoObjectKey ?? '');
|
|
92
101
|
|
|
93
102
|
// Branding is "done" once the user renames the site or uploads their own logo — not merely
|
|
94
103
|
// because the seeded NextBlock title/logo exist.
|
|
@@ -12,6 +12,7 @@ import { readFile, writeFile, access } from 'node:fs/promises';
|
|
|
12
12
|
import { resolve, dirname } from 'node:path';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
14
|
import { spawn, spawnSync } from 'node:child_process';
|
|
15
|
+
import { createServer } from 'node:net';
|
|
15
16
|
|
|
16
17
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
18
|
const PROJECT_ROOT = resolve(__dirname, '..');
|
|
@@ -79,6 +80,41 @@ const pathExists = async (p) => {
|
|
|
79
80
|
|
|
80
81
|
const commandWorks = (cmd, args) => spawnSync(cmd, args, { stdio: 'ignore' }).status === 0;
|
|
81
82
|
|
|
83
|
+
// Can the host PUBLISH (bind) this TCP port? Docker publishes on 0.0.0.0, and on Windows the
|
|
84
|
+
// Hyper-V/WinNAT excluded port ranges (which sit in the 49152+ ephemeral band — e.g. the default
|
|
85
|
+
// Postgres port 54322) or any process already holding the port make the bind fail with
|
|
86
|
+
// EACCES ("access permissions") / EADDRINUSE — exactly what `docker compose up` fails on. Probing
|
|
87
|
+
// here lets us pick a working host port BEFORE compose tries and aborts the whole stack.
|
|
88
|
+
function canBindPort(port) {
|
|
89
|
+
return new Promise((res) => {
|
|
90
|
+
const srv = createServer();
|
|
91
|
+
srv.once('error', () => res(false));
|
|
92
|
+
srv.once('listening', () => srv.close(() => res(true)));
|
|
93
|
+
try {
|
|
94
|
+
srv.listen(port, '0.0.0.0');
|
|
95
|
+
} catch {
|
|
96
|
+
res(false);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Prefer the conventional port; if it's unavailable, fall back into the registered-port band
|
|
102
|
+
// (1024–49151, below the ephemeral range WinNAT reserves) rather than just incrementing — a
|
|
103
|
+
// reserved/used port often sits inside a contiguous block, so +1 tends to be taken too.
|
|
104
|
+
async function findAvailablePort(preferred, fallbackBase, taken) {
|
|
105
|
+
const candidates = [preferred];
|
|
106
|
+
for (let i = 0; i < 100; i++) candidates.push(fallbackBase + i);
|
|
107
|
+
for (const p of candidates) {
|
|
108
|
+
if (taken.has(p)) continue;
|
|
109
|
+
if (await canBindPort(p)) {
|
|
110
|
+
taken.add(p);
|
|
111
|
+
return p;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
taken.add(preferred);
|
|
115
|
+
return preferred; // give up gracefully — let compose surface the real error
|
|
116
|
+
}
|
|
117
|
+
|
|
82
118
|
function detectCompose() {
|
|
83
119
|
if (commandWorks('docker', ['compose', 'version'])) return { cmd: 'docker', args: ['compose'] };
|
|
84
120
|
if (commandWorks('docker-compose', ['version'])) return { cmd: 'docker-compose', args: [] };
|
|
@@ -138,6 +174,41 @@ async function main() {
|
|
|
138
174
|
const minioPassword = reuse('MINIO_ROOT_PASSWORD', generateSecret);
|
|
139
175
|
const bucket = readEnvValue(existing, 'STORAGE_BUCKET') || 'nextblock';
|
|
140
176
|
|
|
177
|
+
// Resolve host ports up front so a port that's in use or reserved by the OS (very common on
|
|
178
|
+
// Windows for the default Postgres port 54322) never aborts `docker compose up`. Reuse a port
|
|
179
|
+
// already encoded in .env — the explicit *_PORT* var, else the port parsed from a coupled URL —
|
|
180
|
+
// so re-runs stay stable and never disturb an already-running stack; only a FRESH install probes
|
|
181
|
+
// for free ports. Coupled ports (Kong/MinIO/app) also drive the URLs + loopback proxy below.
|
|
182
|
+
const takenPorts = new Set();
|
|
183
|
+
const reusedPort = (key, urlKey) => {
|
|
184
|
+
const direct = parseInt(readEnvValue(existing, key), 10);
|
|
185
|
+
if (Number.isInteger(direct) && direct > 0) return direct;
|
|
186
|
+
if (urlKey) {
|
|
187
|
+
const m = readEnvValue(existing, urlKey).match(/:(\d{2,5})(?:\/|$)/);
|
|
188
|
+
if (m) return parseInt(m[1], 10);
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
};
|
|
192
|
+
const resolvePort = async (key, preferred, fallbackBase, urlKey) => {
|
|
193
|
+
const prev = reusedPort(key, urlKey);
|
|
194
|
+
if (prev) {
|
|
195
|
+
takenPorts.add(prev);
|
|
196
|
+
return prev;
|
|
197
|
+
}
|
|
198
|
+
if (existing) {
|
|
199
|
+
// An existing .env with no recorded port: keep the default rather than reshuffle a setup
|
|
200
|
+
// the user may already be running.
|
|
201
|
+
takenPorts.add(preferred);
|
|
202
|
+
return preferred;
|
|
203
|
+
}
|
|
204
|
+
return findAvailablePort(preferred, fallbackBase, takenPorts);
|
|
205
|
+
};
|
|
206
|
+
const appPort = await resolvePort('APP_PORT', 3000, 13000, 'NEXT_PUBLIC_URL');
|
|
207
|
+
const kongPort = await resolvePort('KONG_HTTP_PORT', 8000, 18000, 'NEXT_PUBLIC_SUPABASE_URL');
|
|
208
|
+
const minioS3Port = await resolvePort('MINIO_S3_PORT', 9000, 19000, 'R2_S3_PUBLIC_ENDPOINT');
|
|
209
|
+
const minioConsolePort = await resolvePort('MINIO_CONSOLE_PORT', 9001, 19001, null);
|
|
210
|
+
const dbPort = await resolvePort('POSTGRES_PORT_EXTERNAL', 54322, 15432, null);
|
|
211
|
+
|
|
141
212
|
const replacements = {
|
|
142
213
|
POSTGRES_PASSWORD: `POSTGRES_PASSWORD=${postgresPassword}`,
|
|
143
214
|
POSTGRES_DB: 'POSTGRES_DB=postgres',
|
|
@@ -145,12 +216,22 @@ async function main() {
|
|
|
145
216
|
JWT_EXP: 'JWT_EXP=3600',
|
|
146
217
|
ANON_KEY: `ANON_KEY=${anonKey}`,
|
|
147
218
|
SERVICE_ROLE_KEY: `SERVICE_ROLE_KEY=${serviceRoleKey}`,
|
|
148
|
-
|
|
219
|
+
// Host port mappings (compose reads these) + the coupled public URLs, kept in lockstep so a
|
|
220
|
+
// remapped port stays consistent everywhere it's referenced.
|
|
221
|
+
APP_PORT: `APP_PORT=${appPort}`,
|
|
222
|
+
KONG_HTTP_PORT: `KONG_HTTP_PORT=${kongPort}`,
|
|
223
|
+
MINIO_S3_PORT: `MINIO_S3_PORT=${minioS3Port}`,
|
|
224
|
+
MINIO_CONSOLE_PORT: `MINIO_CONSOLE_PORT=${minioConsolePort}`,
|
|
225
|
+
POSTGRES_PORT_EXTERNAL: `POSTGRES_PORT_EXTERNAL=${dbPort}`,
|
|
226
|
+
// In-container loopback: server code hits localhost:<kongPort> / 127.0.0.1:<minioS3Port> and
|
|
227
|
+
// socat forwards to the real service. Left ports MUST match the URLs below.
|
|
228
|
+
LOOPBACK_PROXIES: `LOOPBACK_PROXIES=${kongPort}:kong:8000 ${minioS3Port}:minio:9000`,
|
|
229
|
+
NEXT_PUBLIC_SUPABASE_URL: `NEXT_PUBLIC_SUPABASE_URL=http://localhost:${kongPort}`,
|
|
149
230
|
NEXT_PUBLIC_SUPABASE_ANON_KEY: `NEXT_PUBLIC_SUPABASE_ANON_KEY=${anonKey}`,
|
|
150
231
|
SUPABASE_SERVICE_ROLE_KEY: `SUPABASE_SERVICE_ROLE_KEY=${serviceRoleKey}`,
|
|
151
|
-
API_EXTERNAL_URL:
|
|
152
|
-
SITE_URL:
|
|
153
|
-
NEXT_PUBLIC_URL:
|
|
232
|
+
API_EXTERNAL_URL: `API_EXTERNAL_URL=http://localhost:${kongPort}`,
|
|
233
|
+
SITE_URL: `SITE_URL=http://localhost:${appPort}`,
|
|
234
|
+
NEXT_PUBLIC_URL: `NEXT_PUBLIC_URL=http://localhost:${appPort}`,
|
|
154
235
|
NEXT_PUBLIC_IS_SANDBOX: 'NEXT_PUBLIC_IS_SANDBOX=true',
|
|
155
236
|
CRON_SECRET: `CRON_SECRET=${cronSecret}`,
|
|
156
237
|
DRAFT_MODE_SECRET: `DRAFT_MODE_SECRET=${draftSecret}`,
|
|
@@ -165,10 +246,10 @@ async function main() {
|
|
|
165
246
|
// port-scoped, so the browser would send the app's Supabase auth cookies to MinIO too — and
|
|
166
247
|
// MinIO rejects oversized header sets (MetadataTooLarge), breaking image display once cookies
|
|
167
248
|
// grow. 127.0.0.1 is a different cookie host, so the browser never sends them there.
|
|
168
|
-
R2_S3_PUBLIC_ENDPOINT:
|
|
249
|
+
R2_S3_PUBLIC_ENDPOINT: `R2_S3_PUBLIC_ENDPOINT=http://127.0.0.1:${minioS3Port}`,
|
|
169
250
|
R2_FORCE_PATH_STYLE: 'R2_FORCE_PATH_STYLE=true',
|
|
170
|
-
NEXT_PUBLIC_R2_BASE_URL: `NEXT_PUBLIC_R2_BASE_URL=http://127.0.0.1
|
|
171
|
-
NEXT_PUBLIC_R2_PUBLIC_URL: `NEXT_PUBLIC_R2_PUBLIC_URL=http://127.0.0.1
|
|
251
|
+
NEXT_PUBLIC_R2_BASE_URL: `NEXT_PUBLIC_R2_BASE_URL=http://127.0.0.1:${minioS3Port}/${bucket}`,
|
|
252
|
+
NEXT_PUBLIC_R2_PUBLIC_URL: `NEXT_PUBLIC_R2_PUBLIC_URL=http://127.0.0.1:${minioS3Port}/${bucket}`,
|
|
172
253
|
NEXT_PUBLIC_TURNSTILE_SITE_KEY: `NEXT_PUBLIC_TURNSTILE_SITE_KEY=${turnstileSiteKey}`,
|
|
173
254
|
TURNSTILE_SECRET_KEY: `TURNSTILE_SECRET_KEY=${turnstileSecretKey}`,
|
|
174
255
|
GOTRUE_MAILER_AUTOCONFIRM: `GOTRUE_MAILER_AUTOCONFIRM=${mailerAutoconfirm}`,
|
|
@@ -186,6 +267,19 @@ async function main() {
|
|
|
186
267
|
await writeFile(ENV_PATH, nextEnv, 'utf8');
|
|
187
268
|
console.log('✓ Wrote .env (Postgres, JWT secret + signed anon/service keys, MinIO, app secrets).\n');
|
|
188
269
|
|
|
270
|
+
const remapped = [
|
|
271
|
+
appPort !== 3000 && `app ${appPort} (default 3000)`,
|
|
272
|
+
kongPort !== 8000 && `Supabase API ${kongPort} (default 8000)`,
|
|
273
|
+
minioS3Port !== 9000 && `MinIO S3 ${minioS3Port} (default 9000)`,
|
|
274
|
+
minioConsolePort !== 9001 && `MinIO console ${minioConsolePort} (default 9001)`,
|
|
275
|
+
dbPort !== 54322 && `Postgres ${dbPort} (default 54322)`,
|
|
276
|
+
].filter(Boolean);
|
|
277
|
+
if (remapped.length) {
|
|
278
|
+
console.log(
|
|
279
|
+
`↔ Some default host ports were unavailable (in use, or reserved by the OS — common on Windows).\n Remapped to free ports: ${remapped.join(', ')}.\n`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
189
283
|
// A brand-new .env means brand-new secrets. Postgres only runs its init scripts (which set role
|
|
190
284
|
// passwords) on an EMPTY volume, so a leftover volume from a previous install would keep the old
|
|
191
285
|
// credentials and GoTrue/PostgREST could not log in. Reset volumes when the config is fresh.
|
|
@@ -202,10 +296,10 @@ async function main() {
|
|
|
202
296
|
await run(compose.cmd, [...compose.args, 'up', '-d', '--build'], { cwd: PROJECT_ROOT });
|
|
203
297
|
|
|
204
298
|
console.log('\n🎉 Stack is up!');
|
|
205
|
-
console.log(
|
|
206
|
-
console.log(
|
|
299
|
+
console.log(` 1. Open the app: http://localhost:${appPort}`);
|
|
300
|
+
console.log(` 2. Finish setup: complete the browser wizard at http://localhost:${appPort}/setup`);
|
|
207
301
|
console.log(' (creates your first admin — auto-confirmed, no email needed).');
|
|
208
|
-
console.log(
|
|
302
|
+
console.log(` 3. Supabase API: http://localhost:${kongPort} MinIO console: http://localhost:${minioConsolePort}`);
|
|
209
303
|
const composeStr = `${compose.cmd} ${compose.args.join(' ')}`.trim();
|
|
210
304
|
console.log(`\n Logs: ${composeStr} logs -f nextblock-cms | Stop: ${composeStr} down (add -v to wipe data)`);
|
|
211
305
|
}
|