Cloud Architecture
Campus Connect is fully containerized and deployed inside a single-node Kubernetes cluster (via Minikube) on CloudLab. All components are managed using Helm and exposed through an NGINX ingress controller.
Application Overview
The app consists of:
- A React frontend built and served through NGINX
- A Node.js/NestJS backend connected to MongoDB and Redis
- Kubernetes-managed services for container networking
- A Helm-based config injection system for runtime flexibility
flowchart LR
subgraph "Kubernetes Cluster (Minikube)"
Ingress[NGINX Ingress Controller<br>campus-connect-ingress]
Frontend[Frontend Pod<br>campus-connect-frontend]
Backend[Backend Pod<br>campus-connect-backend]
Redis[Redis Pod<br>redis]
Mongo[MongoDB Pod<br>mongo]
Ingress -->|HTTP Requests| Frontend
Ingress -->|/api, /socket.io| Backend
Backend -->|Sessions| Redis
Backend -->|Database| Mongo
end
User[User Browser<br>cloudlab.hostname] --> Ingress
Backend Container
The backend is a stateless Node.js API built with NestJS, containerized using a multi-stage Dockerfile.
Dockerfile (Backend)
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
RUN npm install -g @nestjs/cli
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
RUN chown -R appuser:appgroup /app
USER appuser
ENV NODE_ENV=production
EXPOSE ${BACKEND_PORT}
CMD ["npm", "run", "start"]
- Non-root user (
appuser) for improved security - Uses environment variables passed via ConfigMap
- Exposes port
3000inside the cluster - All logic resides in memory (stateless), with Redis and Mongo for storage/session
Frontend Container
The frontend is built as a static React site, then served through an NGINX container with custom configuration. This also acts as a reverse proxy for backend API and WebSocket traffic.
Dockerfile (Frontend)
# Build React App
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Serve with NGINX
FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
COPY nginx/default.conf.template /etc/nginx/conf.d/default.conf.template
COPY nginx/nginx.conf /etc/nginx/nginx.conf
RUN mkdir -p /var/cache/nginx \
&& chown -R nginx:nginx /usr/share/nginx /var/cache/nginx /etc/nginx
USER nginx
EXPOSE 80
CMD ["/bin/sh", "-c", "envsubst '$DOMAIN $NGINX_BACKEND_UPSTREAM' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf && nginx -g 'pid /tmp/nginx.pid; daemon off;'"]
- NGINX config is dynamically rendered at runtime using
envsubst - Handles frontend routing and API proxying
NGINX
The frontend container uses a templated NGINX config to:
- Serve static frontend files (
/) - Proxy API traffic to the backend (
/api) - Proxy WebSocket traffic to backend endpoints (e.g.,
/notifications/socket.io)
default.conf.template
server {
listen 80;
server_name $DOMAIN;
location /api/ {
proxy_pass http://$NGINX_BACKEND_UPSTREAM/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /notifications/socket.io {
proxy_pass http://$NGINX_BACKEND_UPSTREAM/notifications/socket.io;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
location /channels/socket.io {
proxy_pass http://$NGINX_BACKEND_UPSTREAM/channels/socket.io;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
location / {
root /usr/share/nginx/html;
try_files $uri /index.html;
}
}
This configuration allows all services to run behind a single domain using paths like:
/→ React frontend/api/→ Backend REST API/channels/socket.io→ WebSocket real-time messaging
flowchart LR
User["User Request: (hostname -f)"] --> NGINX_Frontend["NGINX (Frontend Container)"]
NGINX_Frontend -->|"/"| ReactApp["/usr/share/nginx/html"]
NGINX_Frontend -->|"/api/"| BackendAPI[campus-connect-backend-service:3000]
NGINX_Frontend -->|"/notifications/socket.io"| WS1[campus-connect-backend-service:3000/notifications/socket.io]
NGINX_Frontend -->|"/channels/socket.io"| WS2[campus-connect-backend-service:3000/channels/socket.io]
Runtime Configuration via Helm
At deploy time, values like DOMAIN and service hostnames are injected into the container via:
- Helm templates (from
values.yaml) - Kubernetes
ConfigMaps - Environment variable substitution at container startup (
envsubst)
This allows switching between environments (local, CloudLab) without rebuilding containers.
Security Notes
- All containers run as non-root users
- Sensitive values like passwords and encryption keys are currently in plain-text (TODO: migrate to Kubernetes/Helm Secrets)