Skip to content

Django on Debian 12 (AWS EC2)

Amazon Linux is avoided because the installation process for mod_wsgi and certbot is 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 file
  • non_root: Your non-root username
  • your-name: Your name for git commits
  • your-email-id: Your email ID for git commits
  • example.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:

ParameterValue
Regionap-south-2 (Hyderabad)
NameGive your preferred name
OSDebian (Debian 12 as of 5-Jun-2025)
Architecture64-bit (Arm)
EC2 Typem6g.medium
EC2 KeyPairSelect your Key Pair (.pem)
VPCCreate and select a VPC
SubnetSelect a public subnet since Django/Apache server needs to be accessed via Internet
Auto-assign Public IPEnable
Auto-assign IPv6 IPEnable
Security GroupSelect Django Security Group that allows SSH from Admin System IP and HTTP/HTTPS from Internet
EBS Size8GB gp3 (Encrypted - using KMS key) (Min size and can't be downgraded after setup)
AdvancedEnable 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.

shell
chmod 400 /path/to/your-key-pair.pem

SSH into your new EC2 instance using the default Debian user, which is admin.

shell
# 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:

shell
sudo apt update && sudo apt upgrade -y

Set Timezone

Install all locales first to disable locale warnings:

shell
sudo apt install locales-all

All new EC2 servers are set to UTC time by default. To change it to IST, use:

shell
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:

shell
sudo adduser non_root
# You'll be prompted to provide password

Add the new user to the sudo group for administrative privileges:

shell
sudo adduser non_root sudo

To 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.

shell
sudo rsync --archive --chown=non_root:non_root ~/.ssh /home/non_root

Exit the session and SSH back into the server as your new user:

shell
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:

shell
mkdir ~/.ssh && vi ~/.ssh/authorized_keys

AWS already disables root login and password authentication by default, but you can confirm this by checking the SSH configuration file:

shell
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:

shell
sudo systemctl restart sshd

Setup 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:

shell
curl -LsSf https://astral.sh/uv/install.sh | sh

Clone the GitHub project

To clone the GitHub project, you first need to create a new SSH Key pair:

shell
ssh-keygen -t rsa -b 4096

Grab the generated public key and add it to your GitHub account settings:

shell
cat ~/.ssh/id_rsa.pub

Install git to manage the GitHub repositories locally:

shell
sudo apt -y install git

Confirm git installation:

shell
git --version

Configure your git credentials locally:

shell
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:

shell
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:

shell
python -c "import secrets; print(secrets.token_urlsafe(64))"

Activate the environment and install dependencies:

shell
uv sync

Create the required log file and assign the appropriate permissions:

shell
sudo mkdir /logs && cd /logs && sudo touch debug.log && sudo chmod 777 /logs && sudo chmod 666 /logs/debug.log

Return to your project directory and collect the static files:

shell
cd ~/my-project
uv run manage.py collectstatic

Create the Django Cache table:

shell
uv run manage.py createcachetable

Check the project for any configuration errors before deploying:

shell
uv run manage.py check
uv run manage.py check --deploy

Run migrations to get the database up to date:

shell
uv run manage.py makemigrations # Shouldn't create any new migrations
uv run manage.py migrate

Create a new Super User for Admin UI access:

shell
uv run manage.py createsuperuser

Install Apache & mod_wsgi

Install apache2 and mod_wsgi on Debian, using the following command:

shell
sudo apt -y install apache2 libapache2-mod-wsgi-py3

Start the Apache web server:

shell
sudo systemctl restart apache2

You 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:

shell
# 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-project

Set Global ServerName

To set the global ServerName for the Apache web server, open the primary Apache config file:

shell
sudo vi /etc/apache2/apache2.conf

Navigate to the bottom of the file and add the following line:

txt
ServerName api.example.com

Configure 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:

shell
sudo vi /etc/apache2/sites-available/000-default.conf

Overwrite its contents with the following configuration:

apache
<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:

shell
sudo a2enmod rewrite

Restart the Apache server to check for any errors:

shell
sudo systemctl restart apache2

Remove certbot-auto and any older Certbot OS packages:

shell
sudo apt remove certbot

Install Certbot using snapd and prepare the command alias:

shell
sudo apt -y install snapd && sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

At 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.

txt
api.example.com
www.api.example.com

Get and install the SSL certificate. When prompted for the ServerName, select the default or original configuration file:

shell
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:

shell
sudo vi /etc/apache2/sites-enabled/000-default-le-ssl.conf

Most 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:

apache
<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:

shell
sudo systemctl restart apache2

Now 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.