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
|
node_modules
|
||||||
.next
|
.next
|
||||||
out
|
out
|
||||||
|
dist
|
||||||
coverage
|
coverage
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|||||||
@@ -46,3 +46,4 @@ next-env.d.ts
|
|||||||
|
|
||||||
# local tooling
|
# local tooling
|
||||||
/scratch/
|
/scratch/
|
||||||
|
/dist
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ coverage
|
|||||||
uploads
|
uploads
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
docs/PROMPT.md
|
docs/PROMPT.md
|
||||||
|
dist/
|
||||||
|
|||||||
+8
-3
@@ -2,6 +2,7 @@
|
|||||||
# BÜTÇEM — çok aşamalı üretim imajı (Next.js standalone çıktı).
|
# 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 .
|
# 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
|
# Ç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
|
ARG NODE_VERSION=26
|
||||||
|
|
||||||
@@ -22,7 +23,8 @@ ENV NEXT_TELEMETRY_DISABLED=1 \
|
|||||||
APP_COMMIT=${APP_COMMIT}
|
APP_COMMIT=${APP_COMMIT}
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
COPY . .
|
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ı.
|
# 3) Çalışma zamanı — yalnızca standalone çıktı, root olmayan kullanıcı.
|
||||||
FROM node:${NODE_VERSION}-alpine AS runner
|
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/public ./public
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
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/.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
|
USER nextjs
|
||||||
VOLUME ["/app/uploads"]
|
VOLUME ["/app/uploads"]
|
||||||
EXPOSE 3000
|
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 -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/**",
|
".next/**",
|
||||||
"out/**",
|
"out/**",
|
||||||
"build/**",
|
"build/**",
|
||||||
|
// Derlenmiş başlatma betiği (esbuild paketi).
|
||||||
|
"dist/**",
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "butcem",
|
"name": "butcem",
|
||||||
"version": "0.14.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "BÜTÇEM — aile bütçe, cari, birikim, ev stoğu ve akıllı alışveriş uygulaması",
|
"description": "BÜTÇEM — aile bütçe, cari, birikim, ev stoğu ve akıllı alışveriş uygulaması",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -23,6 +23,8 @@
|
|||||||
"icons": "tsx scripts/generate-icons.ts",
|
"icons": "tsx scripts/generate-icons.ts",
|
||||||
"test:integration": "vitest run --config vitest.integration.config.mts",
|
"test:integration": "vitest run --config vitest.integration.config.mts",
|
||||||
"migrate": "tsx --conditions=react-server --env-file=.env.local scripts/migrate.ts",
|
"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"
|
"cleanup": "tsx --conditions=react-server --env-file=.env.local scripts/cleanup.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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 { 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";
|
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.
|
// Cache Components açıkken GET handler'ı build'de prerender edilmesin.
|
||||||
await connection();
|
await connection();
|
||||||
const dbOk = await pingDb();
|
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(
|
return Response.json(
|
||||||
{
|
{
|
||||||
status: dbOk ? "ok" : "degraded",
|
status: dbOk ? "ok" : "degraded",
|
||||||
db: dbOk ? "ok" : "unreachable",
|
db: dbOk ? "ok" : "unreachable",
|
||||||
|
replicaSet,
|
||||||
version: APP_VERSION,
|
version: APP_VERSION,
|
||||||
commit: APP_COMMIT,
|
commit: APP_COMMIT,
|
||||||
time: new Date().toISOString(),
|
time: new Date().toISOString(),
|
||||||
|
|||||||
Reference in New Issue
Block a user