At some point, every developer faces the same moment: the app works perfectly on localhost, and now it needs to run for real users, all the time, without your laptop. Cloud deployment is the bridge between a side project and a real product. This guide covers both AWS and GCP — the two platforms you'll encounter most in the industry — with practical, copy-pasteable configurations rather than marketing slides.
Why Cloud Over Traditional Hosting?
Traditional VPS hosting (DigitalOcean Droplets, Linode VMs) is still a valid option for small applications. But cloud platforms offer something traditional hosting doesn't: managed services. Instead of setting up and maintaining a PostgreSQL server yourself, you use RDS (AWS) or Cloud SQL (GCP) — the database is patched, backed up, and replicated automatically. Instead of configuring a load balancer, you use AWS ALB or GCP Load Balancer. Managed services have a cost premium but they trade money for operational complexity.
- Auto-scaling: handle traffic spikes without pre-provisioning
- Managed databases: backups, replication, and patching handled for you
- Global CDN: serve static files from edge locations worldwide
- IAM: fine-grained access control for every resource
- Pay-as-you-go: no hardware costs, idle resources cost nothing
Containerise First: Docker Is the Foundation
Before deploying to any cloud, containerise your application. Docker packages your app and all its dependencies into a single portable image. This eliminates 'works on my machine' problems and makes deployment consistent across environments. Both AWS and GCP have first-class Docker support.
# Dockerfile for a Django application
FROM python:3.12-slim
# Prevent Python from writing .pyc files
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
libpq-dev gcc \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Collect static files
RUN python manage.py collectstatic --noinput
EXPOSE 8000
# Use gunicorn for production
CMD ["gunicorn", "myproject.wsgi:application", \
"--bind", "0.0.0.0:8000", \
"--workers", "4", \
"--worker-class", "gthread", \
"--threads", "2", \
"--timeout", "120"]# docker-compose.yml for local development
version: "3.9"
services:
web:
build: .
command: python manage.py runserver 0.0.0.0:8000
volumes:
- .:/app
ports:
- "8000:8000"
environment:
- DEBUG=True
- DATABASE_URL=postgres://postgres:password@db:5432/myapp
depends_on:
- db
- redis
db:
image: postgres:16
environment:
POSTGRES_DB: myapp
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
postgres_data:AWS Deployment: EC2 with Nginx and Gunicorn
EC2 (Elastic Compute Cloud) is AWS's virtual machine service. For a typical web application, the deployment stack is: EC2 instance running your app (via Docker/Gunicorn) behind an Nginx reverse proxy, with an RDS PostgreSQL database and S3 for static/media files.
Step 1: Launch an EC2 instance
- 1Go to EC2 Dashboard → Launch Instance
- 2Choose Amazon Linux 2023 or Ubuntu 22.04
- 3Select t3.small (2GB RAM) minimum for a real app
- 4Create a key pair (.pem file) and download it
- 5Configure security group: allow port 80 (HTTP), 443 (HTTPS), 22 (SSH from your IP only)
- 6Launch and note the public IP
# Connect to your EC2 instance
chmod 400 my-key.pem
ssh -i my-key.pem ec2-user@YOUR_EC2_PUBLIC_IP
# Install Docker
sudo yum update -y
sudo yum install -y docker
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker ec2-user
# Install Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
# Clone your repo and deploy
git clone https://github.com/yourname/yourapp.git
cd yourapp
cp .env.example .env # fill in production values
docker-compose -f docker-compose.prod.yml up -dNginx as reverse proxy
# /etc/nginx/sites-available/myapp
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
# Redirect HTTP to HTTPS (after SSL setup)
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
client_max_body_size 20M;
location /static/ {
alias /app/staticfiles/;
expires 30d;
add_header Cache-Control "public, immutable";
}
location / {
proxy_pass http://127.0.0.1:8000;
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_read_timeout 120s;
}
}AWS RDS: Managed PostgreSQL
Never run your production database on the same EC2 instance as your application. Use RDS — it handles automated backups, failover, and security patches. Set it up in the same VPC as your EC2 instance so traffic stays private.
# RDS setup via AWS CLI
aws rds create-db-instance \
--db-instance-identifier myapp-prod \
--db-instance-class db.t3.micro \
--engine postgres \
--engine-version 16.1 \
--master-username dbadmin \
--master-user-password YourStrongPassword \
--allocated-storage 20 \
--vpc-security-group-ids sg-xxxxxxxx \
--db-subnet-group-name myapp-subnet-group \
--backup-retention-period 7 \
--no-publicly-accessible
# In your Django settings:
# DATABASE_URL=postgres://dbadmin:password@your-rds-endpoint.rds.amazonaws.com:5432/myappGCP: Cloud Run for Serverless Container Deployment
Google Cloud Run is serverless — you deploy a Docker container and GCP handles scaling from zero to thousands of requests automatically. You pay only for the time your container is actually handling requests. For most web applications, Cloud Run is simpler, cheaper, and faster to deploy than EC2.
# Install and authenticate gcloud CLI
gcloud auth login
gcloud config set project YOUR_PROJECT_ID
# Build and push container to Google Container Registry
gcloud builds submit --tag gcr.io/YOUR_PROJECT_ID/myapp
# Deploy to Cloud Run
gcloud run deploy myapp \
--image gcr.io/YOUR_PROJECT_ID/myapp \
--platform managed \
--region us-central1 \
--allow-unauthenticated \
--port 8000 \
--memory 512Mi \
--cpu 1 \
--min-instances 0 \
--max-instances 10 \
--set-env-vars="DATABASE_URL=postgres://...,SECRET_KEY=...,DEBUG=False"
# Cloud Run gives you a URL like:
# https://myapp-xxxxxxxx-uc.a.run.appCI/CD with GitHub Actions: Automate Everything
Manually SSH-ing into a server to deploy is not a production process. Automate your deployments with GitHub Actions — every push to main triggers a build, test, and deploy pipeline.
# .github/workflows/deploy.yml
name: Deploy to Cloud Run
on:
push:
branches: [main]
env:
PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}
SERVICE: myapp
REGION: us-central1
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -r requirements.txt
- run: python manage.py test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- id: auth
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
- uses: google-github-actions/setup-gcloud@v2
- name: Build and push Docker image
run: |
gcloud builds submit --tag gcr.io/$PROJECT_ID/$SERVICE
- name: Deploy to Cloud Run
run: |
gcloud run deploy $SERVICE \\
--image gcr.io/$PROJECT_ID/$SERVICE \\
--platform managed \\
--region $REGION \\
--quiet💡 Tip
Store all secrets (database passwords, API keys, SECRET_KEY) in GitHub Secrets or GCP Secret Manager — never in your code or environment files committed to git. Use python-decouple or pydantic-settings to load them at runtime.
Cost Optimisation: Running on a Budget
- Use Cloud Run (GCP) or Lambda (AWS) for low-traffic apps — pay per request, not per hour
- Set up billing alerts at $10, $50, and $100 thresholds so surprises don't happen
- Use db.t3.micro RDS for dev/staging; upgrade only when you have real load
- Enable S3 Intelligent-Tiering for media files to automatically reduce storage costs
- Use reserved instances or committed use discounts when traffic is predictable
- Delete unused resources — old snapshots, stopped instances, and idle load balancers all cost money