Notes on Antonis Christofides's Book, Deploying Django on a Single Debian or Ubuntu Server
Acknowledgement and Starting Point
This post is about steps I took to deploy a Django project to a VPS running Debian 12. I used Putty on a Windows 11 laptop to connect to the VPS. My Django project is on the laptop for development.
This post is mostly my notes on Deploying Django on a single Debian or Ubuntu Server by Antonis Christofides. That book has been a guide to me and I keep refering to it. But here I've added notes about steps that you may do differently, in some cases must do differently because the book is a few years old. I recommend following Mr. Christofides's book and looking to this post for additional information that might be helpful. That's especially true if you're setting up your VPS from scratch, because Chapters 1 and 2 have a lot of information about finding a VPS provider that I won't be touching here
The book uses placeholders $DJANGO_PROJECT for the project name and $DJANGO_USER for the user name, noting that they're usually the same but can be different. In this post, I have more placeholders and instead of the $SOMETHING format, my placeholders will look more like benvillesomething.
Some of my placeholders will be:
- benvillelibrarysite for the project name, the project root directory, and other directories named after the project
- benvillelibrary for the project package, sometimes called the settings directory, which usually has the same name as the project name but can be different
- benvilleus for the user account under which the project runs
- benvilleadmin for the user who you are logged in as when you create and edit this project if you're not logging in as root
Plus a couple more. The first three names can all be the same in your setup
Users
Mr. Christofides strongly recommends using ssh keys, but there are some cases where you must or prefer to stick with user names and passwords. I'm not recommending one over the other. If you stick with passwords, you should create a user with sudo privileges and disable ssh for root. For the project I'm working on as I write this post, I'm logging in as a sudo-enabled user. Most of my examples will reflect that. I also added two-factor authentication.
There are several tutorials online for creating a sudo-enabled user and disabling ssh for root on Debian systems, for example How-To Geek's. Here's how I do it:
- Using an ssh client like Putty, log in as root
- Create a new user for yourself
adduser benvilleadmin
- Add that new user to sudoers
usermod -aG sudo benvilleadmin
- Create a new user for yourself
- Stay logged in as root. Open a separate ssh instance and log in as benvilleadmin
- Test benvilleadmin's sudo privileges
sudo ls
- edit /etc/ssh/sshd_config to change PermitRootLogin from 'yes' to 'no'
sudo nano /etc/ssh/sshd_config
In the Nano editor,- Search for PermitRootLogin by pressing Ctrl-w.
- Change PermitRootLogin from 'yes' to 'no'
- Press ctrl-x to exit, then press 'y' to confirm that you want to save
- Hit 'enter' to confirm saving to /etc/ssh/sshd_config
- Restart the ssh daemon.
sudo systemctl restart ssh
- Test benvilleadmin's sudo privileges
- Log out as root, and try logging back in as root to confirm that root login is disabled
Two Factor Authentication
For the project I'm working on, I'm using passwords and 2FA
2FA works with an app that has to be set up on a device such as your phone. I use Bitwarden which also has a Windows desktop app.
To enable 2fa, I installed libpam-google-authenticator
sudo apt install -y libpam-google-authenticator
For those suspicious of Google, Linux Babe tells us that the authentication process doesn't send anything to Google - they just wrote the open-source software
The next step will display a QR code in your terminal.
google-authenticator
I was suprized the first time I did this to see the QR code pop up in a console. Your authenticator app should have an option somewhere that allows you to scan a code to set up TOTP (Time-based One Time Password). If the code is too big for you to scan and you're using Putty, you can make your font smaller by following these steps:
- Ctrl-right-click in the terminal window
- Choose "Change Settings"
- Choose "Appearance" in the menu under "Window"
- Click "Change" next to the font display
- Select a smaller font size and click "Ok"
- Click "Apply"
If you can't scan the code, your app should also have an option to enter the secret key manually
I suggest continuing with Linux Babe's instructions, although there are others you can find.
I also suggest staying logged in with one ssh window to make changes and log in with a separate window to test.
I also recommend learning on a VPS that you can painlessly rebuild in case you lock yourself out. If you can't or don't want to rebuild, your VPS provider probably has a way for you to log in with an admin console from their website
Project System User
Logged in as benvilleadmin, I created benvilleus with the following command:
sudo adduser --system --home=/var/opt/benvillelibrarysite --disabled-password --group --shell=/bin/bash benvilleus
If I were logged in as root with ssh keys, the command would be the same except without sudo.
As the book says, it doesn't matter much what the home directory is so you're free to give it a different name than benvillelibrarysite, or even place it somewhere other than /var/opt/. You can also skip the --home option and leave the home directory as non-existant. In most cases sticking with --home=/var/opt/benvillelibrarysite is the best choice
Mr. Christofides's example includes "no-create-home", because he covered creating the directory separately, but I didn't bother with that so I let the system create the directory for me
Directories
Chapter 3 of Deploying Django on a single Debian or Ubuntu Server provides good information about the directory structure of a Linux machine and how that structure relates to the software people develop and install.
On my laptop, my project structure is something like what's below. I made some changes for clarity. I also included additional apps (touglates, wibekwa, and wibekwa_base) that I added to the project I'm working on. I won't discuss those apps in this post but I included them in the structure so you can see where the apps that you might be working on would be
My development structure is:
benvillelibrarysite/
benvillelibrary/
settings/
base.py
dev.py
static/
templates/
urls.py
wsgi.py
media/
touglates/
venv/
wibekwa/
wibekwa_base/
db.sqlite3
manage.py
requirements.txt
On the production server, the files and directories look something like this
/etc/opt/benvillelibrarysite/ /etc/opt is for settings specific to the app on this server
static/
settings/
local.py
/opt/benvillelibrarysite/ /opt is for applications not part of the operating system
benvillelibrary/
settings/
base.py
production.py
static/
templates/
urls.py
wsgi.py
touglates/
venv/
wibekwa/
wibekwa_base/
manage.py
requirements.txt
/var/opt/benvillelibrarysite/ /var is for data that changes as the app is used
benvillelibrarysite.db
media/
Project Root Directory
The project root directory holds the program files. I created the project root directory /opt/benvillelibrarysite
sudo mkdir /opt/benvillelibrarysite
Then, as the book says, I "clone or otherwise copy" my Django project into the new directory.
For me, "clone or otherwise copy" meant a combination of using sftp to upload from my laptop,
cloning apps from my repositories, and creating or recreating some of files.
I used Filezilla (but other sftp clients, like Win-SCP are fine) to upload requirements.txt, manage.py, and the django package directory
In my case, with root disabled, I can't sftp directly to /opt/benvillelibrarysite/, so I created the directory /home/benvilleadmin/transfer/, and uploaded to that directory.
Then with Putty I copied the files and directories from the transfer directory to the project root directory.
sudo cp /home/benvilleadmin/transfer/* /opt/benvillelibrarysite/
I added some of the apps that I'm using for this project by cloning them from Github
From inside the project root directory:
sudo git clone https://github.com/tougshire/wibekwa
sudo git clone https://github.com/tougshire/wibekwa_base
sudo git clone https://github.com/tougshire/touglates
Virtualenv
The next step is setting up the virtual environment. The following code block from the book combines two commands:
virtualenv --system-site-packages --python=/usr/bin/python3 \
/opt/$DJANGO_PROJECT/venv
/opt/$DJANGO_PROJECT/venv/bin/pip install \
-r /opt/$DJANGO_PROJECT/requirements.txt
The first command is
virtualenv --system-site-packages --python=/usr/bin/python3 \
/opt/$DJANGO_PROJECT/venv
If your system doesn't have virtualenv installed, install it with
sudo apt install virtualenv
For my set up, the first command translates to
sudo virtualenv --system-site-packages --python=/usr/bin/python3 /opt/benvillelibrarysite/venv
The above created the virtual environment /opt/benvillelibrarysite/venv
On my laptop, I had previously created requirements.txt using pip freeze. To do this, I called the following command with the virtual environment activated from the development project root directory:
pip freeze > requirements.txt
During develpment, you almost certainly issued commands from within an activated virtual environment, but you don't have to activate the environment before running these commands on the server
With requirements.txt uploaded to the server, the next command installs the same packages on the server that you had in development
In the book, the command is
/opt/$DJANGO_PROJECT/venv/bin/pip install \
-r /opt/$DJANGO_PROJECT/requirements.txt
For my project, that translates to
sudo /opt/benvillelibrarysite/venv/bin/pip install \-r /opt/benvillelibrarysite/requirements.txt
' Then I compiled the project
sudo /opt/benvillelibrarysite/venv/bin/python -m compileall -x /opt/benvillelibrarysite/venv/ /opt/benvillelibrarysite
The Data Directory and the Log Directory
Following the book, you would create the data directory at this point but I already created /var/opt/benvillelibrarysite when I created benvilleus.
I created the log directory as per section 3.4
sudo mkdir -p /var/log/benvillelibrarysite
sudo chown benvilleus /var/log/benvillelibrarysite
The Production Settings Directory
As noted, on Debian based systems, configuration settings for programs that are installed in /opt/ go in /etc/opt/
sudo mkdir /etc/opt/benvillelibrarysite
I created /etc/opt/benvillelibrarysite/settings.py and edited it as follows:
from benvillelibrary.settings import *
DEBUG = True
ALLOWED_HOSTS = ['benvillelibrary.gov', 'www.benvillelibrary.gov']
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': '/var/opt/benvillelibrarysite/benvillelibrarysite.db',
}
}
Then I set the permissions as below.
sudo chgrp benvilleus /etc/opt/benvillelibrarysite
sudo chmod u=rwx,g=rx,o= /etc/opt/benvillelibrarysite
Then I compiled the files
sudo /opt/benvillelibrarysite/venv/bin/python -m compileall /etc/opt/benvillelibrarysite
With all of this set up, I tested the server, as per Section 3.7 near the end
sudo -u benvilleus PYTHONPATH=/etc/opt/benvillelibrarysite:/opt/benvillelibrarysite \
DJANGO_SETTINGS_MODULE=settings \
/opt/benvillelibrarysite/venv/bin/python \
/opt/benvillelibrarysite/manage.py \
runserver 0.0.0.0:8000
The book notes that you can use --settings instead of DJANGO_SETTINGS_MODULE but I prefer DJANGO_SETTINGS_MODULE because it's clearer to me.
I'm using sudo -u benvilleus to become benvilleus for this command. Sudo without -u defaults to root.
Since I am using sudo, I'm not using su. So instead of
su - c "/opt/benvillelibrarysite ..."
I used
/opt/benvillelibrarysite ...
I changed directory to /opt/benvillelibrarysite before running the commands because I can't run the them from benvilleadmin's home dictory
The web server
I followed Chapter 4, using Apache because I'm more familiar with Apache than with Nginx.
But one big thing I did, which isn't in the book, is I added ssl with Certbot.
Ssl changes your protocol from http to https (https://benvillelibrary.gov). Without ssl, your visiters may get security warnings.
You can follow the first sections and stop at 4.3 for Nginx or 4.6 for Apache, install the certificate as per my instructions below, and then continue where you stopped
To enable ssl, you need a certificate, and one way to obtain a certificate is with Certbot.
For those who have used Certbot before, you may have installed it with
sudo apt install certbot #<-- no longer recommended
but Certbot now "strongly recommends" installing with Snap. Since Snap wasn't preinstalled on Debian 12 minimal, I installed it following the instructions at snapcraft.io/docs/installing-snap-on-debian, which are
sudo apt update
sudo apt install snapd
with Snap installed, I installed Certbot in accordance with their instructions at certbot.eff.org/instructions?ws=apache&os=snap with
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
And ran it with
sudo certbot --apache
(certbot also has a --nginx option)
From there I followed Certbot's instructions, choosing to add a redirect so when visitors request http://benvillelibrary.gov they automatically get https://benvillelibrary.gov
As per the book, I already created a config file named /etc/apache2/sites-available/benvillelibrary.gov.conf. Certbot created a new file named /etc/apache2/sites-available/benvillelibrary.gov-le-ssl.conf, and added a rewrite in the first file to redirect to the second
At this point, the only directives in the first conf file that are important are the server name, server aliases, and the rewrite directives. So any edits the book says to make in bevillelibrary.gov.conf should be made in benvillelibrary.gov-le-ssl.conf instead.
Static and Media Files
I followed Chapter 5 pretty much as described. The collectstatic command looks like this for me
sudo PYTHONPATH=/etc/opt/benvillelibrarysite:/opt/benvillelibrarysite \
DJANGO_SETTINGS_MODULE=settings \
/opt/benvillelibrarysite/venv/bin/python \
/opt/benvillelibrarysite/manage.py collectstatic
Where the book says to go to http://$DOMAIN/static/admin/img/icon_searchbox.png use https://$DOMAIN/static/admin/img/search.svg
(i.e. https://benvillelibrary.gov/static/admin/img/search.svg) instead because
the latest Django uses a different set of images. Also http is changed to https on the assumption that you've enabled ssl
Gunicorn
Chapter 6 says to install Gunicorn with Pip instead of Apt
because the Debian packaged Gunicorn only supports Python 2.
The part about Python 2 is no longer true, but it's still best to install Gunicorn into the virtual environment with Pip.
sudo /opt/benvillelibrarysite/venv/bin/pip install gunicorn
If you already installed Gunicorn on your system (outside the virtual environment), you'll probably get a message saying Gunicorn is already installed - and it won't let you go forward. You can override that check with -I
sudo /opt/benvillelibrarysite/venv/bin/pip install -I gunicorn
See the Gunicorn docs page Deploying Gunicorn
My version of the command to run Gunicorn from the command line is
sudo -u benvilleus PYTHONPATH=/etc/opt/benvillelibrarysite:/opt/benvillelibrarysite \
DJANGO_SETTINGS_MODULE=settings \
/opt/benvillelibrarysite/venv/bin/gunicorn \
benvillelibrary.wsgi:application
Systemd
Section 6.3 of the book is configuring systemd. This is how to get the project running when the server starts without you having to start it from the command line. The remaining sections of Chapter 6 cover optimizing. I don't have much to add to Chapter 6 except for the 2 core VPS that I'm using as I write these instructions, I increased my workers to 8. I can probably go higher, but I don't expect the project I'm working on to have a lot of visiters
Mr. Christofides recommended a mail transport agent called Dragonfly Mail Agent, or dma. I couldn't get dma to work. I was able to draft the test message but dma never sent the email, didn't create a log, and didn't report any errors. I'll try on a differnet vps sometime and maybe update this post, but for project I'm working on as I write this post, I used Postfix
sudo apt install postfix
Where the Postfix installation wizard asked for type of configuration, I chose "Internet Site". Where it asked for the system mail name, I entered "mailer.benvillelibrary.gov".
Then I edited /etc/postfix/main.cf as follows:
- Find or create the myhostname entry and set it to something like
myhostname = mailer.benvillelibrary.gov
You can use something other than "mailer." - Find or edit relayhost as follows:
relayhost = [$EMAIL_HOST]:587
Where $EMAIL_HOST is the host name provided by your smarthost. In the book this would be mail.runbox.com. I'm using Brevo for my project so the one I used is smtp-relay.brevo.com. I'll be using Brevo for the remaining examples. - Add the following:
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
- Create the password file hash:/etc/postfix/sasl_passwd and enter a line like this:
[smtp-relay.brevo.com]:587 admin@benvillelibrary.gov:S3cretPa$$w0rd
After making the above edits, I mapped the password file to a database with:
sudo postmap /etc/postfix/sasl_passwd
Then I restarted Postfix with
sudo systemctl stop postfix
sudo systemctl start postfix
I added the following lines to /etc/opt/benvillelibrarysite/settings.py:
SERVER_EMAIL = 'info@benvillelibrary.gov'
DEFAULT_FROM_EMAIL = 'info@benvillelibrary.gov'
ADMINS = [
('administrator', 'admin@benvillelibrary.gov'),
]
MANAGERS = ADMINS
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'localhost'
EMAIL_HOST_USER = ''
EMAIL_HOST_PASSWORD = ''
EMAIL_PORT = 25
EMAIL_USE_TLS = False
Then restarted the project with
sudo service benvillelibrarysite stop
sudo service benvillelibrarysite start
I checked Django's ability to send an email through Postfix by running
sudo -u benvilleus PYTHONPATH=/etc/opt/benvillelibrarysite:/opt/benvillelibrarysite \
DJANGO_SETTINGS_MODULE=settings \
/opt/benvillelibrarysite/venv/bin/python \
/opt/benvillelibrarysite/manage.py shell
and with the shell active:
from django.conf import settings
from django.core.mail import send_mail
admin_emails = [x[1] for x in settings.ADMINS]
send_mail("Test", "This is a test", settings.SERVER_EMAIL,
admin_emails)
More to come
I'll add more to this post but Mr. Christofides's book is pretty complete and you can probably just continue with that from here.