Skip to main content

Overview

SkillRise includes Docker configurations for both development and production deployments. Docker provides a consistent, isolated environment that works across all platforms.
Benefits of Docker:
  • No need to install Node.js, MongoDB, or other dependencies locally
  • Consistent environment across development and production
  • Easy scaling and deployment
  • Pre-configured networking between services

Prerequisites

Docker

Docker Engine 20.x or higher

Docker Compose

v2.x (included with Docker Desktop)

Install Docker

Option 1: Docker Desktop (Recommended)
  1. Download Docker Desktop for Mac
  2. Install the .dmg file
  3. Open Docker Desktop and follow the setup wizard
  4. Verify installation:
docker --version
# Expected: Docker version 24.x.x

docker compose version
# Expected: Docker Compose version v2.x.x
Option 2: Homebrew
brew install --cask docker

Project Docker Structure

SkillRise includes the following Docker configuration files:
skillrise/
├── docker-compose.yml          # Production compose file
├── server/
│   └── Dockerfile              # Backend container definition
└── client/
    ├── Dockerfile              # Frontend container definition (multi-stage)
    └── nginx.conf              # Nginx configuration for serving React app

Dockerfile Overview

Backend Dockerfile

The server uses a simple Node.js Alpine image for a lightweight container.
server/Dockerfile
FROM node:20-alpine

WORKDIR /app

# Create non-root user and group early
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Install dependencies
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

# Copy source code with correct ownership
COPY --chown=appuser:appgroup . .

# Switch to non-root user
USER appuser

EXPOSE 3000
CMD ["node", "server.js"]
  • Alpine Linux - Minimal base image (~5 MB vs 1+ GB for full Ubuntu)
  • Non-root user - Security best practice (runs as appuser instead of root)
  • Production dependencies only - npm ci --omit=dev skips devDependencies
  • Layer caching - package.json copied before source code for faster rebuilds

Frontend Dockerfile

The client uses a multi-stage build to keep the final image small.
client/Dockerfile
# Stage 1: Build
FROM node:20-alpine AS builder

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci

ARG VITE_CLERK_PUBLISHABLE_KEY
ARG VITE_RAZORPAY_KEY_ID
ARG VITE_BACKEND_URL

ENV VITE_CLERK_PUBLISHABLE_KEY=$VITE_CLERK_PUBLISHABLE_KEY
ENV VITE_RAZORPAY_KEY_ID=$VITE_RAZORPAY_KEY_ID
ENV VITE_BACKEND_URL=$VITE_BACKEND_URL

COPY . .
RUN npm run build

# Stage 2: Serve with nginx
FROM nginx:alpine

COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
  • Multi-stage build - Build stage is discarded, final image only contains static files + nginx
  • Build arguments - Environment variables are baked into the build at compile time
  • Nginx for serving - Efficient static file server with SPA fallback routing
  • Tiny image size - Final image is ~50 MB (vs ~1 GB if we kept Node.js)

Nginx Configuration

client/nginx.conf
server {
    listen 80;
    root /usr/share/nginx/html;
    index index.html;

    # SPA fallback — all routes serve index.html
    location / {
        try_files $uri $uri/ /index.html;
    }
}
The try_files directive ensures that React Router routes (e.g., /courses/123) serve index.html instead of returning 404.

Docker Compose Configuration

The docker-compose.yml file orchestrates both services:
docker-compose.yml
services:
  server:
    image: pushkarverma/skillrise-server:latest
    ports:
      - '3000:3000'
    env_file:
      - ./server/.env
    restart: unless-stopped

  client:
    image: pushkarverma/skillrise-client:latest
    ports:
      - '80:80'
    depends_on:
      - server
    restart: unless-stopped
services.server.image
string
Pre-built Docker Hub image for the backend. You can replace this with build: ./server to build locally.
services.server.ports
array
Maps host port 3000 to container port 3000 (Express server).
services.server.env_file
string
Loads environment variables from server/.env into the container.
services.client.depends_on
array
Ensures the server starts before the client container.

Running with Docker Compose

Using Pre-built Images (Quickest)

The easiest way is to use pre-built images from Docker Hub:
1

Create environment files

Ensure both server/.env and client/.env are configured. See Configuration.
2

Start services

docker compose up
[+] Running 2/2
 ⠿ Container skillrise-server-1  Started  1.2s
 ⠿ Container skillrise-client-1  Started  2.3s
Attaching to skillrise-client-1, skillrise-server-1
skillrise-server-1  | Database Connected
skillrise-server-1  | Server running on port 3000
skillrise-client-1  | /docker-entrypoint.sh: Configuration complete; ready for start up
The -d flag runs in detached mode (background):
docker compose up -d
3

Access the application

4

Stop services

# Stop containers (keep data)
docker compose stop

# Stop and remove containers
docker compose down

Building Locally

To build images from source instead of using Docker Hub:
1

Modify docker-compose.yml

Replace image references with build contexts:
docker-compose.yml
services:
  server:
    build: ./server
    ports:
      - '3000:3000'
    env_file:
      - ./server/.env
    restart: unless-stopped

  client:
    build:
      context: ./client
      args:
        VITE_CLERK_PUBLISHABLE_KEY: ${VITE_CLERK_PUBLISHABLE_KEY}
        VITE_RAZORPAY_KEY_ID: ${VITE_RAZORPAY_KEY_ID}
        VITE_BACKEND_URL: ${VITE_BACKEND_URL}
    ports:
      - '80:80'
    depends_on:
      - server
    restart: unless-stopped
2

Create root .env file

For client build args, create a .env in the project root:
.env
VITE_CLERK_PUBLISHABLE_KEY=pk_test_...
VITE_RAZORPAY_KEY_ID=rzp_test_...
VITE_BACKEND_URL=http://localhost:3000
3

Build and run

docker compose up --build
This builds both images from scratch (takes 2-5 minutes on first run).
[+] Building 142.3s (23/23) FINISHED
 => [server internal] load build definition from Dockerfile  0.1s
 => [server] COPY package.json package-lock.json ./          0.2s
 => [server] RUN npm ci --omit=dev                           45.3s
 => [client builder] RUN npm run build                       89.7s
 => [client] COPY --from=builder /app/dist                   0.3s
[+] Running 2/2
 ⠿ Container skillrise-server-1  Started  1.1s
 ⠿ Container skillrise-client-1  Started  2.2s

Docker Commands Reference

# Start in foreground (see logs)
docker compose up

# Start in background
docker compose up -d

# Rebuild images and start
docker compose up --build

# Start specific service
docker compose up server

Seeding Data in Docker

To populate the database with demo data:
# Start services
docker compose up -d

# Run seed script inside server container
docker compose exec server node seed.js
🧹 Clearing existing seed data...
👤 Seeding users...
   ✓ 10 users
📚 Seeding courses...
   ✓ 10 courses
💳 Seeding purchases...
   ✓ 19 purchases
📈 Seeding course progress...
   ✓ 19 progress records
🧠 Seeding quizzes...
   ✓ 15 quizzes
🌐 Seeding community groups...
   ✓ 4 groups
💬 Seeding posts...
   ✓ 12 posts
💬 Seeding replies...
   ✓ 18 replies
✅ Database seeded successfully!
See Seeding Data for more details.

Environment Variables in Docker

Backend

Environment variables are loaded from server/.env via the env_file directive:
services:
  server:
    env_file:
      - ./server/.env
All variables in server/.env are automatically available in the container.

Frontend (Build Arguments)

For the client, environment variables must be passed as build arguments because Vite bundles them at build time:
services:
  client:
    build:
      context: ./client
      args:
        VITE_CLERK_PUBLISHABLE_KEY: ${VITE_CLERK_PUBLISHABLE_KEY}
        VITE_BACKEND_URL: ${VITE_BACKEND_URL}
These values are read from a root .env file (not client/.env).
Important: Changes to VITE_* variables require rebuilding the client image:
docker compose up --build client

Production Deployment

For production, use the pre-built images with a Docker registry:

Building and Pushing Images

1

Build production images

# Build server
docker build -t your-registry/skillrise-server:latest ./server

# Build client (with production env vars)
docker build \
  --build-arg VITE_CLERK_PUBLISHABLE_KEY=pk_live_... \
  --build-arg VITE_BACKEND_URL=https://api.yourapp.com \
  -t your-registry/skillrise-client:latest \
  ./client
2

Push to registry

docker push your-registry/skillrise-server:latest
docker push your-registry/skillrise-client:latest
3

Update docker-compose.yml on server

services:
  server:
    image: your-registry/skillrise-server:latest
    # ... rest of config

  client:
    image: your-registry/skillrise-client:latest
    # ... rest of config
4

Deploy

docker compose pull
docker compose up -d
The official SkillRise images are available at:
  • pushkarverma/skillrise-server:latest
  • pushkarverma/skillrise-client:latest

Docker vs Local Development

FeatureLocal DevelopmentDocker
Setup Time15-30 minutes5 minutes
DependenciesNode.js, MongoDB, npmDocker only
Hot Reload✅ Built-in (Vite, nodemon)❌ Requires volume mounts
Environment ParityMay differ✅ Identical to production
Resource UsageLowMedium (containers overhead)
Debugging✅ Easy (direct access)⚠️ Requires docker exec
Best ForActive developmentTesting, CI/CD, production
Recommendation: Use local development for day-to-day coding (faster hot reload). Use Docker for testing full-stack integration and deployment.

Troubleshooting

Error: Bind for 0.0.0.0:3000 failed: port is already allocatedSolutions:
  1. Stop the process using the port:
    # Find process
    lsof -ti:3000 | xargs kill -9
    
  2. Or change the port in docker-compose.yml:
    services:
      server:
        ports:
          - '4000:3000'  # Host:Container
    
Error: npm ERR! network or npm ERR! code ENOTFOUNDSolutions:
  1. Check your internet connection
  2. Clear Docker build cache:
    docker builder prune -a
    docker compose build --no-cache
    
  3. Use a different npm registry:
    RUN npm config set registry https://registry.npmjs.org/
    RUN npm ci
    
Symptom: App can’t connect to database or external APIsSolutions:
  1. Verify server/.env exists and has correct values
  2. Restart containers:
    docker compose down
    docker compose up
    
  3. For client env vars, ensure they’re in root .env and rebuild:
    docker compose up --build client
    
Symptom: Frontend loads but shows white screen or errorsSolutions:
  1. Check browser console for errors (F12 → Console)
  2. Verify VITE_BACKEND_URL matches the server URL:
    VITE_BACKEND_URL=http://localhost:3000
    
  3. Rebuild client image:
    docker compose up --build client
    
  4. Check nginx logs:
    docker compose logs client
    
Error: MongoServerError: connect ECONNREFUSEDSolutions:
  1. If using local MongoDB, change MONGODB_URI to use host network:
    # macOS/Windows
    MONGODB_URI=mongodb://host.docker.internal:27017
    
    # Linux
    MONGODB_URI=mongodb://172.17.0.1:27017
    
  2. Or add MongoDB as a service in docker-compose.yml:
    services:
      mongo:
        image: mongo:8
        ports:
          - '27017:27017'
        volumes:
          - mongo-data:/data/db
    
      server:
        # ...
        environment:
          - MONGODB_URI=mongodb://mongo:27017
        depends_on:
          - mongo
    
    volumes:
      mongo-data:
    
Error: MongoServerError: bad authSolutions:
  1. Ensure server container is running:
    docker compose ps
    
  2. Verify MONGODB_URI in server/.env is correct
  3. Check server logs:
    docker compose logs server
    

Next Steps