diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e99e150 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,68 @@ +# ═══════════════════════════════════════════════════════════════ +# Oltalama - Single Container (Backend + Frontend) +# ═══════════════════════════════════════════════════════════════ + +# Stage 1: Build Frontend +FROM node:20-alpine AS frontend-builder + +WORKDIR /app/frontend + +# Copy frontend files +COPY frontend/package*.json ./ +RUN npm ci + +COPY frontend/ ./ +RUN npm run build + +# Stage 2: Production (Backend + Frontend) +FROM node:20-alpine + +# Install tini for proper signal handling +RUN apk add --no-cache tini + +# Create app user +RUN addgroup -g 1001 -S oltalama && \ + adduser -S -u 1001 -G oltalama oltalama + +WORKDIR /app + +# Copy backend package files +COPY backend/package*.json ./backend/ +WORKDIR /app/backend + +# Install backend dependencies (production only) +RUN if [ -f package-lock.json ]; then \ + npm ci --omit=dev; \ + else \ + npm install --only=production; \ + fi + +# Copy backend source +WORKDIR /app +COPY backend/ ./backend/ + +# Copy frontend build from builder stage +COPY --from=frontend-builder /app/frontend/dist ./frontend/dist + +# Copy entrypoint script +COPY docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +# Create necessary directories +RUN mkdir -p /app/backend/database /app/backend/logs && \ + chown -R oltalama:oltalama /app + +# Switch to non-root user +USER oltalama + +# Expose port +EXPOSE 3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" + +# Use tini as entrypoint +ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/docker-entrypoint.sh"] +CMD ["node", "backend/src/app.js"] + diff --git a/backend/src/app.js b/backend/src/app.js index 47cd9f2..2006ff9 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -100,6 +100,23 @@ app.use(express.urlencoded({ extended: true })); // Serve static files (landing page) app.use(express.static('src/public')); +// Serve frontend build files (if exists, for production) +const path = require('path'); +const frontendDistPath = path.join(__dirname, '../../frontend/dist'); +const fs = require('fs'); +if (fs.existsSync(frontendDistPath)) { + app.use(express.static(frontendDistPath)); + + // SPA fallback: serve index.html for all non-API routes + app.get('*', (req, res, next) => { + // Skip API routes and tracking routes + if (req.path.startsWith('/api') || req.path.startsWith('/t/') || req.path.startsWith('/health')) { + return next(); + } + res.sendFile(path.join(frontendDistPath, 'index.html')); + }); +} + // Session middleware app.use(session(sessionConfig)); diff --git a/docker-compose.single.yml b/docker-compose.single.yml new file mode 100644 index 0000000..1f38525 --- /dev/null +++ b/docker-compose.single.yml @@ -0,0 +1,54 @@ +services: + oltalama: + build: + context: . + dockerfile: Dockerfile + container_name: oltalama + restart: unless-stopped + ports: + - "3000:3000" + environment: + - NODE_ENV=production + - PORT=3000 + - SESSION_SECRET=${SESSION_SECRET:-} # Boşsa otomatik oluşturulur + - AUTO_SEED=${AUTO_SEED:-false} # true yaparsanız örnek data eklenir + # Gmail ayarları (ZORUNLU - .env'den veya buradan) + - GMAIL_USER=${GMAIL_USER:-} + - GMAIL_APP_PASSWORD=${GMAIL_APP_PASSWORD:-} + # Telegram ayarları (ZORUNLU - .env'den veya buradan) + - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-} + - TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-} + # Ollama ayarları (Opsiyonel) + - OLLAMA_SERVER_URL=${OLLAMA_SERVER_URL:-http://host.docker.internal:11434} + - OLLAMA_MODEL=${OLLAMA_MODEL:-llama3.2:latest} + # Domain ayarları (Opsiyonel) + - DOMAIN_URL=${DOMAIN_URL:-} + - FRONTEND_URL=${FRONTEND_URL:-} + volumes: + # Database persistence + - oltalama-db:/app/backend/database + # Logs persistence + - oltalama-logs:/app/backend/logs + # .env persistence (SESSION_SECRET için) + - oltalama-env:/app/backend + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 40s + networks: + - oltalama-network + +volumes: + oltalama-db: + driver: local + oltalama-logs: + driver: local + oltalama-env: + driver: local + +networks: + oltalama-network: + driver: bridge + diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..34e7df2 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,84 @@ +#!/bin/sh +set -e + +# Docker Entrypoint Script - Single Container (Backend + Frontend) +# Otomatik SESSION_SECRET oluşturma ve yönetimi + +echo "🚀 Oltalama başlatılıyor (Backend + Frontend)..." + +# SESSION_SECRET kontrolü ve otomatik oluşturma +if [ -z "$SESSION_SECRET" ] || [ "$SESSION_SECRET" = "change-this-to-a-very-strong-random-secret" ]; then + echo "⚠️ SESSION_SECRET boş veya varsayılan değerde!" + + # .env dosyası varsa SESSION_SECRET'ı oradan al + if [ -f "/app/backend/.env" ]; then + SESSION_SECRET_FROM_FILE=$(grep "^SESSION_SECRET=" /app/backend/.env | cut -d'=' -f2-) + if [ -n "$SESSION_SECRET_FROM_FILE" ] && [ "$SESSION_SECRET_FROM_FILE" != "change-this-to-a-very-strong-random-secret" ]; then + export SESSION_SECRET="$SESSION_SECRET_FROM_FILE" + echo "✅ SESSION_SECRET .env dosyasından yüklendi" + fi + fi + + # Hala boşsa otomatik oluştur + if [ -z "$SESSION_SECRET" ] || [ "$SESSION_SECRET" = "change-this-to-a-very-strong-random-secret" ]; then + echo "🔑 Yeni SESSION_SECRET otomatik oluşturuluyor..." + + # Node.js ile güçlü random secret oluştur + SESSION_SECRET=$(node -e "console.log(require('crypto').randomBytes(64).toString('hex'))") + export SESSION_SECRET + + # Session secret'ı .env dosyasına kaydet (persist) + if [ ! -f "/app/backend/.env" ]; then + mkdir -p /app/backend + echo "SESSION_SECRET=$SESSION_SECRET" > /app/backend/.env + echo "✅ Yeni SESSION_SECRET oluşturuldu ve .env dosyasına kaydedildi" + echo "📝 SESSION_SECRET: ${SESSION_SECRET:0:20}... (ilk 20 karakter)" + else + # Mevcut .env dosyasını güncelle + if grep -q "^SESSION_SECRET=" /app/backend/.env; then + sed -i "s|^SESSION_SECRET=.*|SESSION_SECRET=$SESSION_SECRET|" /app/backend/.env + else + echo "SESSION_SECRET=$SESSION_SECRET" >> /app/backend/.env + fi + echo "✅ SESSION_SECRET güncellendi ve .env dosyasına kaydedildi" + fi + fi +else + echo "✅ SESSION_SECRET zaten ayarlanmış" +fi + +# Database dizinini kontrol et +if [ ! -d "/app/backend/database" ]; then + echo "📁 Database dizini oluşturuluyor..." + mkdir -p /app/backend/database +fi + +# Logs dizinini kontrol et +if [ ! -d "/app/backend/logs" ]; then + echo "📁 Logs dizini oluşturuluyor..." + mkdir -p /app/backend/logs +fi + +# Database migration'ları çalıştır (ilk kurulumda) +if [ ! -f "/app/backend/database/oltalama.db" ]; then + echo "🗄️ İlk kurulum tespit edildi, database oluşturuluyor..." + cd /app/backend + if [ -f "migrations/run-migrations.js" ]; then + node migrations/run-migrations.js || echo "⚠️ Migration hatası (normal olabilir)" + fi + + # Seed data (opsiyonel) + if [ "${AUTO_SEED}" = "true" ] && [ -f "seeders/run-seeders.js" ]; then + echo "🌱 Seed data ekleniyor..." + node seeders/run-seeders.js || echo "⚠️ Seeding hatası (normal olabilir)" + fi +else + echo "✅ Database mevcut, migration atlanıyor" +fi + +echo "✅ Oltalama hazır, uygulama başlatılıyor..." +echo "" + +# CMD komutunu çalıştır (exec ile PID 1'e geç) +exec "$@" +