Skip to content

Deploying a Full-Stack Portfolio to Production with Docker

Deploying a full-stack application to production can be daunting. How do you ensure consistency across environments? How do you manage dependencies? How do you handle database migrations without downtime? How do you scale your application when traffic increases?

In this article, I’ll walk you through the complete deployment protocol I use for my portfolio application. We’ll containerize a Flask backend, React frontend, and MySQL database, configure production-grade settings, and set up CI/CD pipelines for automated deployments.

Let’s start by understanding our application architecture:

  • Directoryruizdev7-portfolio/
    • Directorybackend/
      • Directoryportfolio_app/
        • __init__.py
        • config.py
        • Directorymodels/
        • Directoryresources/
        • Directoryschemas/
        • Directoryservices/
      • Directorymigrations/
      • Dockerfile
      • Dockerfile.dev
      • requirements.txt
      • wsgi.py
      • app.py
    • Directoryfrontend/
      • Directorysrc/
        • Directorycomponents/
        • Directorypages/
        • Directoryassets/
        • main.jsx
        • App.jsx
      • Directorypublic/
      • Directorydist/
      • Dockerfile
      • Dockerfile.dev
      • package.json
      • vite.config.js
      • nginx.conf
    • docker-compose.yml
    • docker-compose.production.yml
    • docker-compose.development.yml
    • .env
    • .env.example
    • build_and_deploy.sh

The backend uses Flask with Gunicorn as the WSGI server for better performance and concurrent request handling.

# backend/Dockerfile
FROM python:3.9-slim
# Set working directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \
gcc \
default-libmysqlclient-dev \
pkg-config \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements first for better caching
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create non-root user for security
RUN useradd -m -u 1000 appuser && \
chown -R appuser:appuser /app
USER appuser
# Expose port
EXPOSE 6000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:6000/api/v1/health')"
# Run with Gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:6000", \
"--workers", "4", \
"--threads", "2", \
"--timeout", "60", \
"--access-logfile", "-", \
"--error-logfile", "-", \
"--log-level", "info", \
"wsgi:app"]
backend/Dockerfile.dev
FROM python:3.9-slim
WORKDIR /app
# Install system dependencies and useful tools
RUN apt-get update && \
apt-get install -y \
gcc \
default-libmysqlclient-dev \
pkg-config \
vim \
curl \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Development environment variable
ENV FLASK_ENV=development
ENV FLASK_DEBUG=1
EXPOSE 6000
# Run Flask development server with auto-reload
CMD ["flask", "run", "--host=0.0.0.0", "--port=6000"]
backend/wsgi.py
"""
WSGI entry point for production deployment.
Gunicorn uses this file to run the Flask application.
"""
from portfolio_app import create_app
# Create application instance
app = create_app()
if __name__ == "__main__":
# This is used when running with 'python wsgi.py' for debugging
app.run(host="0.0.0.0", port=6000, debug=False)
backend/portfolio_app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_cors import CORS
from flask_jwt_extended import JWTManager
from flask_marshmallow import Marshmallow
# Initialize extensions
db = SQLAlchemy()
migrate = Migrate()
ma = Marshmallow()
jwt = JWTManager()
def create_app(config_name='production'):
"""Application factory pattern"""
app = Flask(__name__)
# Load configuration
if config_name == 'development':
app.config.from_object('portfolio_app.config.DevelopmentConfig')
elif config_name == 'production':
app.config.from_object('portfolio_app.config.ProductionConfig')
else:
app.config.from_object('portfolio_app.config.Config')
# Initialize extensions with app
db.init_app(app)
migrate.init_app(app, db)
ma.init_app(app)
jwt.init_app(app)
# Configure CORS
CORS(app, resources={
r"/api/*": {
"origins": app.config['CORS_ORIGINS'],
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
"allow_headers": ["Content-Type", "Authorization"]
}
})
# Register blueprints
from portfolio_app.resources.resource_posts import blueprint_api_post
from portfolio_app.resources.resource_users import blueprint_api_user
from portfolio_app.resources.resource_authorization import blueprint_api_auth
from portfolio_app.resources.resource_pumps import blueprint_api_pump
from portfolio_app.resources.resource_contact import blueprint_api_contact
app.register_blueprint(blueprint_api_post)
app.register_blueprint(blueprint_api_user)
app.register_blueprint(blueprint_api_auth)
app.register_blueprint(blueprint_api_pump)
app.register_blueprint(blueprint_api_contact)
# Health check endpoint
@app.route('/api/v1/health')
def health_check():
return {'status': 'healthy', 'service': 'portfolio-backend'}, 200
return app
backend/portfolio_app/config.py
import os
from datetime import timedelta
class Config:
"""Base configuration"""
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production')
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY', 'jwt-secret-key-change-in-production')
JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1)
JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=30)
# Database configuration
DB_USER = os.environ.get('DB_USER', 'root')
DB_PASSWORD = os.environ.get('DB_PASSWORD', 'root')
DB_HOST = os.environ.get('DB_HOST', 'localhost')
DB_PORT = os.environ.get('DB_PORT', '3306')
DB_NAME = os.environ.get('DB_NAME', 'portfolio_db')
SQLALCHEMY_DATABASE_URI = (
f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
)
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_ENGINE_OPTIONS = {
'pool_pre_ping': True,
'pool_recycle': 300,
'pool_size': 10,
'max_overflow': 20
}
class DevelopmentConfig(Config):
"""Development configuration"""
DEBUG = True
FLASK_ENV = 'development'
CORS_ORIGINS = ['http://localhost:3000', 'http://localhost:5173']
class ProductionConfig(Config):
"""Production configuration"""
DEBUG = False
FLASK_ENV = 'production'
CORS_ORIGINS = [
'https://ruizdev7.com',
'https://www.ruizdev7.com'
]
# Production-specific settings
SQLALCHEMY_ECHO = False
PREFERRED_URL_SCHEME = 'https'
# Security headers
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'

The frontend uses a multi-stage build to optimize the final image size and serve the static files with Nginx.

# frontend/Dockerfile
# Stage 1: Build the React application
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy application code
COPY . .
# Build the application
RUN npm run build
# Stage 2: Serve with Nginx
FROM nginx:alpine
# Copy custom Nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf
# Copy built files from builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy environment configuration script
COPY env.sh /docker-entrypoint.d/env.sh
RUN chmod +x /docker-entrypoint.d/env.sh
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:3000/ || exit 1
# Nginx runs as daemon off by default in official image
CMD ["nginx", "-g", "daemon off;"]
frontend/Dockerfile.dev
FROM node:18-alpine
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm install
# Copy application code
COPY . .
EXPOSE 3000
# Run development server with hot reload
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "3000"]
frontend/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
}
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;
client_max_body_size 20M;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript
application/x-javascript application/xml+rss
application/javascript application/json;
server {
listen 3000;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# React Router support - SPA fallback
location / {
try_files $uri $uri/ /index.html;
}
# API proxy (if needed)
location /api/ {
proxy_pass http://backend:6000;
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;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Disable caching for index.html
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
}
}
frontend/vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
host: true,
port: 3000,
watch: {
usePolling: true
}
},
build: {
outDir: 'dist',
sourcemap: false,
minify: 'terser',
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom', 'react-router-dom'],
redux: ['@reduxjs/toolkit', 'react-redux'],
ui: ['@headlessui/react', '@heroicons/react']
}
}
},
chunkSizeWarningLimit: 1000
},
preview: {
port: 3000,
host: true
}
})
docker-compose.production.yml
version: '3.8'
services:
database:
image: mysql:8.0
container_name: portfolio-database
restart: always
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- db_data:/var/lib/mysql
- ./backend/init.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- "3306:3306"
networks:
- portfolio-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${DB_ROOT_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 5
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: portfolio-backend
restart: always
environment:
FLASK_ENV: production
DB_HOST: database
DB_PORT: 3306
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
DB_NAME: ${DB_NAME}
SECRET_KEY: ${SECRET_KEY}
JWT_SECRET_KEY: ${JWT_SECRET_KEY}
ports:
- "8000:6000"
depends_on:
database:
condition: service_healthy
networks:
- portfolio-network
volumes:
- ./backend/portfolio_app/static:/app/portfolio_app/static
command: >
sh -c "flask db upgrade &&
gunicorn --bind 0.0.0.0:6000
--workers 4
--threads 2
--timeout 60
--access-logfile -
--error-logfile -
wsgi:app"
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: portfolio-frontend
restart: always
environment:
VITE_API_URL: ${VITE_API_URL}
VITE_APP_NAME: ${VITE_APP_NAME}
ports:
- "3000:3000"
depends_on:
- backend
networks:
- portfolio-network
nginx-proxy:
image: nginx:alpine
container_name: portfolio-nginx
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- certbot-data:/var/www/certbot
depends_on:
- frontend
- backend
networks:
- portfolio-network
certbot:
image: certbot/certbot
container_name: portfolio-certbot
volumes:
- ./nginx/ssl:/etc/letsencrypt
- certbot-data:/var/www/certbot
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
networks:
portfolio-network:
driver: bridge
volumes:
db_data:
driver: local
certbot-data:
driver: local
docker-compose.development.yml
version: '3.8'
services:
database:
image: mysql:8.0
container_name: portfolio-database-dev
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: portfolio_dev
MYSQL_USER: portfolio_user
MYSQL_PASSWORD: portfolio_pass
volumes:
- db_data_dev:/var/lib/mysql
ports:
- "3307:3306"
networks:
- portfolio-network-dev
backend:
build:
context: ./backend
dockerfile: Dockerfile.dev
container_name: portfolio-backend-dev
environment:
FLASK_ENV: development
FLASK_DEBUG: 1
DB_HOST: database
DB_PORT: 3306
DB_USER: portfolio_user
DB_PASSWORD: portfolio_pass
DB_NAME: portfolio_dev
ports:
- "8000:6000"
depends_on:
- database
networks:
- portfolio-network-dev
volumes:
- ./backend:/app
- /app/.venv
command: flask run --host=0.0.0.0 --port=6000
frontend:
build:
context: ./frontend
dockerfile: Dockerfile.dev
container_name: portfolio-frontend-dev
environment:
VITE_API_URL: http://localhost:8000
ports:
- "5173:3000"
depends_on:
- backend
networks:
- portfolio-network-dev
volumes:
- ./frontend:/app
- /app/node_modules
stdin_open: true
tty: true
networks:
portfolio-network-dev:
driver: bridge
volumes:
db_data_dev:
driver: local
.env.example
# Copy this file to .env and fill in your values
# Database Configuration
DB_ROOT_PASSWORD=your_secure_root_password
DB_NAME=portfolio_db
DB_USER=portfolio_user
DB_PASSWORD=your_secure_password
# Flask Configuration
SECRET_KEY=your-secret-key-min-32-chars
JWT_SECRET_KEY=your-jwt-secret-key-min-32-chars
FLASK_ENV=production
# Frontend Configuration
VITE_API_URL=https://api.ruizdev7.com
VITE_APP_NAME=ruizdev7 Portfolio
# Domain Configuration
DOMAIN=ruizdev7.com
EMAIL=ruizdev7@outlook.com
build_and_deploy.sh
#!/bin/bash
set -e # Exit on error
echo "🚀 Starting deployment process..."
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Configuration
COMPOSE_FILE="docker-compose.production.yml"
BACKUP_DIR="./backups"
DATE=$(date +%Y%m%d_%H%M%S)
# Function to print colored output
print_status() {
echo -e "${GREEN}[INFO]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check if .env file exists
if [ ! -f .env ]; then
print_error ".env file not found!"
print_status "Please copy .env.example to .env and configure it"
exit 1
fi
# Load environment variables
source .env
# Create backup directory if it doesn't exist
mkdir -p $BACKUP_DIR
# Backup database
print_status "Creating database backup..."
docker-compose -f $COMPOSE_FILE exec -T database \
mysqldump -u root -p${DB_ROOT_PASSWORD} ${DB_NAME} \
> "${BACKUP_DIR}/db_backup_${DATE}.sql" 2>/dev/null || \
print_warning "Database backup failed (might be first deployment)"
# Pull latest changes
print_status "Pulling latest code..."
git pull origin main
# Build images
print_status "Building Docker images..."
docker-compose -f $COMPOSE_FILE build --no-cache
# Stop old containers
print_status "Stopping old containers..."
docker-compose -f $COMPOSE_FILE down
# Start new containers
print_status "Starting new containers..."
docker-compose -f $COMPOSE_FILE up -d
# Wait for services to be healthy
print_status "Waiting for services to be healthy..."
sleep 10
# Run database migrations
print_status "Running database migrations..."
docker-compose -f $COMPOSE_FILE exec -T backend flask db upgrade
# Check service health
print_status "Checking service health..."
BACKEND_HEALTH=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/api/v1/health)
FRONTEND_HEALTH=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/)
if [ "$BACKEND_HEALTH" -eq 200 ] && [ "$FRONTEND_HEALTH" -eq 200 ]; then
print_status "✅ Deployment successful!"
print_status "Backend health: $BACKEND_HEALTH"
print_status "Frontend health: $FRONTEND_HEALTH"
else
print_error "⚠️ Deployment completed but health checks failed"
print_error "Backend health: $BACKEND_HEALTH"
print_error "Frontend health: $FRONTEND_HEALTH"
exit 1
fi
# Clean up old images
print_status "Cleaning up old images..."
docker image prune -f
print_status "🎉 Deployment completed successfully!"
print_status "Access your application at: http://localhost:3000"

Make the script executable:

Terminal window
chmod +x build_and_deploy.sh
Terminal window
# Inside the backend container
docker-compose exec backend flask db init
docker-compose exec backend flask db migrate -m "Initial migration"
docker-compose exec backend flask db upgrade
Terminal window
# After modifying models
docker-compose exec backend flask db migrate -m "Description of changes"
docker-compose exec backend flask db upgrade
  1. Always backup before migrating
  2. Test migrations in development first
  3. Review generated migration files
  4. Handle data transformations carefully
  5. Document breaking changes
# nginx/nginx.conf (SSL enabled)
http {
# ... previous configuration ...
# Redirect HTTP to HTTPS
server {
listen 80;
server_name ruizdev7.com www.ruizdev7.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$server_name$request_uri;
}
}
# HTTPS server
server {
listen 443 ssl http2;
server_name ruizdev7.com www.ruizdev7.com;
ssl_certificate /etc/letsencrypt/live/ruizdev7.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ruizdev7.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Backend proxy
location /api/ {
proxy_pass http://backend:6000;
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;
}
# Frontend
location / {
proxy_pass http://frontend:3000;
proxy_http_version 1.1;
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;
}
}
}
Terminal window
# Initial certificate generation
docker-compose run --rm certbot certonly \
--webroot \
--webroot-path=/var/www/certbot \
--email ruizdev7@outlook.com \
--agree-tos \
--no-eff-email \
-d ruizdev7.com \
-d www.ruizdev7.com
Terminal window
# View all logs
docker-compose -f docker-compose.production.yml logs -f
# View specific service logs
docker-compose -f docker-compose.production.yml logs -f backend
docker-compose -f docker-compose.production.yml logs -f frontend
# View last 100 lines
docker-compose -f docker-compose.production.yml logs --tail=100 backend
backend/portfolio_app/__init__.py
@app.route('/api/v1/health')
def health_check():
"""Comprehensive health check"""
try:
# Check database connection
db.session.execute('SELECT 1')
db_status = 'healthy'
except Exception as e:
db_status = f'unhealthy: {str(e)}'
return {
'status': 'healthy',
'service': 'portfolio-backend',
'database': db_status,
'timestamp': datetime.now().isoformat()
}, 200
.github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up SSH
uses: webfactory/ssh-agent@v0.7.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Deploy to server
run: |
ssh -o StrictHostKeyChecking=no ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'EOF'
cd /var/www/ruizdev7-portfolio
git pull origin main
./build_and_deploy.sh
EOF
- name: Notify deployment
if: success()
run: echo "Deployment successful!"
  1. Database Connection Pooling
  2. Query Optimization with Indexes
  3. Caching with Redis (optional)
  4. Gunicorn Workers Configuration
  1. Code Splitting
  2. Lazy Loading Routes
  3. Image Optimization
  4. Gzip Compression
  5. CDN Integration (optional)
Terminal window
# Check logs
docker-compose logs container_name
# Inspect container
docker inspect container_name
# Check if port is already in use
lsof -i :8000
Terminal window
# Verify database is running
docker-compose ps
# Check database logs
docker-compose logs database
# Test connection manually
docker-compose exec backend python -c "from portfolio_app import db; db.session.execute('SELECT 1')"
Terminal window
# Downgrade and re-upgrade
docker-compose exec backend flask db downgrade
docker-compose exec backend flask db upgrade
# Force migration (use with caution)
docker-compose exec backend flask db stamp head
backup.sh
# Create automated backup script
#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
docker-compose exec -T database \
mysqldump -u root -p${DB_ROOT_PASSWORD} ${DB_NAME} \
| gzip > "backup_${DATE}.sql.gz"
Terminal window
# Update Python packages
docker-compose exec backend pip list --outdated
docker-compose exec backend pip install --upgrade package_name
# Update Node packages
docker-compose exec frontend npm outdated
docker-compose exec frontend npm update
Terminal window
# Update base images
docker-compose pull
docker-compose build --no-cache
docker-compose up -d

You now have a complete production deployment setup with:

  • ✅ Containerized Flask backend with Gunicorn
  • ✅ Optimized React frontend with Nginx
  • ✅ MySQL database with persistence
  • ✅ SSL certificates with Let’s Encrypt
  • ✅ Automated deployment scripts
  • ✅ Health checks and monitoring
  • ✅ CI/CD pipeline integration

This setup provides a solid foundation for deploying full-stack applications to production. You can extend it further by adding:

  • Redis caching layer
  • Elasticsearch for search functionality
  • Load balancing with multiple instances
  • Automated testing in CI/CD
  • Monitoring with Prometheus and Grafana

The complete deployment setup is available in my GitHub repository.

Next Steps: Check out my article on implementing RBAC and securing your API endpoints!


Questions or suggestions? Connect with me on LinkedIn!