AWS Lightsail Django Deploy: From Nothing to Live
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 -yThen create a non-root user:
sudo adduser deploy
sudo usermod -aG sudo deploy
sudo rsync --archive --chown=deploy:deploy ~/.ssh /home/deployChange two settings in the SSH config:
sudo nano /etc/ssh/sshd_configPermitRootLogin no
PasswordAuthentication no
sudo systemctl restart sshDisabling 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:
| Port | Purpose |
|---|---|
| 22 | SSH |
| 80 | HTTP (for HTTPS redirect and certificate validation) |
| 443 | HTTPS |
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 deployLog out and back in for the group change to apply. Verify:
docker run --rm hello-worldGetting 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.pubAdd 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 appCreate the environment file:
cp .env.example .env
nano .env
chmod 600 .envGenerate 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 createsuperuserCheck the state:
docker compose ps
docker compose logs -f --tail=50Pointing 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 nginxMount the certificates into the nginx container:
nginx:
volumes:
- /etc/letsencrypt:/etc/letsencrypt:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:roAdd 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 -e0 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 --deployBackups
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 -deletechmod +x ~/backup.sh
crontab -e30 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 migrateUnused images fill the disk over time; prune occasionally:
docker image prune -fIf 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.