Python Django Deployment Guide on DigitalOcean Droplets 2026
คู่มือนี้พาไล่ทีละขั้นตอนสำหรับ deploy เว็บแอป Python Django บน DigitalOcean Droplet ตั้งแต่เตรียมเครื่องด้วย virtualenv ติดตั้ง Gunicorn เชื่อมต่อ PostgreSQL ไปจนถึงตั้งค่า Nginx reverse proxy และเปิด SSL ฟรีด้วย Let's Encrypt พร้อมคำสั่งจริงที่ใช้รันได้ทันทีในแต่ละขั้น เหมาะสำหรับนักพัฒนาที่ต้องการควบคุม production environment เองแบบเต็มรูปแบบโดยไม่ผ่าน PaaS
This guide walks you through deploying a Python Django web application on a DigitalOcean Droplet step-by-step, from preparing the machine with virtualenv, installing Gunicorn, connecting to PostgreSQL, configuring an Nginx reverse proxy, and enabling free SSL with Let's Encrypt—complete with real commands ready to run at each stage. Ideal for developers who want full control over their production environment without relying on a PaaS platform.
Contents
Setting Up Droplet and Python virtualenv
Before deploying Django, choose a Droplet size appropriate for your project. For small to medium-sized applications with low traffic, a $6/month Basic Droplet (1 GiB RAM, 1 vCPU, 25 GB SSD, 1,000 GiB transfer) is sufficient to run a web application with Gunicorn and Nginx on the same machine. If you expect concurrent users or plan to run PostgreSQL on the same machine, consider upgrading to the $12/month plan (2 GiB RAM, 1 vCPU, 50 GB SSD, 2,000 GiB transfer) to avoid memory issues. For users in Thailand, choose the sgp1 region (Singapore) for the lowest latency, followed by blr1 (Bangalore). Once your Droplet is created with SSH key authentication, SSH into the machine with ssh root@your_droplet_ip. Always update the system first with apt update && apt upgrade -y. Next, create a separate non-root user for security using adduser deployer, then grant sudo privileges with usermod -aG sudo deployer. Switch to the new user with su - deployer. Install Python and essential tools for creating virtual environments: sudo apt install python3-venv python3-pip build-essential -y. Python 3 comes pre-installed with Ubuntu 24.04 LTS, but you need to install the venv package separately. When ready, create a project folder and virtual environment for each app to prevent library version conflicts between multiple projects on the same Droplet: mkdir -p ~/myproject && cd ~/myproject followed by python3 -m venv venv, then activate it with source venv/bin/activate. The terminal prompt will change to show (venv) at the beginning, confirming the virtual environment is active. From this point on, all Python packages installed via pip will be stored separately in the venv folder, not mixed with the system. This is the standard practice for deploying Django to production.
- Basic Droplet $6/month (1 GiB RAM) sufficient for small apps; $12/month (2 GiB RAM) recommended if running database on the same machine
- sgp1 region (Singapore) provides lowest latency for Thai users, followed by blr1 (Bangalore)
- Always create a non-root user with adduser + usermod -aG sudo before production use
Installing Django and Gunicorn
With the virtual environment activated, install the three essential packages for production Django deployment with one command: pip install django gunicorn psycopg2-binary. Django is the core framework, Gunicorn is the WSGI HTTP server that runs your Python application instead of Django's built-in development server, and psycopg2-binary is the PostgreSQL driver for Python. Next, create a new Django project in the current folder with django-admin startproject myproject . (note the dot at the end to avoid creating nested directories). Before proceeding, test that the app runs with the development server: python manage.py runserver 0.0.0.0:8000, then open port 8000 with sudo ufw allow 8000 to test via browser using your Droplet's IP. This step is for debugging only—never leave port 8000 open in production because the development server is not designed for real traffic or handling multiple concurrent requests. Once tested, close the port with sudo ufw delete allow 8000. Next, test with Gunicorn instead of the development server: gunicorn --bind 0.0.0.0:8000 --workers 3 myproject.wsgi. The recommended number of workers is typically (2 × number of CPU cores) + 1; for a 1 vCPU Droplet, 3 workers is adequate. Before running live, edit myproject/settings.py to set ALLOWED_HOSTS = ['your_domain.com', 'your_droplet_ip']—otherwise Django will immediately return a 400 Bad Request when accessed via your domain or IP because the default only allows localhost. Once Gunicorn runs successfully without errors, you're ready to configure it as a permanent background service using systemd in the next step.
- pip install django gunicorn psycopg2-binary installs all three in one command
- django-admin startproject myproject . (note the dot to prevent nested folders)
- Test with gunicorn --bind first; set workers = (2×CPU)+1
Connecting to PostgreSQL
PostgreSQL is recommended for Django production over SQLite because it handles concurrent writes from multiple processes much better. Install it with sudo apt install postgresql postgresql-contrib libpq-dev -y. Enter the psql shell as the postgres user with sudo -u postgres psql, then create a separate database and user specifically for this application—never use the postgres user directly from your app. Run these SQL commands in sequence: CREATE DATABASE myproject_db; CREATE USER myproject_user WITH PASSWORD 'your_strong_password'; ALTER ROLE myproject_user SET client_encoding TO 'utf8'; GRANT ALL PRIVILEGES ON DATABASE myproject_db TO myproject_user;, then exit with \q. Next, modify settings.py to change the DATABASES section from the default sqlite3 to PostgreSQL: DATABASES = {'default': {'ENGINE': 'django.db.backends.postgresql', 'NAME': 'myproject_db', 'USER': 'myproject_user', 'PASSWORD': 'your_strong_password', 'HOST': 'localhost', 'PORT': '5432'}}. In practice, you should pull the password from an environment variable instead of writing it directly in the file that gets committed to git. Once configured, run the first migration with python manage.py migrate to create Django's system tables in the new database, and python manage.py createsuperuser to create an admin account for your team. If you prefer not to manage PostgreSQL yourself, DigitalOcean offers a Managed PostgreSQL Database starting at $15.15/month for a plan with 1 vCPU/1 GiB RAM and 10-30 GB storage, including automated backups and separation from your web Droplet. This approach reduces operational overhead and eliminates the risk of needing to resize your web server without impacting the database (pricing as of July 2026—verify current prices on the provider's website).
- Install PostgreSQL: apt install postgresql postgresql-contrib libpq-dev
- Create separate DB+user for this app; never use the postgres user directly from the application
- Change ENGINE to django.db.backends.postgresql in settings.py, then run migrate
- Alternative: Managed PostgreSQL starting at $15.15/month if you prefer not to manage it yourself
Configuring Nginx Reverse Proxy
With Gunicorn running successfully, the next step is to run it as a permanent background service via systemd instead of keeping a command running in the terminal. Create a socket file at /etc/systemd/system/gunicorn.socket and a service file at /etc/systemd/system/gunicorn.service. The service file should contain approximately: [Service] User=deployer Group=www-data WorkingDirectory=/home/deployer/myproject ExecStart=/home/deployer/myproject/venv/bin/gunicorn --workers 3 --bind unix:/run/gunicorn.sock myproject.wsgi:application. Enable it with sudo systemctl start gunicorn.socket && sudo systemctl enable gunicorn.socket. Using a Unix socket instead of a TCP port is more secure because it doesn't expose a port to the internet. Install Nginx with sudo apt install nginx -y, then create a new config file at /etc/nginx/sites-available/myproject. The main content is a server block that proxies requests to Gunicorn's socket: server { listen 80; server_name your_domain.com; location = /favicon.ico { access_log off; log_not_found off; } location /static/ { root /home/deployer/myproject; } location / { include proxy_params; proxy_pass http://unix:/run/gunicorn.sock; } }. Enable this config by creating a symlink: sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled/. Always check the syntax before restarting with sudo nginx -t. If no errors appear, restart with sudo systemctl restart nginx. Finally, open the firewall for web traffic with sudo ufw allow 'Nginx Full', which opens both port 80 and 443 at once. If you still have port 8000 open from earlier testing, close it. A common mistake is forgetting to check the permissions on your project folder—Nginx runs as user www-data and must have execute permission to access the deployer's home directory. If you see 502 Bad Gateway, verify that gunicorn.socket is running with sudo systemctl status gunicorn.socket and check logs with journalctl -u gunicorn.
- Configure gunicorn as a systemd service bound to a Unix socket, not a direct TCP port
- Nginx server block proxy_pass to unix:/run/gunicorn.sock
- Always test config with nginx -t before restarting
Static Files and SSL
Based on real-world use, django does not serve static files (CSS/JS/images) in production for performance reasons—Nginx must do it instead. First, define the static files folder in settings.py with STATIC_URL = 'static/' and STATIC_ROOT = BASE_DIR / 'staticfiles', then collect all static files from every app into one folder with python manage.py collectstatic. This command must be re-run every time you modify static files or deploy new code, otherwise the page will load but without CSS styling. Verify that the Nginx config's /static/ location block points to the correct staticfiles folder path. Next, enable free SSL with Let's Encrypt via Certbot. Install it with sudo apt install certbot python3-certbot-nginx -y, then issue and configure an SSL certificate in one command: sudo certbot --nginx -d your_domain.com -d www.your_domain.com. Certbot automatically edits your Nginx config to redirect HTTP to HTTPS and sets up auto-renewal via a systemd timer that runs in the background without manual setup. Verify the timer is active with sudo systemctl status certbot.timer. The system will automatically renew your certificate every 90 days before expiration. After SSL is live, update settings.py for true production use: set DEBUG = False always, add SECURE_SSL_REDIRECT = True to force HTTPS, and add CSRF_TRUSTED_ORIGINS = ['https://your_domain.com'] for newer Django versions that enforce strict origin checking. Leaving DEBUG=True in production is one of the most common security mistakes—it exposes stack traces, server paths, and all settings to anyone who triggers an error.
- STATIC_ROOT + collectstatic must be re-run every time you edit static files or deploy new code
- certbot --nginx -d domain issues free SSL and auto-configures HTTP→HTTPS redirect
- certbot.timer auto-renews certificates every 90 days—no manual cron setup needed
- Production must have DEBUG=False and SECURE_SSL_REDIRECT=True always
When to Use This Approach (Real-World Use Cases)
The self-managed Droplet deployment approach described here is ideal for teams that want full environment control, need to install specialized system libraries (like GDAL for GeoDjango or ffmpeg for video processing), or have limited budgets and need predictable costs. It differs from App Platform (a managed PaaS) which automates infrastructure but costs more per spec and offers less customization. For new projects or MVPs with low traffic, a $6/month Basic Droplet (1 GiB RAM) easily runs Django, Gunicorn, Nginx, and PostgreSQL together. As your app gains real users and traffic grows, watch for warning signs: when RAM consistently exceeds 80%, or response times slow down during peak usage, it's time to upgrade to $12/month (2 GiB) or $24/month (4 GiB, 2 vCPU). If traffic outgrows a single machine, the next step is horizontal scaling using a Load Balancer (starting at $12/month) in front of multiple web Droplets. For the database, if you started with self-managed PostgreSQL per this guide, migrate to Managed PostgreSQL (starting at $15.15/month) when your team lacks time to handle patching and backups, or when the database becomes your system's bottleneck and you want to scale it independently from your web server, which avoids having to resize your application machine unnecessarily. Typical use cases for this approach include in-house development teams, agencies deploying multiple separate customer projects, or small SaaS startups that don't yet need Kubernetes complexity. Interested readers can sign up and receive starter credit at Get $200 Free Credit → (all pricing in this article is as of July 2026—verify current prices on the provider's website before deciding).
- Ideal for teams needing full environment control and specialized system libraries
- RAM consistently over 80% = time to resize to next plan ($12 or $24/month)
- Traffic exceeds single-machine capacity = add Load Balancer starting at $12/month
- Team lacks DB maintenance time or database becomes bottleneck = switch to Managed PostgreSQL from $15.15/month
Common Mistakes and Solutions
This is important — the number-one mistake is leaving ALLOWED_HOSTS empty or incomplete, resulting in 400 Bad Request when accessing via your real domain or IP even though it works fine on the development server. Fix this by ensuring your domain and Droplet IP are both added to the list in settings.py. The second common mistake is forgetting to set DEBUG = False before going live—if you leave DEBUG = True in production, it's a major security risk because error pages will display full stack traces to the public, leaking server paths, settings, and sensitive information. Use an environment variable to keep DEBUG values separate between dev and production instead of editing the file each time. Another frequent issue is 502 Bad Gateway, usually caused by Gunicorn not running or Nginx being unable to access the Unix socket due to file permissions. Check this with sudo systemctl status gunicorn.socket and examine recent logs with journalctl -u gunicorn -n 50. Most often the culprit is an incorrect path in the service file's ExecStart, or the virtualenv not being properly activated when writing the gunicorn binary path—always verify the path matches exactly. Another common pitfall is psycopg2-binary installation failing because system packages libpq-dev and build tools are missing. Fix this by installing build-essential libpq-dev before running pip install. Static files problems are also frequent: the page loads but has no CSS styling, usually because collectstatic was forgotten after a new deployment or the Nginx /static/ location path doesn't match your actual STATIC_ROOT. Don't forget to close port 8000 left open for testing at the beginning. Finally, a common mistake is failing to run python manage.py migrate after pulling code with model changes—you'll get errors about missing columns even though the code looks correct. Make migration part of your deploy script so it runs automatically, not as a manual step you have to remember.
- ALLOWED_HOSTS empty/incomplete = 400 Bad Request on real domain/IP
- DEBUG=True left in production = security risk, exposes stack traces and settings
- 502 Bad Gateway mostly from wrong path in gunicorn.service or incorrect socket permissions
- Forgot collectstatic / migrate after new deploy = missing CSS or column errors
Best Practices
Store all secrets—SECRET_KEY, database password, API keys—in environment variables via a .env file using libraries like python-dotenv or django-environ, never hardcoded in settings.py. Add .env to .gitignore from day one to prevent it from leaking into your git repository. Pin all library versions with pip freeze > requirements.txt to ensure your production environment matches what you tested locally—this prevents unexpected breakage when newer dependency versions are released. Configure the systemd gunicorn.service to restart automatically on crash and start on reboot by using Restart=always in the service file and running sudo systemctl enable gunicorn. Use firewall rules strictly to open only necessary ports with ufw, and consider installing fail2ban to block brute-force SSH attempts from outside. For each code deployment, follow a consistent process: pull code, install any new dependencies via pip install -r requirements.txt, run migrate if models changed, run collectstatic if static files changed, then restart gunicorn last—this minimizes downtime and prevents accidentally skipping a step. Before making significant changes like upgrading Django versions or altering database schema, create a Droplet Snapshot beforehand at just $0.06/GiB per month—much cheaper than rebuilding the system from scratch if something breaks during the update. Enable DigitalOcean Monitoring (free with one complimentary Uptime Check per account) to get instant alerts when RAM or disk usage spikes unexpectedly. Finally, separate your settings.py into distinct dev and production files, or use environment variables to control behavior differently across environments, preventing debug settings or test values from accidentally being used in production.
- Store secrets in .env + python-dotenv/django-environ; never commit to git
- pip freeze > requirements.txt pins versions to match dev/production exactly
- systemd Restart=always lets gunicorn restart on crash/reboot automatically
- Create Droplet Snapshot ($0.06/GiB/month) before major changes as a safeguard