CampusBite includes a production-ready Dockerfile with multi-stage builds for minimal image size and fast deployments.
Dockerfile overview
The Dockerfile uses three stages:
# Stage 1: Build frontend
FROM node:20-bookworm-slim AS frontend-build
# Stage 2: Install backend dependencies
FROM node:20-bookworm-slim AS backend-deps
# Stage 3: Runtime image
FROM node:20-bookworm-slim AS runtime
Stage 1: Frontend build
FROM node:20-bookworm-slim AS frontend-build
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build
Builds the React frontend into static files at frontend/dist.
Stage 2: Backend dependencies
FROM node:20-bookworm-slim AS backend-deps
WORKDIR /app/backend
COPY backend/package*.json ./
RUN npm ci --omit=dev
Installs only production dependencies (no devDependencies).
Stage 3: Runtime
FROM node:20-bookworm-slim AS runtime
ENV NODE_ENV=production
ENV PORT=8080
WORKDIR /app
COPY --from=backend-deps /app/backend/node_modules ./backend/node_modules
COPY backend/ ./backend/
COPY --from=frontend-build /app/frontend/dist ./frontend/dist
RUN mkdir -p /app/backend/public/uploads \
&& chown -R node:node /app
USER node
EXPOSE 8080
CMD ["node", "backend/src/index.js"]
Combines backend code, frontend static files, and production dependencies into a minimal runtime image.
Building the image
Build the image
docker build -t campusbite:latest .
This builds all three stages and tags the final image as campusbite:latest.The build process requires ~2GB of disk space for intermediate layers.
Verify the build
You should see output like:REPOSITORY TAG IMAGE ID CREATED SIZE
campusbite latest abc123def456 2 minutes ago 450MB
Running the container
Quick start
Run with environment variables:
docker run -d \
--name campusbite \
-p 8080:8080 \
-e JWT_SECRET="your-super-secret-jwt-key-at-least-32-chars-long" \
-e JWT_REFRESH_SECRET="your-super-secret-refresh-key-different-from-jwt" \
-e MONGODB_URI="mongodb://host.docker.internal:27017/campusbite" \
-e FRONTEND_URL="http://localhost:8080" \
campusbite:latest
Use host.docker.internal instead of localhost to connect to MongoDB running on your host machine.
Using an .env file
Create a .env file:
JWT_SECRET=your-super-secret-jwt-key-at-least-32-chars-long
JWT_REFRESH_SECRET=your-super-secret-refresh-key-different-from-jwt
MONGODB_URI=mongodb://host.docker.internal:27017/campusbite
FRONTEND_URL=http://localhost:8080
APP_URL=http://localhost:8080
# Optional SMTP
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=[email protected]
SMTP_PASS=your-app-password
FROM_EMAIL=[email protected]
Run with the env file:
docker run -d \
--name campusbite \
-p 8080:8080 \
--env-file .env \
campusbite:latest
Persistent uploads with volumes
To persist uploaded files across container restarts:
docker run -d \
--name campusbite \
-p 8080:8080 \
-v campusbite-uploads:/app/backend/public/uploads \
--env-file .env \
campusbite:latest
This creates a named volume campusbite-uploads that persists menu images and store logos.
Docker Compose
For a complete setup with MongoDB, create docker-compose.yml:
version: '3.8'
services:
mongodb:
image: mongo:7
container_name: campusbite-mongodb
restart: unless-stopped
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: secure_password
MONGO_INITDB_DATABASE: campusbite
volumes:
- mongodb-data:/data/db
ports:
- "27017:27017"
backend:
build: .
container_name: campusbite-backend
restart: unless-stopped
ports:
- "8080:8080"
environment:
NODE_ENV: production
PORT: 8080
MONGODB_URI: mongodb://admin:secure_password@mongodb:27017/campusbite?authSource=admin
JWT_SECRET: ${JWT_SECRET}
JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET}
FRONTEND_URL: http://localhost:8080
APP_URL: http://localhost:8080
SMTP_HOST: ${SMTP_HOST}
SMTP_PORT: ${SMTP_PORT}
SMTP_USER: ${SMTP_USER}
SMTP_PASS: ${SMTP_PASS}
FROM_EMAIL: ${FROM_EMAIL}
volumes:
- uploads:/app/backend/public/uploads
depends_on:
- mongodb
volumes:
mongodb-data:
uploads:
Create a .env file:
JWT_SECRET=your-super-secret-jwt-key-at-least-32-chars-long
JWT_REFRESH_SECRET=your-super-secret-refresh-key-different-from-jwt
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=[email protected]
SMTP_PASS=your-app-password
FROM_EMAIL=[email protected]
Run the stack:
Stop the stack:
Stop and remove volumes:
Container management
View logs
# Live tail
docker logs -f campusbite
# Last 100 lines
docker logs --tail 100 campusbite
Execute commands in the container
# Open a shell
docker exec -it campusbite sh
# Check Node.js version
docker exec campusbite node --version
# List uploaded files
docker exec campusbite ls -lh /app/backend/public/uploads
Stop and start
# Stop
docker stop campusbite
# Start
docker start campusbite
# Restart
docker restart campusbite
Remove container
# Stop and remove
docker rm -f campusbite
# Remove image
docker rmi campusbite:latest
Environment variables in Docker
All environment variables can be set in Docker using:
docker run -e VARIABLE=value campusbite:latest
Production best practices
Security
-
Run as non-root user: The Dockerfile already does this with
USER node
-
Use secrets management: For production, use Docker secrets or environment variable injection from your orchestrator:
docker secret create jwt_secret /path/to/secret
docker service create --secret jwt_secret campusbite:latest
-
Scan for vulnerabilities:
docker scan campusbite:latest
Resource limits
Set memory and CPU limits:
docker run -d \
--name campusbite \
--memory="512m" \
--cpus="1.0" \
-p 8080:8080 \
--env-file .env \
campusbite:latest
In Docker Compose:
services:
backend:
deploy:
resources:
limits:
memory: 512M
cpus: '1.0'
reservations:
memory: 256M
cpus: '0.5'
Health checks
Add a health check to your docker-compose.yml:
services:
backend:
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
Or in Dockerfile:
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD node -e "require('http').get('http://localhost:8080/api/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"
Troubleshooting
Build fails with “npm install” errors
Solution: Clear Docker build cache:
docker build --no-cache -t campusbite:latest .
Solution: Check logs for missing environment variables:
Look for:
[Startup] Missing required environment variables: JWT_SECRET, MONGODB_URI
Cannot connect to MongoDB
Solution: Use the correct hostname:
MONGODB_URI=mongodb://host.docker.internal:27017/campusbite
Port already in use
Error: bind: address already in use
Solution: Use a different host port:
docker run -p 3000:8080 campusbite:latest
# ^^^^ host port (change this)
# ^^^^ container port (don't change)
Or stop the conflicting service:
lsof -ti:8080 | xargs kill -9
Uploads not persisting
Solution: Mount a volume:
docker run -v campusbite-uploads:/app/backend/public/uploads campusbite:latest
Verify the volume:
docker volume ls
docker volume inspect campusbite-uploads
Build for ARM64 (Apple Silicon, AWS Graviton) and AMD64:
docker buildx create --use
docker buildx build --platform linux/amd64,linux/arm64 -t campusbite:latest --push .
This requires pushing to a registry (Docker Hub, GitHub Container Registry, etc.).