Django on Debian 13 (Linode)
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 Server IP Address>: Your Linode's Public IP Addressnon_root: Your non-root usernameyour-name: Your name for git commitsyour-email-id: Your email ID for git commitsexample.com: Your domain name
You should also update the IP addresses and VPC CIDR blocks, to match your VPC settings.
(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 Linode (Debian) for Django
Launch a Linode
| Parameter | Value |
|---|---|
| Region | in-maa (Chennai) |
| OS | Debian (Debian 13 as of 22-Feb-2026) |
| Plan | Nanode 1 GB (Shared CPU) |
| Label | Give your preferred label (Label can't have spaces) |
| Root Password | Create a Strong Password and store it somewhere safe |
| SSH Keys | You can add an existing SSH key or add this later when you deploy a new server |
| Disk Encryption | Enable |
| VPC | Create and assign a VPC |
| Subnet | Select a public subnet since Django Server should be accessed via internet |
| Auto-assign a VPC IPv4 | Enable |
| Allow public IPv4 access | Enable |
| Network Interface Type | Linode Interfaces |
| VPC Interface Firewall | Create and assign a Firewall (that allows private connections within VPC and allows HTTP traffic from internet) |
| Backups | Disable (The backups are useful only for databases) |
Upgrade Packages
TIP
Use the LISH Console to connect to the Linode server. If you added an SSH key above, you can log in from your local machine directly.
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 Linode 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.
Disable Root Login
NOTE
The LISH Console doesn't rely on SSH, so you can still access the internals of your system using it, including root login.
First, create a limited user account:
adduser non_root
# You'll be prompted to provide passwordAdd the new user to the sudo group for administrative privileges:
adduser non_root sudoExit the session and SSH back into the server as your new user:
exit
ssh non_root@<DJANGO Server 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_keysDisable Root login and Password Authentication:
sudo vi /etc/ssh/sshd_config
# Set `PasswordAuthentication` to `no`
# Set `PermitRootLogin` to `no`Finally, restart the SSH service to apply the changes:
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 Server 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 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 Linode 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.
