diff --git a/DOCKER_SETUP.md b/DOCKER_SETUP.md index 22fa557..a730526 100644 --- a/DOCKER_SETUP.md +++ b/DOCKER_SETUP.md @@ -1,63 +1,148 @@ # Docker Setup for Recipe Management App -## MongoDB Configuration +## Full Stack Containerized Setup -This project now includes a Docker Compose setup for MongoDB with persistent storage. +This project includes a complete Docker Compose setup that orchestrates the entire application stack: +- **Frontend**: React TypeScript application with Nginx +- **Backend**: Node.js Express API +- **Database**: MongoDB with persistent storage +- **Admin Interface**: Mongo Express for database management -### Environment Variables +## Quick Start -Create a `.env` file in the `backend/` directory with the following variables: +1. **Create environment file**: + ```bash + cp env.example .env + ``` + Edit the `.env` file with your desired credentials. -``` -# MongoDB Configuration (parameterized for security) +2. **Start all services**: + ```bash + docker-compose up -d + ``` + +3. **Access the application**: + - **Frontend**: http://localhost:3000 + - **Backend API**: http://localhost:5000 + - **Mongo Express**: http://localhost:8081 + +## Environment Configuration + +Create a `.env` file in the project root with the following variables: + +```bash +# MongoDB Configuration MONGODB_USERNAME=admin MONGODB_PASSWORD=password123 -MONGODB_HOST=localhost -MONGODB_PORT=27017 MONGODB_DATABASE=recipe-management -# Alternative: Use full connection string (overrides individual parameters) -# MONGODB_URI=mongodb://admin:password123@localhost:27017/recipe-management?authSource=admin - -# Server Configuration -PORT=5000 +# JWT Secret for Backend Authentication +JWT_SECRET=your-super-secret-jwt-key-change-this-in-production ``` -**Note**: The application will automatically construct the MongoDB URI from the individual parameters. You can also override this by setting `MONGODB_URI` directly. +## Services Architecture -### Starting the Services +### Frontend Service +- **Container**: `recipe-management-frontend` +- **Port**: 3000 (mapped to container port 80) +- **Technology**: React TypeScript with Nginx +- **Features**: + - Production-optimized build + - API proxy to backend + - Gzip compression + - Security headers -1. Start MongoDB and Mongo Express: +### Backend Service +- **Container**: `recipe-management-backend` +- **Port**: 5000 +- **Technology**: Node.js Express +- **Features**: + - Health checks + - Environment-based configuration + - Automatic MongoDB connection + - Non-root user security + +### MongoDB Service +- **Container**: `recipe-management-mongodb` +- **Port**: 27017 +- **Features**: + - Persistent data volume + - Health checks + - Automatic seed data loading + - Authentication enabled + +### Mongo Express Service +- **Container**: `recipe-management-mongo-express` +- **Port**: 8081 +- **Features**: + - Web-based MongoDB administration + - Connected to main MongoDB instance + +## Development Workflow + +### Building and Running ```bash -docker-compose up -d +# Build and start all services +docker-compose up -d --build + +# View logs +docker-compose logs -f + +# Stop all services +docker-compose down + +# Stop and remove volumes (WARNING: This deletes data) +docker-compose down -v ``` -2. Start the backend application: +### Individual Service Management ```bash -cd backend -npm start +# Restart specific service +docker-compose restart backend + +# View logs for specific service +docker-compose logs -f frontend + +# Execute commands in running container +docker-compose exec backend sh ``` -### Services Included +## Data Persistence -- **MongoDB**: Database server on port 27017 with persistent volume -- **Mongo Express**: Web-based MongoDB admin interface on port 8081 +- **MongoDB Data**: Stored in `mongodb_data` Docker volume +- **Seed Data**: Automatically loaded from `backend/seedData.js` on first startup +- **Persistent Storage**: Data survives container restarts and rebuilds -### Default Credentials +## Network Architecture -- **MongoDB Admin User**: admin -- **MongoDB Admin Password**: password123 -- **Database Name**: recipe-management +All services communicate through a dedicated Docker network (`recipe-network`): +- Frontend → Backend: Internal container communication +- Backend → MongoDB: Internal container communication +- External access via mapped ports only -### Accessing Services +## Security Features -- **Mongo Express**: http://localhost:8081 -- **Backend API**: http://localhost:5000 +- **Non-root containers**: All services run as non-privileged users +- **Environment-based secrets**: Credentials managed via environment variables +- **Network isolation**: Services communicate only through defined network +- **Health checks**: Automatic service health monitoring +- **Security headers**: Frontend served with security headers via Nginx -### Data Persistence +## Troubleshooting -MongoDB data is stored in a Docker volume named `mongodb_data` which persists between container restarts. +### Common Issues +1. **Port conflicts**: Ensure ports 3000, 5000, 8081, 27017 are available +2. **Environment variables**: Check `.env` file exists and has correct values +3. **Build failures**: Run `docker-compose build --no-cache` to rebuild from scratch -### Seed Data +### Useful Commands +```bash +# Check service status +docker-compose ps -The seed data from `backend/seedData.js` is automatically loaded when the MongoDB container starts for the first time. +# View resource usage +docker stats + +# Clean up unused Docker resources +docker system prune -f +``` diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..83a25d4 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,32 @@ +# Use Node.js 18 LTS as base image +FROM node:18-alpine + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci --only=production + +# Copy application code +COPY . . + +# Create non-root user for security +RUN addgroup -g 1001 -S nodejs +RUN adduser -S nodejs -u 1001 + +# Change ownership of the app directory +RUN chown -R nodejs:nodejs /app +USER nodejs + +# Expose port +EXPOSE 5000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:5000/', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })" + +# Start the application +CMD ["npm", "start"] diff --git a/backend/dockerignore b/backend/dockerignore new file mode 100644 index 0000000..7067c42 --- /dev/null +++ b/backend/dockerignore @@ -0,0 +1,12 @@ +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.git +.gitignore +README.md +.env +.nyc_output +coverage +.DS_Store +*.log diff --git a/backend/seedData.js b/backend/seedData.js index cff8ff8..d7be3dc 100644 --- a/backend/seedData.js +++ b/backend/seedData.js @@ -27,7 +27,7 @@ const sampleRecipes = [ cookTime: 15, category: "dinner", difficulty: "medium", - imageUrl: "https://images.unsplash.com/photo-1621996346565-e3dbc353d2e5?w=500" + imageUrl: "https://images.unsplash.com/photo-1692071097529-320eb2b32292?w=500" }, { title: "Chocolate Chip Cookies", diff --git a/backend/server.js b/backend/server.js index a554429..de0b2cf 100644 --- a/backend/server.js +++ b/backend/server.js @@ -11,17 +11,20 @@ app.use(cors()); app.use(express.json()); // MongoDB connection -const MONGODB_USERNAME = process.env.MONGODB_USERNAME; -const MONGODB_PASSWORD = process.env.MONGODB_PASSWORD; -const MONGODB_HOST = process.env.MONGODB_HOST; -const MONGODB_PORT = process.env.MONGODB_PORT; -const MONGODB_DATABASE = process.env.MONGODB_DATABASE; +const MONGODB_HOST = process.env.MONGODB_HOST || 'mongodb'; +const MONGODB_PORT = process.env.MONGODB_PORT || '27017'; +const MONGODB_DATABASE = process.env.MONGODB_DATABASE || 'recipe-management'; + +const MONGODB_URI = `mongodb://${MONGODB_HOST}:${MONGODB_PORT}/${MONGODB_DATABASE}`; + +console.log('Connecting to MongoDB with URI:', MONGODB_URI); -const MONGODB_URI = process.env.MONGODB_URI || - `mongodb://${MONGODB_USERNAME}:${MONGODB_PASSWORD}@${MONGODB_HOST}:${MONGODB_PORT}/${MONGODB_DATABASE}?authSource=admin`; mongoose.connect(MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true, + serverSelectionTimeoutMS: 30000, // 30 seconds + connectTimeoutMS: 30000, // 30 seconds + socketTimeoutMS: 30000, // 30 seconds }); const db = mongoose.connection; diff --git a/docker-compose.yml b/docker-compose.yml index 9e2d84e..7cf7458 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,21 +1,64 @@ version: '3.8' services: + # MongoDB Database mongodb: image: mongo:7.0 container_name: recipe-management-mongodb restart: unless-stopped - environment: - MONGO_INITDB_ROOT_USERNAME: ${MONGODB_USERNAME} - MONGO_INITDB_ROOT_PASSWORD: ${MONGODB_PASSWORD} - MONGO_INITDB_DATABASE: ${MONGODB_DATABASE} ports: - "27017:27017" volumes: - mongodb_data:/data/db - - ./backend/seedData.js:/docker-entrypoint-initdb.d/seedData.js:ro + - ./backend/seedData.js:/docker-entrypoint-initdb.d/01-seedData.js:ro networks: - recipe-network + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')", "--quiet"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 40s + + # Backend API Service + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: recipe-management-backend + restart: unless-stopped + environment: + - PORT=5000 + - MONGODB_HOST=mongodb + - MONGODB_PORT=27017 + - MONGODB_DATABASE=recipe-management + - JWT_SECRET=${JWT_SECRET} + ports: + - "5000:5000" + depends_on: + mongodb: + condition: service_healthy + networks: + - recipe-network + volumes: + - ./backend:/app + - /app/node_modules + + # Frontend React App + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: recipe-management-frontend + restart: unless-stopped + ports: + - "3000:80" + depends_on: + - backend + networks: + - recipe-network + environment: + - REACT_APP_API_URL=http://localhost:5000 # Optional: MongoDB Express for database management mongo-express: @@ -25,12 +68,11 @@ services: ports: - "8081:8081" environment: - ME_CONFIG_MONGODB_ADMINUSERNAME: ${MONGODB_USERNAME} - ME_CONFIG_MONGODB_ADMINPASSWORD: ${MONGODB_PASSWORD} - ME_CONFIG_MONGODB_URL: mongodb://${MONGODB_USERNAME}:${MONGODB_PASSWORD}@mongodb:27017/ + ME_CONFIG_MONGODB_URL: mongodb://mongodb:27017/ ME_CONFIG_BASICAUTH: false depends_on: - - mongodb + mongodb: + condition: service_healthy networks: - recipe-network diff --git a/env.example b/env.example new file mode 100644 index 0000000..899756b --- /dev/null +++ b/env.example @@ -0,0 +1,7 @@ +# MongoDB Configuration for Docker Compose (no authentication) +MONGODB_HOST=mongodb +MONGODB_PORT=27017 +MONGODB_DATABASE=recipe-management + +# JWT Secret for Backend +JWT_SECRET=your-super-secret-jwt-key-change-this-in-production \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..83ca72f --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,48 @@ +# Multi-stage build for React TypeScript app +FROM node:18-alpine as build + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy source code +COPY . . + +# Build the application +RUN npm run build + +# Production stage with nginx +FROM nginx:alpine + +# Copy custom nginx configs +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY nginx-nonroot.conf /etc/nginx/nginx.conf + +# Copy built app from build stage +COPY --from=build /app/build /usr/share/nginx/html + +# Set proper permissions for nginx user (already exists in nginx:alpine) +RUN chown -R nginx:nginx /usr/share/nginx/html && \ + chown -R nginx:nginx /var/cache/nginx && \ + chown -R nginx:nginx /var/log/nginx && \ + chown -R nginx:nginx /etc/nginx/conf.d && \ + mkdir -p /tmp/client_temp /tmp/proxy_temp_path /tmp/fastcgi_temp /tmp/uwsgi_temp /tmp/scgi_temp && \ + chown -R nginx:nginx /tmp/client_temp /tmp/proxy_temp_path /tmp/fastcgi_temp /tmp/uwsgi_temp /tmp/scgi_temp + +# Switch to nginx user +USER nginx + +# Expose port +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:80/ || exit 1 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/dockerignore b/frontend/dockerignore new file mode 100644 index 0000000..6f311ef --- /dev/null +++ b/frontend/dockerignore @@ -0,0 +1,17 @@ +node_modules +build +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.git +.gitignore +README.md +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +.DS_Store +*.log +coverage +.nyc_output diff --git a/frontend/nginx-nonroot.conf b/frontend/nginx-nonroot.conf new file mode 100644 index 0000000..0b4611a --- /dev/null +++ b/frontend/nginx-nonroot.conf @@ -0,0 +1,33 @@ +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /tmp/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + # Temporary directories for nginx non-root user + client_body_temp_path /tmp/client_temp; + proxy_temp_path /tmp/proxy_temp_path; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..c2969a0 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,45 @@ +server { + listen 80; + server_name localhost; + + # Serve static files + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + + # API proxy to backend + location /api/ { + proxy_pass http://backend:5000/api/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied expired no-cache no-store private auth; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/javascript + application/xml+rss + application/json; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; +}