Browse Source

infra: fix docker issues

pull/63/head
isra el 11 months ago
parent
commit
4c77b967ff
  1. 15
      api/.env.example
  2. 72
      api/Dockerfile
  3. 9
      api/src/main.ts
  4. 2
      api/src/users/schemas/user.schema.ts
  5. 163
      docker-compose.yaml
  6. 2
      web/.dockerignore
  7. 2
      web/.env.example
  8. 90
      web/Dockerfile
  9. 8
      web/lib/auth.ts
  10. 14
      web/lib/httpServerClient.ts

15
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 JWT_EXPIRATION=60d
FRONTEND_URL=http://localhost:3000 FRONTEND_URL=http://localhost:3000
#Update from Firebase json file
#Update from Firebase service account json file
FIREBASE_PROJECT_ID= FIREBASE_PROJECT_ID=
FIREBASE_PRIVATE_KEY_ID= FIREBASE_PRIVATE_KEY_ID=
FIREBASE_PRIVATE_KEY= FIREBASE_PRIVATE_KEY=
@ -18,7 +23,7 @@ MAIL_PORT=
MAIL_USER= MAIL_USER=
MAIL_PASS= MAIL_PASS=
MAIL_FROM= MAIL_FROM=
MAIL_REPLY_TO=textbee.dev@gmail.com
MAIL_REPLY_TO=
# SMS Queue Configuration # SMS Queue Configuration
USE_SMS_QUEUE=false USE_SMS_QUEUE=false

72
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 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 ./ 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 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 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"]

9
api/src/main.ts

@ -4,11 +4,11 @@ import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module' import { AppModule } from './app.module'
import * as firebase from 'firebase-admin' import * as firebase from 'firebase-admin'
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger' import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'
import * as express from 'express';
import * as express from 'express'
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule) const app = await NestFactory.create(AppModule)
const PORT = process.env.PORT || 3005
const PORT = process.env.PORT || 3001
app.setGlobalPrefix('api') app.setGlobalPrefix('api')
app.enableVersioning({ app.enableVersioning({
@ -51,7 +51,10 @@ async function bootstrap() {
credential: firebase.credential.cert(firebaseConfig), 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() app.enableCors()
await app.listen(PORT) await app.listen(PORT)
} }

2
api/src/users/schemas/user.schema.ts

@ -14,7 +14,7 @@ export class User {
@Prop({ type: String, required: true, unique: true, lowercase: true }) @Prop({ type: String, required: true, unique: true, lowercase: true })
email: string email: string
@Prop({ type: String, unique: true })
@Prop({ type: String, unique: true, sparse: true })
googleId?: string googleId?: string
@Prop({ type: String }) @Prop({ type: String })

163
docker-compose.yaml

@ -1,56 +1,141 @@
services: 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: 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: 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: build:
context: ./api context: ./api
dockerfile: Dockerfile dockerfile: Dockerfile
target: prod
restart: always
ports: ports:
- "3005:3005"
depends_on:
- mongo
- "${PORT:-3001}:3001"
env_file:
- ./api/.env
environment: 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 restart: always
ports: ports:
- "27017:27017"
- "${PORT:-3000}:3000"
env_file:
- ./web/.env
environment: 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 restart: always
ports: 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: volumes:
textbee-db-data:
mongodb_data:
redis_data:

2
web/.dockerignore

@ -0,0 +1,2 @@
node_modules
.git

2
web/.env.example

@ -1,5 +1,5 @@
NEXT_PUBLIC_SITE_URL=http://localhost:3000 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_GOOGLE_CLIENT_ID=
NEXT_PUBLIC_TAWKTO_EMBED_URL= NEXT_PUBLIC_TAWKTO_EMBED_URL=

90
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 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 ./ 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 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 . .
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"]

8
web/lib/auth.ts

@ -1,5 +1,5 @@
import CredentialsProvider from 'next-auth/providers/credentials' import CredentialsProvider from 'next-auth/providers/credentials'
import httpBrowserClient from './httpBrowserClient'
import { httpServerClient } from './httpServerClient'
import { DefaultSession } from 'next-auth' import { DefaultSession } from 'next-auth'
import { ApiEndpoints } from '@/config/api' import { ApiEndpoints } from '@/config/api'
import { Routes } from '@/config/routes' import { Routes } from '@/config/routes'
@ -31,7 +31,7 @@ export const authOptions = {
async authorize(credentials) { async authorize(credentials) {
const { email, password } = credentials const { email, password } = credentials
try { try {
const res = await httpBrowserClient.post(ApiEndpoints.auth.login(), {
const res = await httpServerClient.post(ApiEndpoints.auth.login(), {
email, email,
password, password,
}) })
@ -62,7 +62,7 @@ export const authOptions = {
async authorize(credentials) { async authorize(credentials) {
const { email, password, name, phone } = credentials const { email, password, name, phone } = credentials
try { try {
const res = await httpBrowserClient.post(
const res = await httpServerClient.post(
ApiEndpoints.auth.register(), ApiEndpoints.auth.register(),
{ {
email, email,
@ -94,7 +94,7 @@ export const authOptions = {
async authorize(credentials) { async authorize(credentials) {
const { idToken } = credentials const { idToken } = credentials
try { try {
const res = await httpBrowserClient.post(
const res = await httpServerClient.post(
ApiEndpoints.auth.signInWithGoogle(), ApiEndpoints.auth.signInWithGoogle(),
{ {
idToken, idToken,

14
web/lib/httpServerClient.ts

@ -3,8 +3,20 @@ import { getServerSession } from 'next-auth/next'
import { authOptions } from '@/lib/auth' import { authOptions } from '@/lib/auth'
import { Session } from 'next-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({ export const httpServerClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || '',
baseURL: getServerSideBaseUrl(),
}) })
httpServerClient.interceptors.request.use(async (config) => { httpServerClient.interceptors.request.use(async (config) => {

Loading…
Cancel
Save