Musa Yılmaz
·6 min read

AWS Lightsail Django Deploy: From Nothing to Live

awsdjangodevopsdeployment

A full EC2 setup is powerful but heavy for small and mid-sized projects: VPC, security groups, elastic IPs, EBS — half a day gone before a simple site is live. Lightsail is the same infrastructure with the complexity stripped out: fixed monthly price, sensible network defaults, one management screen.

Here's how to take a Django app from nothing to live on Lightsail.

Choosing an instance

Lightsail plans are flat-rate and include bandwidth. If you're running Django + PostgreSQL + Next.js together — see running Next.js and Django on one origin with Docker Compose for that setup — take at least 2 GB of RAM. The 1 GB plan can run out of memory during Docker builds.

For the OS, pick "OS Only → Ubuntu LTS". Pre-built application images ship configurations that tend to conflict with your own setup.

Choose a region close to your users — for visitors in Türkiye, Frankfurt typically gives the lowest latency.

First connection and basic hardening

Lightsail gives you an SSH key. First job is updating the system:

sudo apt update && sudo apt upgrade -y

Then create a non-root user:

sudo adduser deploy
sudo usermod -aG sudo deploy
sudo rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy

Change two settings in the SSH config:

sudo nano /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
sudo systemctl restart ssh

Disabling password login neutralises the entire category of automated brute-force attempts against your server. Before you do it, make sure key-based login works — otherwise you'll lock yourself out. Open a second terminal and test; don't close your current session.

Firewall

Lightsail has its own network firewall (console → Networking tab). Leave only these open:

PortPurpose
22SSH
80HTTP (for HTTPS redirect and certificate validation)
443HTTPS

Never expose the database port (5432). If the app runs on the same server it doesn't need to be — it connects over Docker's internal network.

If you can restrict SSH to your own IP, do it; skip this if you're on a dynamic IP, or you may cut off your own access.

Installing Docker

curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker deploy

Log out and back in for the group change to apply. Verify:

docker run --rm hello-world

Getting the code onto the server

A deploy key lets the server pull without storing your credentials:

ssh-keygen -t ed25519 -C "lightsail-deploy" -f ~/.ssh/deploy_key
cat ~/.ssh/deploy_key.pub

Add that key under Deploy keys in your repository settings and do not grant write access. The server only needs to pull; it never needs to push.

git clone git@github.com:user/repo.git app
cd app

Create the environment file:

cp .env.example .env
nano .env
chmod 600 .env

Generate a solid Django SECRET_KEY:

python3 -c "import secrets; print(secrets.token_urlsafe(50))"

Bringing it up

docker compose up -d --build
docker compose run --rm backend python manage.py migrate
docker compose run --rm backend python manage.py collectstatic --noinput
docker compose run --rm backend python manage.py createsuperuser

Check the state:

docker compose ps
docker compose logs -f --tail=50

Pointing your domain

Create an A record in your DNS pointing at the Lightsail static IP.

Don't skip assigning a static IP (console → Networking → Create static IP). The default address isn't permanent; it can change on restart and take your site offline. In Lightsail a static IP is free while attached to an instance.

HTTPS certificate

Let's Encrypt issues free, auto-renewing certificates:

sudo apt install -y certbot
docker compose stop nginx
sudo certbot certonly --standalone -d example.com -d www.example.com
docker compose start nginx

Mount the certificates into the nginx container:

nginx:
  volumes:
    - /etc/letsencrypt:/etc/letsencrypt:ro
    - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro

Add the HTTPS block to nginx:

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}
 
server {
    listen 443 ssl;
    server_name example.com www.example.com;
 
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
 
    location / {
        proxy_pass http://frontend:3000;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Automate renewal with a cron entry:

sudo crontab -e
0 3 * * * certbot renew --quiet --deploy-hook "cd /home/deploy/app && docker compose restart nginx"

--deploy-hook runs only when a certificate was actually renewed, so nginx isn't restarted needlessly every night.

Django production settings

Before going live, confirm:

DEBUG = False
ALLOWED_HOSTS = ["example.com", "www.example.com"]
 
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
CSRF_TRUSTED_ORIGINS = ["https://example.com"]

Shipping with DEBUG = True exposes your environment variables and code structure on error pages. Django's own check command catches this class of problem:

docker compose run --rm backend python manage.py check --deploy

Backups

Lightsail's automatic snapshots are one click in the console and back up the whole disk daily. That's a good baseline, but a logical database dump is more flexible:

#!/bin/bash
# ~/backup.sh
set -euo pipefail
 
BACKUP_DIR="/home/deploy/backups"
mkdir -p "$BACKUP_DIR"
 
docker compose -f /home/deploy/app/docker-compose.yml exec -T postgres \
  pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" | gzip > "$BACKUP_DIR/db-$(date +%F).sql.gz"
 
# Remove backups older than 14 days
find "$BACKUP_DIR" -name "db-*.sql.gz" -mtime +14 -delete
chmod +x ~/backup.sh
crontab -e
30 2 * * * /home/deploy/backup.sh

Backups need a copy somewhere else — if the server disappears, so do the backups sitting on it. Add a line that copies them to object storage or another host.

And most importantly: test that you can restore. An untested backup isn't a backup.

Update workflow

After a code change:

cd ~/app
git pull
docker compose up -d --build
docker compose run --rm backend python manage.py migrate

Unused images fill the disk over time; prune occasionally:

docker image prune -f

If the app needs background jobs — emails, scheduled reports, retries — add a worker container to the same compose file rather than a separate server; see Django, Celery and Redis: Background Tasks Done Right for a production-ready setup.

When Lightsail isn't enough

Lightsail is a single machine with fixed resources. Move to EC2 or managed services when:

  • You need autoscaling as traffic grows
  • High availability (multiple instances behind a load balancer) is a requirement
  • You want the database to be a managed service

For projects a single server can handle, though, Lightsail's predictable cost and simple management carry you a long way.