Deploying a Full-Stack Portfolio to Production with Docker
Introduction
Section titled “Introduction”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.
Project Structure
Section titled “Project Structure”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
Backend Dockerfile: Flask in Production
Section titled “Backend Dockerfile: Flask in Production”The backend uses Flask with Gunicorn as the WSGI server for better performance and concurrent request handling.
Production Dockerfile
Section titled “Production Dockerfile”# backend/DockerfileFROM python:3.9-slim
# Set working directoryWORKDIR /app
# Install system dependenciesRUN 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 cachingCOPY requirements.txt .
# Install Python dependenciesRUN pip install --no-cache-dir -r requirements.txt
# Copy application codeCOPY . .
# Create non-root user for securityRUN useradd -m -u 1000 appuser && \ chown -R appuser:appuser /appUSER appuser
# Expose portEXPOSE 6000
# Health checkHEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \ CMD python -c "import requests; requests.get('http://localhost:6000/api/v1/health')"
# Run with GunicornCMD ["gunicorn", "--bind", "0.0.0.0:6000", \ "--workers", "4", \ "--threads", "2", \ "--timeout", "60", \ "--access-logfile", "-", \ "--error-logfile", "-", \ "--log-level", "info", \ "wsgi:app"]Development Dockerfile
Section titled “Development Dockerfile”FROM python:3.9-slim
WORKDIR /app
# Install system dependencies and useful toolsRUN 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 variableENV FLASK_ENV=developmentENV FLASK_DEBUG=1
EXPOSE 6000
# Run Flask development server with auto-reloadCMD ["flask", "run", "--host=0.0.0.0", "--port=6000"]WSGI Entry Point
Section titled “WSGI Entry Point”"""WSGI entry point for production deployment.Gunicorn uses this file to run the Flask application."""from portfolio_app import create_app
# Create application instanceapp = 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)Application Factory Pattern
Section titled “Application Factory Pattern”from flask import Flaskfrom flask_sqlalchemy import SQLAlchemyfrom flask_migrate import Migratefrom flask_cors import CORSfrom flask_jwt_extended import JWTManagerfrom flask_marshmallow import Marshmallow
# Initialize extensionsdb = 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 appProduction Configuration
Section titled “Production Configuration”import osfrom 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'Frontend Dockerfile: React with Nginx
Section titled “Frontend Dockerfile: React with Nginx”The frontend uses a multi-stage build to optimize the final image size and serve the static files with Nginx.
Production Dockerfile
Section titled “Production Dockerfile”# frontend/Dockerfile# Stage 1: Build the React applicationFROM node:18-alpine AS builder
WORKDIR /app
# Copy package filesCOPY package*.json ./
# Install dependenciesRUN npm ci --only=production
# Copy application codeCOPY . .
# Build the applicationRUN npm run build
# Stage 2: Serve with NginxFROM nginx:alpine
# Copy custom Nginx configurationCOPY nginx.conf /etc/nginx/nginx.conf
# Copy built files from builder stageCOPY --from=builder /app/dist /usr/share/nginx/html
# Copy environment configuration scriptCOPY env.sh /docker-entrypoint.d/env.shRUN chmod +x /docker-entrypoint.d/env.sh
# Expose portEXPOSE 3000
# Health checkHEALTHCHECK --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 imageCMD ["nginx", "-g", "daemon off;"]Development Dockerfile
Section titled “Development Dockerfile”FROM node:18-alpine
WORKDIR /app
# Install dependenciesCOPY package*.json ./RUN npm install
# Copy application codeCOPY . .
EXPOSE 3000
# Run development server with hot reloadCMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "3000"]Nginx Configuration
Section titled “Nginx Configuration”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"; } }}Vite Configuration for Production
Section titled “Vite Configuration for Production”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: Orchestrating Services
Section titled “Docker Compose: Orchestrating Services”Production Docker Compose
Section titled “Production Docker Compose”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: localDevelopment Docker Compose
Section titled “Development Docker Compose”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: localEnvironment Configuration
Section titled “Environment Configuration”Environment Variables
Section titled “Environment Variables”# Copy this file to .env and fill in your values
# Database ConfigurationDB_ROOT_PASSWORD=your_secure_root_passwordDB_NAME=portfolio_dbDB_USER=portfolio_userDB_PASSWORD=your_secure_password
# Flask ConfigurationSECRET_KEY=your-secret-key-min-32-charsJWT_SECRET_KEY=your-jwt-secret-key-min-32-charsFLASK_ENV=production
# Frontend ConfigurationVITE_API_URL=https://api.ruizdev7.comVITE_APP_NAME=ruizdev7 Portfolio
# Domain ConfigurationDOMAIN=ruizdev7.comEMAIL=ruizdev7@outlook.comDeployment Scripts
Section titled “Deployment Scripts”Automated Build and Deploy Script
Section titled “Automated Build and Deploy Script”#!/bin/bashset -e # Exit on error
echo "🚀 Starting deployment process..."
# Colors for outputRED='\033[0;31m'GREEN='\033[0;32m'YELLOW='\033[1;33m'NC='\033[0m' # No Color
# ConfigurationCOMPOSE_FILE="docker-compose.production.yml"BACKUP_DIR="./backups"DATE=$(date +%Y%m%d_%H%M%S)
# Function to print colored outputprint_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 existsif [ ! -f .env ]; then print_error ".env file not found!" print_status "Please copy .env.example to .env and configure it" exit 1fi
# Load environment variablessource .env
# Create backup directory if it doesn't existmkdir -p $BACKUP_DIR
# Backup databaseprint_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 changesprint_status "Pulling latest code..."git pull origin main
# Build imagesprint_status "Building Docker images..."docker-compose -f $COMPOSE_FILE build --no-cache
# Stop old containersprint_status "Stopping old containers..."docker-compose -f $COMPOSE_FILE down
# Start new containersprint_status "Starting new containers..."docker-compose -f $COMPOSE_FILE up -d
# Wait for services to be healthyprint_status "Waiting for services to be healthy..."sleep 10
# Run database migrationsprint_status "Running database migrations..."docker-compose -f $COMPOSE_FILE exec -T backend flask db upgrade
# Check service healthprint_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 1fi
# Clean up old imagesprint_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:
chmod +x build_and_deploy.shDatabase Migrations
Section titled “Database Migrations”Initial Migration Setup
Section titled “Initial Migration Setup”# Inside the backend containerdocker-compose exec backend flask db initdocker-compose exec backend flask db migrate -m "Initial migration"docker-compose exec backend flask db upgradeCreating New Migrations
Section titled “Creating New Migrations”# After modifying modelsdocker-compose exec backend flask db migrate -m "Description of changes"docker-compose exec backend flask db upgradeMigration Best Practices
Section titled “Migration Best Practices”- Always backup before migrating
- Test migrations in development first
- Review generated migration files
- Handle data transformations carefully
- Document breaking changes
SSL Configuration with Let’s Encrypt
Section titled “SSL Configuration with Let’s Encrypt”Nginx SSL Configuration
Section titled “Nginx SSL Configuration”# 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; } }}Obtaining SSL Certificates
Section titled “Obtaining SSL Certificates”# Initial certificate generationdocker-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.comMonitoring and Logging
Section titled “Monitoring and Logging”Viewing Logs
Section titled “Viewing Logs”# View all logsdocker-compose -f docker-compose.production.yml logs -f
# View specific service logsdocker-compose -f docker-compose.production.yml logs -f backenddocker-compose -f docker-compose.production.yml logs -f frontend
# View last 100 linesdocker-compose -f docker-compose.production.yml logs --tail=100 backendHealth Check Endpoint
Section titled “Health Check Endpoint”@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() }, 200CI/CD with GitHub Actions
Section titled “CI/CD with GitHub Actions”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!"Performance Optimization
Section titled “Performance Optimization”Backend Optimizations
Section titled “Backend Optimizations”- Database Connection Pooling
- Query Optimization with Indexes
- Caching with Redis (optional)
- Gunicorn Workers Configuration
Frontend Optimizations
Section titled “Frontend Optimizations”- Code Splitting
- Lazy Loading Routes
- Image Optimization
- Gzip Compression
- CDN Integration (optional)
Troubleshooting Common Issues
Section titled “Troubleshooting Common Issues”Issue 1: Container Won’t Start
Section titled “Issue 1: Container Won’t Start”# Check logsdocker-compose logs container_name
# Inspect containerdocker inspect container_name
# Check if port is already in uselsof -i :8000Issue 2: Database Connection Failed
Section titled “Issue 2: Database Connection Failed”# Verify database is runningdocker-compose ps
# Check database logsdocker-compose logs database
# Test connection manuallydocker-compose exec backend python -c "from portfolio_app import db; db.session.execute('SELECT 1')"Issue 3: Migration Errors
Section titled “Issue 3: Migration Errors”# Downgrade and re-upgradedocker-compose exec backend flask db downgradedocker-compose exec backend flask db upgrade
# Force migration (use with caution)docker-compose exec backend flask db stamp headMaintenance Tasks
Section titled “Maintenance Tasks”Regular Backups
Section titled “Regular Backups”# Create automated backup script#!/bin/bashDATE=$(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"Updating Dependencies
Section titled “Updating Dependencies”# Update Python packagesdocker-compose exec backend pip list --outdateddocker-compose exec backend pip install --upgrade package_name
# Update Node packagesdocker-compose exec frontend npm outdateddocker-compose exec frontend npm updateSecurity Updates
Section titled “Security Updates”# Update base imagesdocker-compose pulldocker-compose build --no-cachedocker-compose up -dConclusion
Section titled “Conclusion”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!