Django on Debian 12 (AWS EC2)
Amazon Linuxis avoided because the installation process formod_wsgiandcertbotis unnecessarily complex.
This setup uses Apache and mod_wsgi to deploy a Django Project. It uses PostgreSQL to handle both the data models and caching and assumes that the local environment uses a similar setup.
NOTE
This setup uses a Public Subnet, so the Django server can be accessed over the internet.
NOTE
You can follow the steps in this guide as written, but replace the following placeholders with your own names:
<DJANGO EC2 Elastic IP Address>: Your EC2 Instance's Elastic IP Address/path/to/your-key-pair.pem: Path to your EC2 private key filenon_root: Your non-root usernameyour-name: Your name for git commitsyour-email-id: Your email ID for git commitsexample.com: Your domain name
(Optional) Create and set up new Django Project
If you don't already have a Django project created, you can use this guide to create and configure one.
Setup AWS EC2 (Debian) for Django
Launch an EC2 Instance
Navigate to the EC2 Dashboard in your AWS Console and launch a new instance with the following parameters:
| Parameter | Value |
|---|---|
| Region | ap-south-2 (Hyderabad) |
| Name | Give your preferred name |
| OS | Debian (Debian 12 as of 5-Jun-2025) |
| Architecture | 64-bit (Arm) |
| EC2 Type | m6g.medium |
| EC2 KeyPair | Select your Key Pair (.pem) |
| VPC | Create and select a VPC |
| Subnet | Select a public subnet since Django/Apache server needs to be accessed via Internet |
| Auto-assign Public IP | Enable |
| Auto-assign IPv6 IP | Enable |
| Security Group | Select Django Security Group that allows SSH from Admin System IP and HTTP/HTTPS from Internet |
| EBS Size | 8GB gp3 (Encrypted - using KMS key) (Min size and can't be downgraded after setup) |
| Advanced | Enable Termination Protection |
[!TIP] AWS IP addresses can change when instances are stopped and started. It is highly recommended to allocate an Elastic IP address in the EC2 console and associate it with this instance so your server IP remains static.
Connect and Upgrade Packages
Open your terminal, locate the .pem key file you downloaded during setup, and set the correct permissions for it.
chmod 400 /path/to/your-key-pair.pemSSH into your new EC2 instance using the default Debian user, which is admin.
# Replace <DJANGO EC2 Elastic IP Address> below
# Notice the user for Debian-based instances is `admin` and not `ec2-user`
ssh -i /path/to/your-key-pair.pem admin@<DJANGO EC2 Elastic IP Address>Upgrade the packages on the server:
sudo apt update && sudo apt upgrade -ySet Timezone
Install all locales first to disable locale warnings:
sudo apt install locales-allAll new EC2 servers are set to UTC time by default. To change it to IST, use:
timedatectl set-timezone 'Asia/Kolkata'Confirm the date by running the date command in the terminal.
Set Up Non-Root User
AWS Debian AMIs disable root password login by default. However, it is still best practice to create your own dedicated limited user account.
First, create a limited user account:
sudo adduser non_root
# You'll be prompted to provide passwordAdd the new user to the sudo group for administrative privileges:
sudo adduser non_root sudoTo allow the new user to log in using the same SSH key you used for the admin user, copy the authorized keys directory and update the ownership.
sudo rsync --archive --chown=non_root:non_root ~/.ssh /home/non_rootExit the session and SSH back into the server as your new user:
exit
ssh -i /path/to/your-key-pair.pem non_root@<Django EC2 Elastic IP Address>Create an SSH directory and add the public key of your local Mac machine to the authorized keys file:
mkdir ~/.ssh && vi ~/.ssh/authorized_keysAWS already disables root login and password authentication by default, but you can confirm this by checking the SSH configuration file:
sudo vi /etc/ssh/sshd_config
# Confirm that `PasswordAuthentication` is set to `no`
# Confirm that `PermitRootLogin` is set to `no`Finally, restart the SSH service to apply the changes, if required:
sudo systemctl restart sshdSetup Database Server
At this point, if you don't have a Postgres Database server ready for production use, you'll need to set one up. You can follow this guide to configure it.
Setup Django Project
uv
First, install the uv package manager:
curl -LsSf https://astral.sh/uv/install.sh | shClone the GitHub project
To clone the GitHub project, you first need to create a new SSH Key pair:
ssh-keygen -t rsa -b 4096Grab the generated public key and add it to your GitHub account settings:
cat ~/.ssh/id_rsa.pubInstall git to manage the GitHub repositories locally:
sudo apt -y install gitConfirm git installation:
git --versionConfigure your git credentials locally:
git config --global user.name "your-name"
git config --global user.email "your-email-id"Clone the GitHub repository in your home directory on the Linux Server:
cd ~ && git clone <remote-URL>Setup Project on the Server
Navigate into your cloned project directory (e.g., cd ~/my-project). Create an .env file with variables that match your local setup but with updated production values. Ensure the DJ_ENV variable is set to PROD.
Generate a new secure SECRET_KEY using the following command and add it to your .env file:
python -c "import secrets; print(secrets.token_urlsafe(64))"Activate the environment and install dependencies:
uv syncCreate the required log file and assign the appropriate permissions:
sudo mkdir /logs && cd /logs && sudo touch debug.log && sudo chmod 777 /logs && sudo chmod 666 /logs/debug.logReturn to your project directory and collect the static files:
cd ~/my-project
uv run manage.py collectstaticCreate the Django Cache table:
uv run manage.py createcachetableCheck the project for any configuration errors before deploying:
uv run manage.py check
uv run manage.py check --deployRun migrations to get the database up to date:
uv run manage.py makemigrations # Shouldn't create any new migrations
uv run manage.py migrateCreate a new Super User for Admin UI access:
uv run manage.py createsuperuserInstall Apache & mod_wsgi
Install apache2 and mod_wsgi on Debian, using the following command:
sudo apt -y install apache2 libapache2-mod-wsgi-py3Start the Apache web server:
sudo systemctl restart apache2You should now see the default website at http://<DJANGO EC2 Elastic IP Address>.
Fix Systemd Logging Issue
By default, systemd creates systemd-private- folders in the tmp directory to protect apache2. To avoid this behavior, create an override file as explained in this guide.
Grant Access to Home Directory
The Apache Server runs as the www-data user and it requires access to your home directory to serve the Django project contents. Set the appropriate ownership and permissions using these commands:
# Set the group of the entire project to www-data
sudo chown -R non_root:www-data /home/non_root/my-project
# Ensure you have full access and the group has read/execute access
chmod -R 750 /home/non_root/my-projectSet Global ServerName
To set the global ServerName for the Apache web server, open the primary Apache config file:
sudo vi /etc/apache2/apache2.confNavigate to the bottom of the file and add the following line:
ServerName api.example.comConfigure Apache and HTTPS
WARNING
The certbot certificate installation will fail due to WSGI lines in the Virtual Host configuration file, so ensure they are commented out or excluded during the initial setup.
Open the default Virtual Host configuration file:
sudo vi /etc/apache2/sites-available/000-default.confOverwrite its contents with the following configuration:
<VirtualHost *:80>
DocumentRoot "/home/non_root/my-project/"
ServerName api.example.com
ServerAlias www.api.example.com
ServerAdmin info@example.com
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
# Allow access to static files
<Directory /home/non_root/my-project/staticfiles>
Require all granted
</Directory>
Alias /static /home/non_root/my-project/staticfiles
# Granting access to wsgi.py
<Directory "/home/non_root/my-project">
Require all granted
</Directory>
<Directory "/home/non_root/my-project/my_project">
<Files wsgi.py>
Require all granted
</Files>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
# To run WSGI daemon process (commented to avoid certbot errors)
# WSGIDaemonProcess api.example.com user=non_root group=www-data python-home=/home/non_root/my-project/.venv python-path=/home/non_root/my-project
# WSGIProcessGroup api.example.com
# WSGIApplicationGroup %{GLOBAL}
# WSGIScriptAlias / /home/non_root/my-project/my_project/wsgi.py
# WSGIPassAuthorization On
</VirtualHost>Since the SSL configuration will use RewriteEngine to redirect www to non-www, enable the rewrite module:
sudo a2enmod rewriteRestart the Apache server to check for any errors:
sudo systemctl restart apache2Remove certbot-auto and any older Certbot OS packages:
sudo apt remove certbotInstall Certbot using snapd and prepare the command alias:
sudo apt -y install snapd && sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbotAt this point, you must point your domain to the current Elastic IP address using Route53 or a similar DNS service. Edit the A records for both domains.
api.example.com
www.api.example.comGet and install the SSL certificate. When prompted for the ServerName, select the default or original configuration file:
sudo certbot --apache -v
# Details to be filled as follows:
# * Email: `info@example.com`
# * Terms of Service: `Y`
# * Share Email Address: `N`
# * Domain Names: `api.example.com,www.api.example.com`
# * Unable to find ServerName: `Select default/original conf file`Now, update the newly generated SSL configuration file:
sudo vi /etc/apache2/sites-enabled/000-default-le-ssl.confMost of the content will be similar to what Certbot generated, but you must manually update the WSGI config, Rewrite config, valid host grants, and root directory access as shown below:
<IfModule mod_ssl.c>
<VirtualHost *:443>
DocumentRoot "/home/non_root/my-project/"
ServerName api.example.com
ServerAlias www.api.example.com
ServerAdmin info@example.com
SSLEngine on
Include /etc/letsencrypt/options-ssl-apache.conf
SSLCertificateFile /etc/letsencrypt/live/api.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/api.example.com/privkey.pem
# BEGIN: Enable www to non-www redirection
RewriteEngine On
RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC]
RewriteCond %{HTTP_HOST} !^localhost
RewriteCond %{HTTP_HOST} !^[0-9]+.[0-9]+.[0-9]+.[0-9]+(:[0-9]+)?$
RewriteCond %{REQUEST_URI} !^/\.well-known
RewriteRule ^(.*)$ https://%1$1 [R=permanent,L]
# END: Enable www to non-www redirection
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
SetEnvIfNoCase Host example\.com VALID_HOST
# Allow access to static files
<Directory /home/non_root/my-project/staticfiles>
Require env VALID_HOST
</Directory>
Alias /static /home/non_root/my-project/staticfiles
# Granting access to wsgi.py
<Directory "/home/non_root/my-project">
Require env VALID_HOST
</Directory>
<Directory "/home/non_root/my-project/my_project">
<Files wsgi.py>
Require env VALID_HOST
</Files>
Options Indexes FollowSymLinks
AllowOverride All
Require env VALID_HOST
</Directory>
LogLevel info
# To run WSGI daemon process
WSGIDaemonProcess api.example.com python-home=/home/non_root/my-project/.venv python-path=/home/non_root/my-project
WSGIProcessGroup api.example.com
WSGIApplicationGroup %{GLOBAL}
WSGIScriptAlias / /home/non_root/my-project/my_project/wsgi.py
WSGIPassAuthorization On
# To avoid invalid host addresses reach Django (such as EC2 IP address)
<Directory "/">
SetEnvIfNoCase Host example\.com VALID_HOST
Require env VALID_HOST
Options
</Directory>
</VirtualHost>
</IfModule>Restart the Apache server one final time:
sudo systemctl restart apache2Now test the secure endpoint at https://api.example.com/. The www subdomain should correctly redirect to non-www, and all http traffic should force an upgrade to https.
