A full-stack Reddit-like social media platform with real-time updates, user authentication, post privacy controls, and image uploads. Deployed on both traditional VM infrastructure (Weeks 1-8) and Kubernetes (Weeks 9-11).
- Reddit-like Full-Stack Application
- Tech Stack & Architecture
- Table of Contents
- Quick Start (Kubernetes)
- High-Level Architecture
- Application Features
- Authentication System
- Real-Time Updates
- Database & Data Model
- Private Posts & Authorization
- Image Storage
- Development Workflow
- Deployment & Infrastructure
- CI/CD Pipeline (GitLab)
- Accessing the Application
- AI Usage Disclosure
- Kubernetes Operations Guide
- Security Practices
- Architecture Comparison: VM vs. Kubernetes
- 📄 License
# Clone and setup
export KUBECONFIG=k8s/cicd-serviceaccount-kubeconfig
kubectl get pods -n team-30-staging
# Access application
curl https://team30-nginx.kubernetes.devops.cs.ut.ee/healthz
# Deploy (push to main branch triggers CI/CD or apply manually)
kubectl apply -f k8s/ -n team-30-stagingThe application is fully decoupled:
- Frontend: Vue 3 + TypeScript SPA served via Nginx
- Backend: Django REST Framework API with WebSocket support via Django Channels
| Layer | Technology |
|---|---|
| Frontend | Vue 3.5, TypeScript 5.9, Vite 7.1, Axios, Vue Router |
| Backend | Django 5.2, Django REST Framework 3.16, Django Channels 4.3, Daphne 4.2 |
| Database | PostgreSQL (swapped from embedded SQLite3) |
| Real-time | WebSockets via Django Channels + Redis (InMemoryChannelLayer in dev) |
| Auth | JWT (via djangorestframework-simplejwt) |
| CI/CD | GitLab CI with stages: setup → build → test → deploy |
| Infra | Ubuntu 22.04 VM, Nginx, Ansible, Let's Encrypt (HTTPS) |
| Testing | Django TestCase, coverage |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
| POST | /auth/register/ |
Register new user | No |
| POST | /auth/login/ |
Login with JWT | No |
| POST | /auth/logout/ |
Client-side token deletion | Yes |
| POST | /auth/change-password/ |
Change password | Yes |
| GET | /posts/ |
List posts (public + own if authed) | No (read-only) |
| POST | /posts/ |
Create new post | Yes |
| POST | /posts/{id}/like/ |
Like a post | Yes |
| POST | /posts/{id}/dislike/ |
Dislike a post | Yes |
| POST | /posts/{id}/toggle_privacy/ |
Toggle post privacy | Yes (author) |
| DELETE | /posts/{id}/ |
Delete post | Yes (author) |
All endpoints respect privacy: unauthenticated users see only public posts.
The backend implements several critical functions that enforce business logic, ensure data integrity, and enable real-time features:
PostViewSet.perform_create(): Saves new posts and triggers WebSocket broadcast viabroadcast_post_created().PostViewSet.like()/dislike(): Use atomic database transactions and row-level locking (select_for_update) to safely handle concurrent votes.PostViewSet.get_queryset(): Filters posts based on authentication and privacy (is_private), ensuring users only see allowed content.broadcast_post_*()(inutils.py): Send serialized post data to WebSocket groups for real-time updates.FeedConsumerevent handlers: Receive ORM-triggered events and push them to connected clients over WebSockets.Post.clean(): Validates post content, title, and image at the model level to enforce business rules.Post.can_be_modified_by(): Centralizes ownership checks for edit/delete operations.PostSerializer.get_user_vote(): Dynamically annotates each post with the current user’s vote status (like/dislike/neutral).
Full auto-generated API docs can be added via
drf-spectacular(not included yet).
- Registration & Login: JWT-based via
djangorestframework-simplejwt - Password Hashing: Uses Django’s default PBKDF2-SHA256 (secure, FIPS-compliant)
- Session Handling: Stateless JWT; no server-side sessions
- Post Ownership:
- All posts are linked to a
Userviaauthorforeign key is_anonymousis a display flag — the real author is always known server-side- Private posts (
is_private=True) are only visible to the author
- All posts are linked to a
Security: Passwords are never stored in plaintext. All auth endpoints validate input and use secure hashing.
- Technology: Django Channels + WebSockets
- Flow:
- User performs action (e.g., likes a post)
- Backend updates DB and calls
broadcast_post_updated(post) broadcast_*functions send serialized post data tofeed_updateschannel group- All connected WebSocket clients receive update instantly
- Frontend:
websocket.tsmanages connection, reconnection, and event dispatching
Supports real-time: post creation, voting, privacy toggle, and deletion.
### Why SQLite?
Lightweight, file-based, zero-config — ideal for development and small-scale deploymentEasily replaceable with PostgreSQL in production (just changeDATABASESinsettings.py)
Why PostgreSQL?
- Production-ready: Robust, reliable, battle-tested
- Scalability: Better performance with larger datasets
- Concurrency: Handles concurrent connections safely with ACID transactions
- Separation of Concerns: Database runs in its own container (resilience, independent scaling)
Architecture:
- Database: PostgreSQL 16 running as a Kubernetes StatefulSet (1 replica)
- Storage: 1Gi Longhorn persistent volume (
postgres-storage-postgres-0) - Credentials: Managed via
app-secretKubernetes Secret (username:app, password: manually generated) - Configuration:
app-configConfigMap stores connection details (DB_HOST,DB_PORT,DB_NAME)
author = ForeignKey(User)
is_anonymous = BooleanField()
is_private = BooleanField()
title = CharField(max_length=50)
content = TextField(blank=True)
image = ImageField(upload_to="post_images/", blank=True, null=True)
created_at = DateTimeField(auto_now_add=True)user = ForeignKey(User)
post = ForeignKey(Post)
vote_type = CharField(choices=[("like", "Like"), ("dislike", "Dislike")])
created_at = DateTimeField(auto_now_add=True)
# Unique constraint: (user, post)Validation: Custom
clean()methods enforce business rules (e.g., title required, image ≤5MB).
- Visibility:
- Public posts: visible to all
- Private posts: only visible to authenticated author
- Modification:
Post.can_be_modified_by(user)checksself.author == user- Backend enforces this in
toggle_privacyanddestroyactions
- Frontend: Privacy toggle button only appears for author
No client-side bypass possible — all checks are enforced server-side.
- Uploads: Handled via Django’s
ImageField - Storage: Files saved to
./media/post_images/on the VM - Serving: Nginx serves
/media/directly (bypassing Django) - Validation:
- Max size: 5 MB
- Allowed formats: JPEG, PNG, GIF
- At least content or image must be provided
-
Clone repo
git clone <repo-url> cd devops2025
-
Backend (Django)
cd reddit_like python -m venv venv source venv/bin/activate # Linux/macOS pip install -r ../requirements.txt python manage.py migrate python manage.py runserver # Runs on http://127.0.0.1:8000
-
Frontend (Vue)
cd ../reddit_like_frontend/reddit_like_frontend npm install npm run dev # Runs on http://localhost:5173
-
Access App: Open
http://localhost:5173
Hot-reload enabled for both frontend (Vite) and backend (Django dev server).
| Task | Command |
|---|---|
| Run all tests | cd reddit_like && python manage.py test |
| Code coverage | coverage run --source='.' manage.py test |
| Backend lint | ruff check . |
| Backend format | ruff format . |
| Frontend lint | npm run lint:check |
| Frontend format | npm run format:check |
| TS check | npm run vue-tsc |
Pre-commit hooks auto-lint/format on
git commit(runpre-commit install).
- OS: Ubuntu 22.04
- Web Server: Nginx (serves static frontend + proxies
/api/to Daphne) - App Server: Daphne (ASGI for WebSockets + HTTP)
- Process Manager: systemd (
daphne.service) - Firewall: UFW (allows 22, 80, 443)
- HTTPS: Let's Encrypt via Certbot (auto-renewal)
- URL:
https://team30vm.cloud.ut.ee
The GitLab CI/CD pipeline uses Kaniko to build Docker images without requiring Docker daemon access.
Build Stages:
build_backend: Builds Django backend fromreddit_like/Dockerfilebuild_frontend: Multi-stage build (Node.js → Nginx) fromreddit_like_frontend/Dockerfile- Image Tags:
$CI_COMMIT_SHAandlatest - Registry:
registry.gitlab.cs.ut.ee/devops/teams/team-30/devops2025/
Key Manifest Files:
k8s-backend-deployment.yaml/k8s-frontend-deployment.yaml- Pod definitions with health probesk8s-backend-service.yaml/k8s-frontend-service.yaml- Internal services (ClusterIP)nginx-ingress.yaml- External routing with TLS terminationdjango-config.yaml/django-secret.yaml- Configuration and secretsk8s-media-pvc.yaml- 5Gi persistent storage for media filespostgres-cluster.yaml- CloudNativePG database cluster
Private Registry Authentication:
Deployments use imagePullSecrets: gitlab-registry to pull images from GitLab Container Registry. Secret created with GitLab Deploy Token.
Traffic Flow Diagram:
┌─────────────────────────────────────┐
│ External User (HTTPS) │
└───────────────┬─────────────────────┘
│
┌───────────────▼─────────────────────┐
│ Nginx Ingress Controller │
│ (TLS Termination, Let's Encrypt) │
│ team30-nginx.kubernetes... │
└───────┬──────────────┬──────────────┘
│ │
┌───────────────┴───┐ ┌───▼──────────────┐
│ Path: / │ │ Path: /api │
│ Path: /ws │ │ Path: /media │
│ Path: /healthz │ │ Path: /readyz │
│ │ │ Path: /metrics │
└───────┬───────────┘ └───┬──────────────┘
│ │
┌───────────────▼──────────┐ ┌───────▼────────────────┐
│ vue-frontend-svc │ │ django-backend-svc │
│ (ClusterIP:80) │ │ (ClusterIP:8000) │
└───────────┬──────────────┘ └───────┬────────────────┘
│ │
┌───────────▼──────────────┐ ┌───────▼────────────────┐
│ vue-frontend Deployment │ │ django-backend Deploy │
│ (1 replica) │ │ (1 replica) │
│ Nginx container │ │ Daphne ASGI container │
└──────────────────────────┘ └───────┬────────────────┘
│
┌───────▼────────────────┐
│ PostgreSQL Deployment │
│ (StatefulSet) │
│ + │
│(CloudNativePG Cluster )│
│ + │
│ Longhorn PVC │
└────────────────────────┘
Component Responsibilities:
-
Ingress (nginx-ingress.yaml):
- TLS Termination: Manages Let's Encrypt SSL certificates via
cert-manager - Host-based Routing: Routes traffic for
team30-nginx.kubernetes.devops.cs.ut.ee - Path-based Routing:
/→vue-frontend-svc:80(frontend static files)/api→django-backend-svc:8000(REST API)- WebSocket connections also proxied to backend
- Annotations:
kubernetes.io/ingress.class: nginx,cert-manager.io/cluster-issuer: letsencrypt-prod
- TLS Termination: Manages Let's Encrypt SSL certificates via
-
Services (ClusterIP):
vue-frontend-svc: Internal load balancer for frontend pods (port 80)django-backend-svc: Internal load balancer for backend pods (port 8000)- Services use label selectors to discover pods:
selector: app: django-backend
-
Deployments:
vue-frontend: Manages Nginx containers serving Vue.js SPAdjango-backend: Manages Daphne ASGI containers with Django app- Deployments ensure desired replica count (1 pod each currently)
- Use
Recreatestrategy for backend to avoid PVC conflicts
ConfigMaps:
django-config: Application settings (MEDIA_ROOT, DJANGO_DEBUG, ALLOWED_HOSTS, DATABASE_URL)app-config: Database connection details (DB_HOST, DB_PORT, DB_NAME)
Secrets:
django-secret: Django secret key (base64-encoded)app-secret: PostgreSQL credentials (username, password)
GitLab CI/CD Variables:
KUBE_URL,KUBE_TOKEN,KUBE_CA,KUBE_NAMESPACE- Kubernetes authentication- All sensitive values are masked and protected
- Deployment applies manifests via
kubectl apply -f k8s/
Deployment Process:
- Generate
kubeconfigfrom CI/CD variables (KUBE_URL, KUBE_TOKEN, KUBE_CA) - Run
kubectl apply -f k8s/ -n team-30-staging - Applies all manifests (Deployments, Services, Ingress, ConfigMaps, Secrets, PVC)
RBAC Setup:
- ServiceAccount:
cicd-serviceaccountwith deployment permissions - Role grants: get, list, watch, create, update, patch, delete on resources
- Authentication via long-lived ServiceAccount token
Why PostgreSQL?
- Production-ready: Robust, reliable, battle-tested
- Scalability: Better performance with larger datasets
- Concurrency: Handles concurrent connections safely with ACID transactions
- Separation of Concerns: Database runs in its own container (resilience, independent scaling)
Architecture:
- Database: PostgreSQL 16 running as a Kubernetes StatefulSet (1 replica)
- Storage: 1Gi Longhorn persistent volume (
postgres-storage-postgres-0) - Credentials: Managed via
app-secretKubernetes Secret (username:app, password: auto-generated) - Configuration:
app-configConfigMap stores connection details (DB_HOST,DB_PORT,DB_NAME)
Code Changes:
# reddit_like/settings.py - Dynamic database selection
DB_ENGINE = os.getenv("DB_ENGINE", "django.db.backends.sqlite3")
if DB_ENGINE == "django.db.backends.postgresql":
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.getenv("DB_NAME", "appDB"),
"USER": os.getenv("DB_USER", "app"),
"PASSWORD": os.getenv("DB_PASSWORD", ""),
"HOST": os.getenv("DB_HOST", "localhost"),
"PORT": os.getenv("DB_PORT", "5432"),
}
}
else:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}Manifest Files:
postgres-deployment.yaml- StatefulSet for PostgreSQL 16-alpine with health probespostgres-service.yaml- Headless service for DNS-based pod discoverydjango-migration-job.yaml- One-time job to runpython manage.py migrateagainst new PostgreSQL
Data Persistence:
- All Django models (Post, Vote, User, etc.) migrated to PostgreSQL
- Tables persist across Pod restarts thanks to StatefulSet + Longhorn PVC
- Tested: Pod deletion and restart — all data remains intact
Endpoints:
/healthz(Liveness): Returns200 OKif app process is alive. Kubernetes restarts pod on failure./readyz(Readiness): Returns200 OKif database is accessible,503otherwise. Updatesdjango_db_readymetric.
Probe Configuration (k8s-backend-deployment.yaml):
livenessProbe:
httpGet:
path: /healthz
port: 8000
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet:
path: /readyz
port: 8000
initialDelaySeconds: 20
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3How It Works:
- Liveness Probe (every 10s): If fails 3 times → Kubernetes restarts pod
- Readiness Probe (every 10s): If fails → pod removed from service load balancing (traffic rerouted)
- Both probes help Kubernetes maintain application health automatically
Testing:
# Terminal 1: Port-forward backend
kubectl port-forward -n team-30-staging deployment/django-backend 8000:8000
# Terminal 2: Test endpoints
curl -v http://localhost:8000/healthz # Should return 200 with {"status": "alive"}
curl -v http://localhost:8000/readyz # Should return 200 with {"status": "ready", "database": "connected"}Custom Metrics (exposed at /metrics on port 8000):
-
app_posts_total- Total posts in database- Benefit: Shows user interaction and verifies posting functionality is working
-
app_votes_total- Total votes (likes + dislikes)- Benefit: Shows user engagement and verifies voting functionality is working
-
http_request_latency_seconds- Request latency histogram- Benefit: Monitors P90 latency (how long it takes to serve 90% of requests) to detect if performance is affecting user experience
-
http_requests_total- Total HTTP requests by method/path/status- Benefit: Shows requests per endpoint (detect unusual activities) and requests per second (detect service overload or downtime)
-
django_db_ready(bonus) - Database readiness (1=ready, 0=not ready)- Benefit: Monitors database connectivity in real-time
Prometheus Scraping:
# k8s-backend-service.yaml
apiVersion: v1
kind: Service
metadata:
name: django-backend-svc
namespace: team-30-staging
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8000"
prometheus.io/path: "/metrics"
spec:
selector:
app: django-backend
ports:
- name: http
port: 8000
targetPort: 8000Grafana Dashboard (14 Panels):
-
HTTP Error Distribution by Code & Endpoint (Last 5m)
- Shows: Tracks HTTP errors (4xx/5xx) by status code and API path over time.
- Usage: Helps identify problematic endpoints.
-
Top 5 API Endpoints by Request Rate (Last 5m)
- Shows: The top 5 endpoints by request rate (over the last 5 minutes). The endpoints are filtered to specific paths (like post-list, post-detail, etc.).
- Usage: Highlights high-traffic areas for optimization.
-
API Success Rate (2xx Responses, Last 5m)
- Shows: The percentage of successful HTTP requests (status codes 2xx) over the last 5 minutes.
- Usage: Aim for >95% (green). Yellow warns of potential issues; red indicates high failures.
-
API Error Rate (4xx/5xx, Last 5m)
- Shows: The percentage of HTTP errors (status codes 4xx and 5xx) over the last 5 minutes.
- Usage: Monitor for spikes in user-facing errors.
-
PostgreSQL Database Storage Usage (%)
- Shows: The percentage of used storage for the PostgreSQL database.
- Usage: Monitor to avoid capacity issues.
-
Media Storage Usage (Images/GIFs, %)
- Shows: The percentage of used storage for media (like images, gifs).
- Usage: Monitor to avoid capacity issues.
-
PostgreSQL Database Status
- Shows: 1 = Up and ready; 0 = Down or unreachable. Based on app's DB health check.
- Usage: Monitor database connectivity.
-
Application Pod Status
- Shows: 1 = App is up and scraped by Prometheus; 0 = Down.
- Usage: Monitor application availability.
-
90th Percentile API Response Time (Last 5m)
- Shows: The 90th percentile of HTTP request latency over the last 5 minutes. The 90th percentile response time indicates that 90% of the recorded response times fall below a specific duration.
- Usage: spikes may indicate performance bottlenecks
- Total HTTP Requests per Second
- Shows: The overall rate of HTTP requests per second.
- Usage: Useful for spotting usage peaks or drops.
- Application Memory Usage (RSS)
- Shows: The resident memory usage of the application.
- Usage: Monitor for leaks; compare to pod limits.
- Application CPU Usage (Cores)
- Shows the CPU usage (rate of CPU seconds) of the application.
- Usage: Values >1 indicate high load across replicas.
- Total Posts Created
- Shows: Cumulative count of posts created in the application.
- Usage: Tracks user engagement and content creation trends.
- Total User Votes
- Shows: Cumulative count of votes (likes + dislikes) cast by users.
- Usage: Monitors user interaction and engagement levels.
Dashboard URL: https://grafana.kubernetes.devops.cs.ut.ee/ (Team 30 dashboard)
Storage Architecture:
| Component | Storage Type | Size | Purpose | Persistence |
|---|---|---|---|---|
| PostgreSQL DB | Longhorn PVC | 1Gi | Stores all application data (posts, votes, users) | ✅ Persistent across restarts |
| Media Files | Longhorn PVC | 5Gi | User-uploaded images | ✅ Persistent across restarts |
Longhorn Implementation:
# postgres-deployment.yaml
volumeClaimTemplates:
- metadata:
name: postgres-storage
spec:
storageClassName: longhorn
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
# k8s-media-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: media-pvc
namespace: team-30-staging
spec:
storageClassName: longhorn
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5GiHow Longhorn Works:
- Replication: Data automatically replicated across cluster nodes (default 3 replicas)
- Snapshots: Supports backup snapshots for disaster recovery
- Expansion: Volumes can be expanded online without downtime
- Monitoring: Integrated with Prometheus for storage metrics
Data Persistence Verification:
# 1. Get current post count
kubectl exec -it deployment/django-backend -n team-30-staging -- python manage.py shell -c "from feed.models import Post; print(f'Posts: {Post.objects.count()}')"
# 2. Delete PostgreSQL pod
kubectl delete pod postgres-0 -n team-30-staging
# 3. Wait for pod to restart
kubectl wait --for=condition=Ready pod postgres-0 -n team-30-staging --timeout=300s
# 4. Verify data is still there
kubectl exec -it deployment/django-backend -n team-30-staging -- python manage.py shell -c "from feed.models import Post; print(f'Posts after restart: {Post.objects.count()}')"
# Count should be the same — data persisted!Storage Monitoring:
# List persistent volumes
kubectl get pvc -n team-30-staging
# Check Longhorn volume usage
kubectl get volumesnapshotcontents -n team-30-stagingThe pipeline now supports both VM and Kubernetes deployments with the following stages:
-
Setup
- Validates CI variables (
DEPLOY_HOST,SSH_PRIVATE_KEY, etc.) - Manual trigger for
VM Setup(Ansible provisioning) only onAnsible-Setupbranch
- Validates CI variables (
-
Code Quality
- Backend & Frontend linting/formatting checks
- Backend Ruff formatting and linting checks
- Frontend ESLint + Prettier checks
- Backend & Frontend linting/formatting checks
-
Test
- Django unit & integration tests (with coverage ≥50%)
- Secret detection
- SAST scan
-
Build Backend:
- Kaniko builds Django backend image (
$CI_COMMIT_SHA+latest)
- Kaniko builds Django backend image (
-
Build Frontend:
- Kaniko builds Vue.js frontend image (
$CI_COMMIT_SHA+latest)
- Kaniko builds Vue.js frontend image (
-
Deploy
- VM Deploy: Builds Vue frontend,
rsyncs to VM, restarts services - Kubernetes Deploy: Generates kubeconfig, runs
kubectl apply -f k8s/
- VM Deploy: Builds Vue frontend,
VM Deployment:
DEPLOY_HOST,DEPLOY_USER,SSH_PRIVATE_KEY,SECRETS_FILE,DJANGO_ENV_FILE(protected & masked that are not files)
Kubernetes Deployment:
KUBE_URL,KUBE_TOKEN,KUBE_CA,KUBE_NAMESPACE(masked & protected)
Workflow: Code push → CI/CD variable checks & VM setup → Lint/Test → Build containers → Deploy to VM and K8s → Live application
- URL:
https://team30vm.cloud.ut.ee - HTTPS: Enforced (HTTP → HTTPS redirect)
- Certificates: Auto-managed by Certbot
- URL:
https://team30-nginx.kubernetes.devops.cs.ut.ee - TLS: Managed by cert-manager with Let's Encrypt
- Endpoints:
/- Vue.js frontend (SPA)/api/- Django REST API/ws/- WebSocket connections (real-time updates)/media/- User-uploaded images (served from Longhorn PVC)/healthz- Liveness check (backend health)/readyz- Readiness check (DB connectivity)/metrics- Prometheus metrics (not exposed via Ingress for security)
- Grafana Dashboard:
https://grafana.kubernetes.devops.cs.ut.ee/- Team 30 dashboard with 10 panels (metrics, health, storage, resources)
- Prometheus: Metrics scraped every 30s from backend service
- Longhorn UI: Storage management and volume monitoring (admin access only)
The following parts were assisted by AI (Qwen3, GitHub Copilot, Grok):
- Initial scaffolding of Vue components (
PostItem.vue,FeedView.vue) - Drafting of Ansible tasks, templates and playbook
- Boilerplate for Django models and serializers
- CI/CD pipeline structure and comments
- Docker containerization (Dockerfiles for backend and frontend)
- Kubernetes manifests (Deployments, Services, Ingress, ConfigMaps, Secrets)
- Health check endpoints and Prometheus metrics implementation
All AI-generated code was:
- Reviewed, modified, and tested by team members
- Annotated with detailed docstrings and comments
- Validated against security and functional requirements
Common Commands:
# View resources
kubectl get pods -n team-30-staging
kubectl logs -f deployment/django-backend -n team-30-staging
kubectl describe pod <pod-name> -n team-30-staging
# Exec into pod
kubectl exec -it deployment/django-backend -n team-30-staging -- /bin/bash
# Port forwarding
kubectl port-forward -n team-30-staging deployment/django-backend 8000:8000
# Scaling
kubectl scale deployment/django-backend --replicas=3 -n team-30-staging
# Apply changes (automatically excludes migration job)
kubectl apply -f k8s/ -n team-30-staging
# Rollback
kubectl rollout undo deployment/django-backend -n team-30-stagingDatabase Migrations:
# Run migrations manually (requires Jobs permission in RBAC)
kubectl apply -f k8s/django-migration-job.yaml -n team-30-staging
# Check migration job status
kubectl get jobs -n team-30-staging
kubectl logs job/django-migration -n team-30-staging
# Clean up completed migration job
kubectl delete job django-migration -n team-30-stagingRBAC Setup (one-time, requires cluster admin):
# Apply RBAC role and rolebinding for CI/CD ServiceAccount
# This grants permissions for Deployments, Services, Jobs, etc.
kubectl apply -f k8s/cicd-role.yaml -n team-30-staging
# Verify permissions
kubectl auth can-i create jobs --as=system:serviceaccount:team-30-staging:cicd-serviceaccount -n team-30-stagingTroubleshooting:
- ImagePullBackOff: Check
gitlab-registrysecret, recreate if expired - CrashLoopBackOff: Check logs with
kubectl logs <pod-name> - DB issues: Check PostgreSQL cluster status with
kubectl get cluster -n team-30-staging
Application:
- XSS mitigation (Django/Vue auto-escaping)
- CSRF protection enabled
- JWT authentication with secure signing
- Input validation via Django models and DRF serializers
Infrastructure:
- VM: UFW firewall, SSH key-only auth, Let's Encrypt HTTPS
- Kubernetes: RBAC for CI/CD, secrets management, private registry with
imagePullSecrets, TLS via cert-manager
Secrets Management:
- ✅ In Git: ConfigMaps, manifest structure
- ❌ Not in Git: Secret values, passwords, tokens, kubeconfig
- All CI/CD variables masked and protected
VM Deployment (Weeks 1-8):
- Single Ubuntu server with Nginx + Daphne + SQLite
- Simple setup, low overhead, easier debugging
- No HA, manual scaling, downtime during deploys
Kubernetes Deployment (Weeks 9-11):
- Distributed pods across cluster with PostgreSQL + Longhorn storage
- High availability, horizontal scaling, zero-downtime deployments
- Self-healing, comprehensive monitoring (Prometheus/Grafana)
- Higher complexity, more operational overhead
Key Changes: SQLite → PostgreSQL, local filesystem → Longhorn PVC, Ansible → GitLab CI + Kaniko + kubectl
MIT License — see LICENSE for details.