feat(deploy): konteyner başlatma adımı ve sağlık kontrolünde replica set
- scripts/bootstrap.ts: replica set/PRIMARY kontrolü, migration'lar, production'da ilk admin - esbuild ile tek dosya (dist/bootstrap.mjs), docker-entrypoint.sh ile uygulamadan önce - /api/health yanıtında replicaSet alanı; healthcheck başlangıç payı 60 sn Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
node_modules
|
||||
.next
|
||||
out
|
||||
dist
|
||||
coverage
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
@@ -46,3 +46,4 @@ next-env.d.ts
|
||||
|
||||
# local tooling
|
||||
/scratch/
|
||||
/dist
|
||||
|
||||
@@ -5,3 +5,4 @@ coverage
|
||||
uploads
|
||||
next-env.d.ts
|
||||
docs/PROMPT.md
|
||||
dist/
|
||||
|
||||
+8
-3
@@ -2,6 +2,7 @@
|
||||
# BÜTÇEM — çok aşamalı üretim imajı (Next.js standalone çıktı).
|
||||
# Build: docker build --build-arg APP_COMMIT=$(git rev-parse --short HEAD) -t butcem .
|
||||
# Çalıştır: docker run -p 3000:3000 --env-file .env.production -v butcem-uploads:/app/uploads butcem
|
||||
# Başlarken: replica set kontrolü, bekleyen migration'lar, ilk admin (docker-entrypoint.sh).
|
||||
|
||||
ARG NODE_VERSION=26
|
||||
|
||||
@@ -22,7 +23,8 @@ ENV NEXT_TELEMETRY_DISABLED=1 \
|
||||
APP_COMMIT=${APP_COMMIT}
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
# Uygulama + başlatma betiği (migration ve ilk admin; tek dosya, tsx gerekmez).
|
||||
RUN npm run build && npm run build:scripts
|
||||
|
||||
# 3) Çalışma zamanı — yalnızca standalone çıktı, root olmayan kullanıcı.
|
||||
FROM node:${NODE_VERSION}-alpine AS runner
|
||||
@@ -43,12 +45,15 @@ RUN addgroup -S -g 1001 nodejs && adduser -S -u 1001 -G nodejs nextjs \
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/dist/bootstrap.mjs ./scripts/bootstrap.mjs
|
||||
COPY --chown=nextjs:nodejs --chmod=755 docker-entrypoint.sh ./docker-entrypoint.sh
|
||||
|
||||
USER nextjs
|
||||
VOLUME ["/app/uploads"]
|
||||
EXPOSE 3000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||
# start-period: ilk deploy'da migration'lar için pay.
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
|
||||
CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
# BÜTÇEM konteyner girişi: önce başlatma (replica set kontrolü, migration'lar, ilk admin),
|
||||
# başarılıysa uygulama. Başlatma başarısız olursa konteyner durur; Dokploy günlüğünde sebep görünür.
|
||||
# Acil durumda atlamak için: SKIP_BOOTSTRAP=1
|
||||
set -e
|
||||
if [ "${SKIP_BOOTSTRAP:-0}" != "1" ]; then
|
||||
node /app/scripts/bootstrap.mjs
|
||||
fi
|
||||
exec node /app/server.js
|
||||
@@ -20,6 +20,8 @@ const eslintConfig = defineConfig([
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
// Derlenmiş başlatma betiği (esbuild paketi).
|
||||
"dist/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "butcem",
|
||||
"version": "0.14.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "BÜTÇEM — aile bütçe, cari, birikim, ev stoğu ve akıllı alışveriş uygulaması",
|
||||
"engines": {
|
||||
@@ -23,6 +23,8 @@
|
||||
"icons": "tsx scripts/generate-icons.ts",
|
||||
"test:integration": "vitest run --config vitest.integration.config.mts",
|
||||
"migrate": "tsx --conditions=react-server --env-file=.env.local scripts/migrate.ts",
|
||||
"bootstrap": "tsx --conditions=react-server --env-file=.env.local scripts/bootstrap.ts",
|
||||
"build:scripts": "esbuild scripts/bootstrap.ts --bundle --platform=node --format=esm --target=node22 --conditions=react-server --outfile=dist/bootstrap.mjs --external:sharp --external:kerberos --external:snappy --external:socks --external:aws4 --external:@mongodb-js/zstd --external:mongodb-client-encryption --external:gcp-metadata --external:@aws-sdk/credential-providers --banner:js=\"import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);\" --log-level=warning",
|
||||
"cleanup": "tsx --conditions=react-server --env-file=.env.local scripts/cleanup.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Canlı başlatma (Docker entrypoint, uygulamadan önce): replica set kontrolü, bekleyen migration'lar
|
||||
* ve SEED_ADMIN_* tanımlıysa ilk admin hesabı (varsa dokunulmaz). İdempotent; her deploy'da çalışır.
|
||||
* İmajda esbuild ile tek dosyaya derlenir (scripts/bootstrap.mjs). Lokal: npm run bootstrap
|
||||
*/
|
||||
import mongoose from "mongoose";
|
||||
import { getDb, getMongoClient } from "@/lib/db/mongo";
|
||||
import { env } from "@/lib/env";
|
||||
import { runMigrations } from "./migrations";
|
||||
import { closeSeedConnections, runSeed } from "./seed/run";
|
||||
|
||||
async function main() {
|
||||
const e = env();
|
||||
const hello = await getDb().admin().command({ hello: 1 });
|
||||
if (!hello.setName) {
|
||||
throw new Error(
|
||||
"MongoDB replica set olarak çalışmıyor; transaction'lı yazmalar başarısız olur. " +
|
||||
"docs/DEPLOY.md › 'MongoDB replica set' adımlarını uygulayın.",
|
||||
);
|
||||
}
|
||||
if (!hello.isWritablePrimary) throw new Error("MongoDB düğümü PRIMARY değil (yazılamaz).");
|
||||
console.log(`• MongoDB ${hello.setName} PRIMARY`);
|
||||
|
||||
if (e.NODE_ENV === "production" && e.SEED_ADMIN_EMAIL && e.SEED_ADMIN_PASSWORD) {
|
||||
// Production'da seed yalnızca migration + admin yapar (örnek veri oluşturulmaz).
|
||||
await runSeed();
|
||||
} else {
|
||||
await runMigrations();
|
||||
if (e.NODE_ENV === "production")
|
||||
console.log("• SEED_ADMIN_* tanımlı değil; admin kontrolü atlandı.");
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((err: unknown) => {
|
||||
console.error(`✗ Başlatma hatası: ${err instanceof Error ? err.message : err}`);
|
||||
process.exitCode = 1;
|
||||
})
|
||||
.finally(async () => {
|
||||
await closeSeedConnections();
|
||||
await mongoose.disconnect();
|
||||
await getMongoClient().close();
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { connection } from "next/server";
|
||||
import { pingDb } from "@/lib/db/mongo";
|
||||
import { getDb, pingDb } from "@/lib/db/mongo";
|
||||
import { APP_COMMIT, APP_VERSION } from "@/lib/version";
|
||||
|
||||
/**
|
||||
@@ -10,10 +10,21 @@ export async function GET() {
|
||||
// Cache Components açıkken GET handler'ı build'de prerender edilmesin.
|
||||
await connection();
|
||||
const dbOk = await pingDb();
|
||||
// Transaction'lar için replica set şart; adı yoksa (standalone) null.
|
||||
let replicaSet: string | null = null;
|
||||
if (dbOk) {
|
||||
try {
|
||||
const hello = await getDb().admin().command({ hello: 1 }, { timeoutMS: 2_000 });
|
||||
replicaSet = typeof hello.setName === "string" ? hello.setName : null;
|
||||
} catch {
|
||||
replicaSet = null;
|
||||
}
|
||||
}
|
||||
return Response.json(
|
||||
{
|
||||
status: dbOk ? "ok" : "degraded",
|
||||
db: dbOk ? "ok" : "unreachable",
|
||||
replicaSet,
|
||||
version: APP_VERSION,
|
||||
commit: APP_COMMIT,
|
||||
time: new Date().toISOString(),
|
||||
|
||||
Reference in New Issue
Block a user