Skip to content

Commit 31e0ea9

Browse files
author
The No Hands Company
committed
feat: auto-create bucket on startup, production compose, setup script (Step 3)
storageInit.ts - ensureBucketExists(): HeadBucket → CreateBucket if missing - Uses same env vars as storageProvider.ts (no new config) - forcePathStyle=true for MinIO compatibility - Fails gracefully — startup succeeds even if bucket creation fails (deploys will fail with a clear error rather than crashing the node) - Skips if storage not configured (development without S3) - Called in index.ts after loadBlocklist(), before bootstrapSeed docker-compose.production.yml - Overlay file: docker compose -f docker-compose.yml -f docker-compose.production.yml up -d - restart: always on all services - Memory limits: app=1g, proxy=256m, db=512m, redis=128m, minio=512m - PostgreSQL tuned: shared_buffers, wal_buffers, work_mem, checkpoint settings - Redis: AOF persistence (appendonly yes), maxmemory 100mb, allkeys-lru eviction - JSON log driver with rotation on all services setup.sh — interactive node setup script - Checks prerequisites: docker, docker compose v2, openssl - 6 questions: domain, node name/region, operator info, OIDC provider, storage capacity - OIDC-specific guidance per provider (Authentik/Keycloak/Auth0/other) - Generates secrets: COOKIE_SECRET, POSTGRES_PASSWORD, MINIO_PASSWORD (openssl rand) - Writes complete .env with all required variables - Skips config if .env already exists (safe to re-run) - docker compose pull + up -d - Waits for DB readiness (pg_isready loop) - Runs migrations - Health check + summary with next steps
1 parent 773b3d1 commit 31e0ea9

File tree

4 files changed

+438
-0
lines changed

4 files changed

+438
-0
lines changed

artifacts/api-server/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { startRetentionJob, stopRetentionJob } from "./lib/retentionCleanup";
3636
import { startEmailQueue, stopEmailQueue } from "./lib/email";
3737
import { startOrphanCleanup, stopOrphanCleanup } from "./lib/orphanCleanup";
3838
import { runBootstrapSeed } from "./lib/bootstrapSeed";
39+
import { ensureBucketExists } from "./lib/storageInit";
3940
import { db, sessionsTable } from "@workspace/db";
4041
import { lt } from "drizzle-orm";
4142
import { seedBundledSites } from "./lib/seedBundledSites";
@@ -132,6 +133,9 @@ ensureLocalNode()
132133
startHealthMonitor();
133134
await loadBlocklist();
134135
startAnalyticsFlusher();
136+
137+
// Ensure S3 bucket exists (creates it if missing — makes docker compose up work without manual MinIO setup)
138+
await ensureBucketExists();
135139
startGossipPusher();
136140
startSyncRetryQueue();
137141
startAcmeRenewalScheduler();
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Storage initialisation — ensures the configured bucket exists.
3+
*
4+
* Called once at startup. Creates the bucket if it doesn't exist yet,
5+
* which means `docker compose up` just works without manual MinIO setup.
6+
*
7+
* Safe to call on every start — no-op if bucket already exists.
8+
* Fails gracefully if the storage provider doesn't support bucket creation
9+
* (e.g. AWS S3 where buckets are created via IAM/console).
10+
*/
11+
12+
import logger from "./logger.js";
13+
14+
const ENDPOINT = process.env.OBJECT_STORAGE_ENDPOINT ?? "";
15+
const ACCESS_KEY = process.env.OBJECT_STORAGE_ACCESS_KEY ?? "";
16+
const SECRET_KEY = process.env.OBJECT_STORAGE_SECRET_KEY ?? "";
17+
const BUCKET = process.env.DEFAULT_OBJECT_STORAGE_BUCKET_ID
18+
?? process.env.OBJECT_STORAGE_BUCKET
19+
?? "nexus-sites";
20+
const REGION = process.env.OBJECT_STORAGE_REGION ?? "us-east-1";
21+
22+
export async function ensureBucketExists(): Promise<void> {
23+
if (!ENDPOINT || !ACCESS_KEY || !SECRET_KEY) {
24+
logger.debug("[storage-init] Skipping bucket check — storage not fully configured");
25+
return;
26+
}
27+
28+
try {
29+
const { S3Client, HeadBucketCommand, CreateBucketCommand } = await import("@aws-sdk/client-s3");
30+
31+
const client = new S3Client({
32+
endpoint: ENDPOINT,
33+
region: REGION,
34+
credentials: { accessKeyId: ACCESS_KEY, secretAccessKey: SECRET_KEY },
35+
forcePathStyle: true, // required for MinIO
36+
});
37+
38+
// Check if bucket exists
39+
try {
40+
await client.send(new HeadBucketCommand({ Bucket: BUCKET }));
41+
logger.info({ bucket: BUCKET }, "[storage-init] Bucket exists ✓");
42+
return;
43+
} catch (err: any) {
44+
// 404 / NoSuchBucket = doesn't exist, create it
45+
// 403 / AccessDenied = exists but no access (don't try to create)
46+
if (err?.name === "AccessDenied" || err?.$metadata?.httpStatusCode === 403) {
47+
logger.warn({ bucket: BUCKET }, "[storage-init] Bucket exists but access denied — check credentials");
48+
return;
49+
}
50+
}
51+
52+
// Create the bucket
53+
await client.send(new CreateBucketCommand({ Bucket: BUCKET }));
54+
logger.info({ bucket: BUCKET }, "[storage-init] Bucket created ✓");
55+
56+
// Set bucket to private (block public access) — objects served via presigned URLs
57+
try {
58+
const { PutBucketPolicyCommand } = await import("@aws-sdk/client-s3");
59+
// Leave as default private — no public policy needed
60+
logger.debug({ bucket: BUCKET }, "[storage-init] Bucket policy: private (default)");
61+
} catch { /* optional — skip if S3 API subset doesn't support it */ }
62+
63+
} catch (err: any) {
64+
// Non-fatal — if bucket creation fails, deploys will fail with a clear error
65+
// rather than crashing the server on startup
66+
logger.warn(
67+
{ bucket: BUCKET, endpoint: ENDPOINT, err: err.message },
68+
"[storage-init] Could not verify/create bucket — storage may not be ready yet. " +
69+
"Deploys will fail until the bucket exists."
70+
);
71+
}
72+
}

docker-compose.production.yml

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# docker-compose.production.yml
2+
#
3+
# Production hardening overlay for Nexus Hosting.
4+
# Use with:
5+
# docker compose -f docker-compose.yml -f docker-compose.production.yml up -d
6+
#
7+
# Or add to your .env:
8+
# COMPOSE_FILE=docker-compose.yml:docker-compose.production.yml
9+
10+
version: "3.9"
11+
12+
services:
13+
app:
14+
restart: always
15+
deploy:
16+
resources:
17+
limits:
18+
memory: 1g
19+
cpus: "1.0"
20+
logging:
21+
driver: "json-file"
22+
options:
23+
max-size: "50m"
24+
max-file: "5"
25+
healthcheck:
26+
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/api/health/ready || exit 1"]
27+
interval: 30s
28+
timeout: 10s
29+
retries: 3
30+
start_period: 30s
31+
environment:
32+
NODE_ENV: production
33+
34+
proxy:
35+
restart: always
36+
deploy:
37+
resources:
38+
limits:
39+
memory: 256m
40+
cpus: "0.5"
41+
logging:
42+
driver: "json-file"
43+
options:
44+
max-size: "20m"
45+
max-file: "3"
46+
47+
db:
48+
restart: always
49+
deploy:
50+
resources:
51+
limits:
52+
memory: 512m
53+
# Tune PostgreSQL for production
54+
command: >
55+
postgres
56+
-c max_connections=100
57+
-c shared_buffers=128MB
58+
-c effective_cache_size=384MB
59+
-c maintenance_work_mem=64MB
60+
-c checkpoint_completion_target=0.9
61+
-c wal_buffers=16MB
62+
-c default_statistics_target=100
63+
-c random_page_cost=1.1
64+
-c effective_io_concurrency=200
65+
-c work_mem=2621kB
66+
-c min_wal_size=1GB
67+
-c max_wal_size=4GB
68+
-c log_min_duration_statement=200
69+
logging:
70+
driver: "json-file"
71+
options:
72+
max-size: "20m"
73+
max-file: "3"
74+
75+
redis:
76+
restart: always
77+
deploy:
78+
resources:
79+
limits:
80+
memory: 128m
81+
# Enable persistence so rate limit / session data survives restarts
82+
command: >
83+
redis-server
84+
--appendonly yes
85+
--appendfsync everysec
86+
--maxmemory 100mb
87+
--maxmemory-policy allkeys-lru
88+
--save 900 1
89+
--save 300 10
90+
logging:
91+
driver: "json-file"
92+
options:
93+
max-size: "10m"
94+
max-file: "2"
95+
96+
minio:
97+
restart: always
98+
deploy:
99+
resources:
100+
limits:
101+
memory: 512m
102+
logging:
103+
driver: "json-file"
104+
options:
105+
max-size: "20m"
106+
max-file: "3"

0 commit comments

Comments
 (0)