Mr. Editor-in-chief Mr. Editor-in-chief January 1, 2021 Updated April 24, 2026

Deploy Django to a Linux (Ubuntu) Server

Deploy Django with Apache and mod_wsgi on a Ubuntu virtual machine on Microsoft Azure.
Create an Azure Virtual Machine
Go to Microsoft Azure -> Azure services -> Create a resource -> Virtual machine -> Create
Resource Group: markdown-blog-server-group
Virtual machine name: markdown-blog-server
Username: user0
Continue with default settings till you get
Click Download private key and create resourse

Set up SSH

mv /mnt/c/Users/[username]/downloads/happyhacker.pem ~/.ssh/happyhacker.pem
chmod 400 ~/.ssh/happyhacker.pem
ssh -i ~/.ssh/happyhacker.pem user0@20.255.61.200
exit
cat ~/.ssh/id_rsa.pub
# Copy the content
ssh -i ~/.ssh/happyhacker.pem user0@20.255.61.200
nano ~/.ssh/authorized_keys
# Append it to the authorized_keys file
sudo chmod 700 ~/.ssh/
sudo chmod 600 ~/.ssh/*
exit
ssh user0@20.255.61.200

Upgrade the system

sudo apt-get update && sudo apt-get upgrade

Get hostname, set hostname(Optional) and add it into hosts file

$ hostname
markdown-blog-server
$ sudo hostnamectl set-hostname [hostname] # Set a new hostname(Optional)
$ sudo nano /etc/hosts

Add 'public.IP.adress hostname' under '127.0.0.1 localhost' into hosts file

Firewall installation and setup, 'ufw' stands for Uncomplicated Firewall

sudo apt install ufw 
sudo ufw default allow outgoing
sudo ufw default deny incoming
sudo ufw allow ssh
sudo ufw allow 8000
sudo ufw enable
sudo ufw status

For Azure VM, we also need to go to Networking -> Inbound port rules and add inbound port rules -> Destination port ranges -> 8000.

Copy source code into VM home Directory

scp -r markdown_blog user0@20.2.100.200:~/

Or pull code from GitHub or your own Git server

ssh-keygen -t ecdsa -b 521
cd ~/.ssh
ls
cat id_ecdsa.pub

copy the content in id_ecdsa.pub and go to www.github.com -> GitHub profile picture (on the top right) -> settings -> SSH and GPG keys -> SSH keys / Add new -> paste it into 'Key'

cd ~
git clone git@github.com:username/repo_name.git

Create a virtual environment, activate it and install Python libraries

cd project-folder
sudo apt install python3-pip python3-venv python3-dev default-libmysqlclient-dev
python3 -m venv venv
source venv/bin/activate
pip list
pip install -r requirements.txt

Copy production .env, db.sqlite3 and media folder into project folder

scp .env db.sqlite3 user0@20.255.61.200:~/markdown_blog/
scp -r media user0@20.255.61.200:~/markdown_blog/

# Copy files from remote server into local computer Example
scp user0@20.255.61.200:~/markdown_blog/db.sqlite3 db.sqlit
e3

Run dev server and test if the website can be open outside VM

python manage.py makemigrations
python manage.py migrate
python manage.py collectstatic
python manage.py runserver 0.0.0.0:8000

Install Apache and mod-wsgi

sudo apt-get install apache2
sudo apt-get install libapache2-mod-wsgi-py3

Configure Apache web server

cd /etc/apache2/sites-available
ls
sudo cp 000-default.conf markdown_blog.conf
sudo nano markdown_blog.conf
        ...
        #Include conf-available/serve-cgi-bin.conf
        alias /static /home/user0/markdown_blog/static
        <Directory /home/user0/markdown_blog/static>
                Require all granted
        </Directory>

        alias /media /home/user0/markdown_blog/media
        <Directory /home/user0/markdown_blog/media>
                Require all granted
        </Directory>

        <Directory /home/user0/markdown_blog/blog>
                <Files wsgi.py>
                        Require all granted
                </Files>
        </Directory>

        WSGIScriptAlias / /home/user0/markdown_blog/blog/wsgi.py
        WSGIDaemonProcess markdown_blog python-path=/home/user0/markdown_blog python-home=/home/user0/markdown_blog/venv
        WSGIProcessGroup markdown_blog
</VirtualHost>
...

Apache2-enable the site

sudo a2ensite markdown_blog
sudo a2dissite 000-default.conf # Disable default configure file
sudo systemctl reload apache2
systemctl status apache2.service

An AH00558 error will not prevent Apache from running correctly. The message is mainly for informational purposes

Set up file permissions

cd ~
sudo chown :www-data markdown_blog/db.sqlite3;sudo chmod 664 markdown_blog/db.sqlite3
sudo chown :www-data markdown_blog/;sudo chmod 775 markdown_blog/
sudo chown -R :www-data markdown_blog/media/;sudo chmod -R 775 markdown_blog/media/

www-data is the user that web servers on Ubuntu (Apache, nginx, for example) use by default for normal operation. The web server process can access any file that www-data can access. It has no other importance.

Allow HTTP traffic
For Azure VM, we need to go to Networking -> Inbound port rules and add inbound port rules -> Destination port ranges -> 80.

sudo ufw allow http/tcp
sudo service apache2 restart

You should be able to open the site with IP address now
Please refer to How To Troubleshoot Common Apache Errors if it doesn't work.

Add custom domain name

Eable HTTPS with a free SSL/TLS certificate using Let's Encrypt

sudo nano /etc/apache2/sites-available/markdown_blog.conf
ServerName peng.works
ServerAlias www.peng.works
#WSGIScriptAlias ...
#WSGIDaemonProcess ...       
#WSGIProcessGroup ...

sudo snap install core; sudo snap refresh core
sudo apt-get remove certbot
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
sudo certbot --apache -d peng.works -d www.peng.works # Create certificates for both domains

sudo cat /etc/apache2/sites-available/markdown_blog.conf # Checking a few new lines at the bottom
sudo cat /etc/apache2/sites-available/markdown_blog-le-ssl.conf # Checking a few new lines at the bottom
sudo nano /etc/apache2/sites-available/markdown_blog.conf
# Remove below lines (Ctrl+K == delete a line in nano)
        alias /static /home/user0/markdown_blog/static
        <Directory /home/user0/markdown_blog/static>
                Require all granted
        </Directory>

        alias /media /home/user0/markdown_blog/media
        <Directory /home/user0/markdown_blog/media>
                Require all granted
        </Directory>

        <Directory /home/user0/markdown_blog/blog>
                <Files wsgi.py>
                        Require all granted
                </Files>
        </Directory>

        #WSGIScriptAlias ...
        #WSGIDaemonProcess ...       
        #WSGIProcessGroup ...

sudo nano /etc/apache2/sites-available/markdown_blog-le-ssl.conf
# Uncomment below lines
        WSGIScriptAlias ...
        WSGIDaemonProcess ...       
        WSGIProcessGroup ...

sudo apachectl configtest
sudo ufw allow https
sudo service apache2 restart

sudo certbot renew --dry-run
sudo crontab -e
1
# m h  dom mon dow   command
# It will run on 4:30am the first day of any month, any day of the week
30 4 1 * * sudo certbot renew --quiet

For Azure VM, we need to go to Networking -> Inbound port rules and add inbound port rules -> Destination port ranges -> 443.
The commands above are subject to change. Please refer to Let's Encrypt for the latest update

Django deployment checklist

python manage.py check --deploy

How to generate a Django secret key

python3 manage.py shell
from django.core.management.utils import get_random_secret_key
get_random_secret_key()

Set 'Debug' to 'False' and disallow port 8000

sudo ufw delete allow 8000
sudo service apache2 restart