From 4c77b967ffebae1366d93e3ca39b2641e35157e0 Mon Sep 17 00:00:00 2001 From: isra el Date: Sun, 30 Mar 2025 09:59:49 +0300 Subject: [PATCH] infra: fix docker issues --- api/.env.example | 15 ++- api/Dockerfile | 72 +++++++++--- api/src/main.ts | 9 +- api/src/users/schemas/user.schema.ts | 2 +- docker-compose.yaml | 163 ++++++++++++++++++++------- web/.dockerignore | 2 + web/.env.example | 2 +- web/Dockerfile | 90 ++++++++++++--- web/lib/auth.ts | 8 +- web/lib/httpServerClient.ts | 14 ++- 10 files changed, 291 insertions(+), 86 deletions(-) create mode 100644 web/.dockerignore diff --git a/api/.env.example b/api/.env.example index 97f3fa2..e3f0d53 100644 --- a/api/.env.example +++ b/api/.env.example @@ -1,11 +1,16 @@ -PORT=3005 -MONGO_URI=mongodb://textbeeUser:textbeePassword@mongo:27017/TextBee -JWT_SECRET=secret +PORT=3001 +MONGO_URI=mongodb://adminUser:adminPassword@textbee-db:27017/textbee?authSource=admin + +# to setup initial password +MONGO_ROOT_USER=adminUser +MONGO_ROOT_PASS=adminPassword + +JWT_SECRET=secret # change this to a secure random string JWT_EXPIRATION=60d FRONTEND_URL=http://localhost:3000 -#Update from Firebase json file +#Update from Firebase service account json file FIREBASE_PROJECT_ID= FIREBASE_PRIVATE_KEY_ID= FIREBASE_PRIVATE_KEY= @@ -18,7 +23,7 @@ MAIL_PORT= MAIL_USER= MAIL_PASS= MAIL_FROM= -MAIL_REPLY_TO=textbee.dev@gmail.com +MAIL_REPLY_TO= # SMS Queue Configuration USE_SMS_QUEUE=false diff --git a/api/Dockerfile b/api/Dockerfile index 2b56f82..5675807 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,25 +1,61 @@ -FROM node:18-alpine AS base -RUN npm i -g pnpm +# Stage 1: Dependencies +FROM node:22-alpine AS deps WORKDIR /app + +# Install pnpm +RUN corepack enable && corepack prepare pnpm@latest --activate + +# Copy package.json and pnpm-lock.yaml COPY package.json pnpm-lock.yaml ./ -RUN pnpm i -COPY . . -FROM base AS dev -ENV NODE_ENV=development -ENTRYPOINT ["pnpm", "start:dev"] +# Install dependencies +RUN pnpm install --frozen-lockfile + +# Stage 2: Builder +FROM node:22-alpine AS builder +WORKDIR /app + +# Install pnpm +RUN corepack enable && corepack prepare pnpm@latest --activate + +# Copy dependencies from deps stage +COPY --from=deps /app/node_modules ./node_modules +COPY . . -FROM base AS build -ENV NODE_ENV=production +# Build the application RUN pnpm build -FROM node:18-alpine AS prod -ENV NODE_ENV=production -EXPOSE 3005 +# Stage 3: Production +FROM node:22-alpine AS runner WORKDIR /app -RUN npm i -g pnpm -COPY --from=build /app/.env ./.env -COPY --from=build /app/dist ./dist -COPY --from=build /app/package.json /app/pnpm-lock.yaml ./ -RUN pnpm i --prod -ENTRYPOINT ["pnpm", "start"] + +# Install pnpm +RUN corepack enable && corepack prepare pnpm@latest --activate + +# Set NODE_ENV to production +ENV NODE_ENV production + +# Copy necessary files for production +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/package.json ./ +COPY --from=builder /app/pnpm-lock.yaml ./ + +# Install only production dependencies +RUN pnpm install --prod --frozen-lockfile + +# Add a non-root user +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nestjs && \ + chown -R nestjs:nodejs /app +USER nestjs + +# Expose the port specified by the PORT environment variable (default: 3001) +ENV PORT 300 +EXPOSE ${PORT} + +# Health check to verify app is running +# HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ +# CMD wget -q -O - http://localhost:${PORT}/api/v1/health || exit 1 + +# Command to run the application +CMD ["node", "dist/main"] \ No newline at end of file diff --git a/api/src/main.ts b/api/src/main.ts index e092a72..87f9d30 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -4,11 +4,11 @@ import { NestFactory } from '@nestjs/core' import { AppModule } from './app.module' import * as firebase from 'firebase-admin' import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger' -import * as express from 'express'; +import * as express from 'express' async function bootstrap() { const app = await NestFactory.create(AppModule) - const PORT = process.env.PORT || 3005 + const PORT = process.env.PORT || 3001 app.setGlobalPrefix('api') app.enableVersioning({ @@ -51,7 +51,10 @@ async function bootstrap() { credential: firebase.credential.cert(firebaseConfig), }) - app.use('/api/v1/billing/webhook/polar', express.raw({ type: 'application/json' })); + app.use( + '/api/v1/billing/webhook/polar', + express.raw({ type: 'application/json' }), + ) app.enableCors() await app.listen(PORT) } diff --git a/api/src/users/schemas/user.schema.ts b/api/src/users/schemas/user.schema.ts index 430c929..2c88a06 100644 --- a/api/src/users/schemas/user.schema.ts +++ b/api/src/users/schemas/user.schema.ts @@ -14,7 +14,7 @@ export class User { @Prop({ type: String, required: true, unique: true, lowercase: true }) email: string - @Prop({ type: String, unique: true }) + @Prop({ type: String, unique: true, sparse: true }) googleId?: string @Prop({ type: String }) diff --git a/docker-compose.yaml b/docker-compose.yaml index 1b840ea..855bc2f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,56 +1,141 @@ + services: - web: - container_name: web - build: - context: ./web - dockerfile: Dockerfile + # MongoDB service + textbee-db: + container_name: textbee-db + image: mongo:latest + restart: always + environment: + - MONGO_INITDB_ROOT_USERNAME=${MONGO_ROOT_USER:-adminUser} + - MONGO_INITDB_ROOT_PASSWORD=${MONGO_ROOT_PASS:-adminPassword} + - MONGO_INITDB_DATABASE=textbee + # - MONGO_DB_USERNAME=${MONGO_USER:-textbeeUser} + # - MONGO_DB_PASSWORD=${MONGO_PASS:-textbeePassword} + volumes: + # - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro + - mongodb_data:/data/db ports: - - "3000:3000" - depends_on: - - mongo + # only allow access from the same machine, and use port 27018 to avoid conflict with default mongo port 27017 + # - "127.0.0.1:${MONGO_PORT:-27018}:27017" + - "${MONGO_PORT:-27018}:27017" + networks: + - textbee-network + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 20s + + # MongoDB Express (optional admin UI) + mongo-express: + container_name: textbee-mongo-express + image: mongo-express:latest + restart: always + ports: + - "${MONGO_EXPRESS_PORT:-8081}:8081" environment: - NODE_ENV: production + - ME_CONFIG_MONGODB_ADMINUSERNAME=${MONGO_ROOT_USER:-adminUser} + - ME_CONFIG_MONGODB_ADMINPASSWORD=${MONGO_ROOT_PASS:-adminPassword} + - ME_CONFIG_MONGODB_SERVER=textbee-db + depends_on: + textbee-db: + condition: service_healthy + networks: + - textbee-network - api: - container_name: api + # NestJS API + textbee-api: + container_name: textbee-api build: context: ./api dockerfile: Dockerfile - target: prod + restart: always ports: - - "3005:3005" - depends_on: - - mongo + - "${PORT:-3001}:3001" + env_file: + - ./api/.env environment: - NODE_ENV: production + - PORT=${PORT:-3001} + # - MONGO_URI=${MONGO_URI:-mongodb://${MONGO_USER:-textbeeUser}:${MONGO_PASS:-textbeePassword}@textbee-db:27018/TextBee} + # - MONGO_URI=mongodb://adminUser:adminPassword@textbee-db:27018/textbee + # - FRONTEND_URL=${NEXT_PUBLIC_SITE_URL:-http://localhost:3000} + # - JWT_SECRET=${JWT_SECRET:-your_jwt_secret_here} + # - JWT_EXPIRATION=${JWT_EXPIRATION:-60d} + # - MAIL_HOST=${MAIL_HOST} + # - MAIL_PORT=${MAIL_PORT} + # - MAIL_USER=${MAIL_USER} + # - MAIL_PASS=${MAIL_PASS} + # - MAIL_FROM=${MAIL_FROM} + # - USE_SMS_QUEUE=${USE_SMS_QUEUE:-false} + # - REDIS_HOST=${REDIS_HOST:-redis} + # - REDIS_PORT=${REDIS_PORT:-6379} + # - FIREBASE_PROJECT_ID=${FIREBASE_PROJECT_ID} + # - FIREBASE_PRIVATE_KEY_ID=${FIREBASE_PRIVATE_KEY_ID} + # - FIREBASE_PRIVATE_KEY=${FIREBASE_PRIVATE_KEY} + # - FIREBASE_CLIENT_EMAIL=${FIREBASE_CLIENT_EMAIL} + # - FIREBASE_CLIENT_ID=${FIREBASE_CLIENT_ID} + # - FIREBASE_CLIENT_C509_CERT_URL=${FIREBASE_CLIENT_C509_CERT_URL} + depends_on: + textbee-db: + condition: service_healthy + networks: + - textbee-network - mongo: - container_name: mongo - image: mongo + # Next.js Web + textbee-web: + container_name: textbee-web + build: + context: ./web + dockerfile: Dockerfile restart: always ports: - - "27017:27017" + - "${PORT:-3000}:3000" + env_file: + - ./web/.env environment: - MONGO_INITDB_ROOT_USERNAME: adminUser - MONGO_INITDB_ROOT_PASSWORD: adminPassword - MONGO_INITDB_DATABASE: TextBee - volumes: - - textbee-db-data:/data/db - # THe following scripts creates TextBee DB automatically, also the user which web and api are connecting with. - - ./mongo-init:/docker-entrypoint-initdb.d:ro - mongo-express: - container_name: mongo-ee - image: mongo-express + - PORT=${PORT:-3000} + # - NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL:-http://localhost:3000} + - NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL:-http://localhost:3001/api/v1} + # - AUTH_SECRET=${AUTH_SECRET:-generate_a_secure_random_string_here} + # - DATABASE_URL=mongodb://adminUser:adminPassword@textbee-db:27018/textbee + # - DATABASE_URL=mongodb://adminUser:adminPassword@textbee-db:27018/textbee + # - MAIL_HOST=${MAIL_HOST} + # - MAIL_PORT=${MAIL_PORT} + # - MAIL_USER=${MAIL_USER} + # - MAIL_PASS=${MAIL_PASS} + # - MAIL_FROM=${MAIL_FROM} + # - ADMIN_EMAIL=${ADMIN_EMAIL} + # - NEXT_PUBLIC_GOOGLE_CLIENT_ID=${NEXT_PUBLIC_GOOGLE_CLIENT_ID} + # - NEXT_PUBLIC_TAWKTO_EMBED_URL=${NEXT_PUBLIC_TAWKTO_EMBED_URL} + depends_on: + - textbee-api + networks: + - textbee-network + + # Redis (if SMS queue is needed) + redis: + container_name: textbee-redis + image: redis:alpine restart: always ports: - - "8081:8081" - environment: - ME_CONFIG_MONGODB_ADMINUSERNAME: adminUser - ME_CONFIG_MONGODB_ADMINPASSWORD: adminPassword - ME_CONFIG_MONGODB_URL: mongodb://adminUser:adminPassword@mongo:27017/ - ME_CONFIG_BASICAUTH: "false" - depends_on: - - mongo + - "${REDIS_PORT:-6379}:6379" + volumes: + - redis_data:/data + networks: + - textbee-network + command: redis-server --appendonly yes + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 20s + +networks: + textbee-network: + driver: bridge volumes: - textbee-db-data: + mongodb_data: + redis_data: \ No newline at end of file diff --git a/web/.dockerignore b/web/.dockerignore new file mode 100644 index 0000000..651665b --- /dev/null +++ b/web/.dockerignore @@ -0,0 +1,2 @@ +node_modules +.git diff --git a/web/.env.example b/web/.env.example index 60a1344..822331f 100644 --- a/web/.env.example +++ b/web/.env.example @@ -1,5 +1,5 @@ NEXT_PUBLIC_SITE_URL=http://localhost:3000 -NEXT_PUBLIC_API_BASE_URL=http://localhost:3005/api/v1 +NEXT_PUBLIC_API_BASE_URL=http://localhost:3001/api/v1 NEXT_PUBLIC_GOOGLE_CLIENT_ID= NEXT_PUBLIC_TAWKTO_EMBED_URL= diff --git a/web/Dockerfile b/web/Dockerfile index 96b44d2..d725235 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -1,20 +1,82 @@ -FROM node:18-alpine AS base - -# Stage 1: Install web dependencies -FROM base AS web-deps +# Stage 1: Dependencies +FROM node:22-alpine AS deps WORKDIR /app -COPY .env ./.env + +# Install pnpm and required OpenSSL dependencies +RUN apk add --no-cache openssl openssl-dev +RUN corepack enable && corepack prepare pnpm@latest --activate + +# Copy package.json and pnpm-lock.yaml COPY package.json pnpm-lock.yaml ./ -RUN corepack enable pnpm && pnpm install --frozen-lockfile -# Stage 2: Build the web application -FROM base AS web-builder -ENV NODE_ENV=production -EXPOSE 3000 +# Install dependencies +RUN pnpm install --frozen-lockfile + +# Stage 2: Builder +FROM node:22-alpine AS builder WORKDIR /app + +# Install pnpm and required OpenSSL dependencies +RUN apk add --no-cache openssl openssl-dev +RUN corepack enable && corepack prepare pnpm@latest --activate + +# Copy dependencies from deps stage +COPY --from=deps /app/node_modules ./node_modules + +# Copy all files COPY . . -COPY --from=web-deps /app/node_modules ./node_modules -COPY --from=web-deps /app/.env .env -RUN corepack enable pnpm && pnpm run vercel-build -CMD ["pnpm", "start"] +# Set environment variables for building +ENV NEXT_TELEMETRY_DISABLED 1 + +# Generate prisma client - make sure it exists +RUN pnpm prisma generate + +# Build the application +RUN pnpm build + +# Stage 3: Production runner +FROM node:22-alpine AS runner +WORKDIR /app + +# Install pnpm and required OpenSSL dependencies +RUN apk add --no-cache openssl openssl-dev +RUN corepack enable && corepack prepare pnpm@latest --activate + +# Set environment variables +ENV NODE_ENV production +ENV NEXT_TELEMETRY_DISABLED 1 +ENV CONTAINER_RUNTIME docker + +# Add a non-root user to run the app +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs && \ + chown -R nextjs:nodejs /app + +# Copy necessary files for the standalone app +COPY --from=builder --chown=nextjs:nodejs /app/next.config.js ./ +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 Prisma schema and generate client during runtime +COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma +COPY --from=builder --chown=nextjs:nodejs /app/package.json ./ +COPY --from=builder --chown=nextjs:nodejs /app/pnpm-lock.yaml ./ + +# Install only production dependencies, including Prisma, and generate Prisma client +RUN pnpm install --prod --frozen-lockfile && \ + pnpm prisma generate + +# Switch to non-root user +USER nextjs + +# Expose the port the app will run on +ENV PORT 3000 +EXPOSE ${PORT} + +# Health check to verify app is running +# HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ +# CMD wget -q -O - http://localhost:${PORT}/api/health || exit 1 + +CMD ["node", "server.js"] \ No newline at end of file diff --git a/web/lib/auth.ts b/web/lib/auth.ts index 0b05b16..ff7aa9f 100644 --- a/web/lib/auth.ts +++ b/web/lib/auth.ts @@ -1,5 +1,5 @@ import CredentialsProvider from 'next-auth/providers/credentials' -import httpBrowserClient from './httpBrowserClient' +import { httpServerClient } from './httpServerClient' import { DefaultSession } from 'next-auth' import { ApiEndpoints } from '@/config/api' import { Routes } from '@/config/routes' @@ -31,7 +31,7 @@ export const authOptions = { async authorize(credentials) { const { email, password } = credentials try { - const res = await httpBrowserClient.post(ApiEndpoints.auth.login(), { + const res = await httpServerClient.post(ApiEndpoints.auth.login(), { email, password, }) @@ -62,7 +62,7 @@ export const authOptions = { async authorize(credentials) { const { email, password, name, phone } = credentials try { - const res = await httpBrowserClient.post( + const res = await httpServerClient.post( ApiEndpoints.auth.register(), { email, @@ -94,7 +94,7 @@ export const authOptions = { async authorize(credentials) { const { idToken } = credentials try { - const res = await httpBrowserClient.post( + const res = await httpServerClient.post( ApiEndpoints.auth.signInWithGoogle(), { idToken, diff --git a/web/lib/httpServerClient.ts b/web/lib/httpServerClient.ts index e1a663d..d4c6177 100644 --- a/web/lib/httpServerClient.ts +++ b/web/lib/httpServerClient.ts @@ -3,8 +3,20 @@ import { getServerSession } from 'next-auth/next' import { authOptions } from '@/lib/auth' import { Session } from 'next-auth' +// Create a base URL that works in Docker container network if running in a container +// or falls back to the public URL if not in a container +const getServerSideBaseUrl = () => { + // When running server-side in Docker, use the service name from docker-compose + if (process.env.CONTAINER_RUNTIME === 'docker') { + console.log('Running in Docker container') + return 'http://textbee-api:3001/api/v1' + } + // Otherwise use the public URL + return process.env.NEXT_PUBLIC_API_BASE_URL || '' +} + export const httpServerClient = axios.create({ - baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || '', + baseURL: getServerSideBaseUrl(), }) httpServerClient.interceptors.request.use(async (config) => {