Deployment day, and it's Django. Where to start? We got a server we can SSH into, we got our code and endpoints, if any, ready to be consumed.
Right here, we are going to look into the best practices in terms of developing our backend applications, or SSRs (if we were using Django templating). Through the process, we elaborate on the 'why's' of having a production-ready application as soon as we create our project as opposed to waiting on the completion of the whole development process.
For one, a good product is never really finished. There is always that one UI change, that one bug or logic discrepancy, or better yet, user feedback prompting a feature addition: our application must be getting some traction. Look at this in this way, would we wait till we got all our user feedback before optimizing our application for production? What about how we serve our media files? Would we still be serving it directly all through? Would we have passwords to our databases hardcoded? What if I add a feature or adjust a UI on my local setup? Would it break the main production app right off the batt?
A pointer we are getting to is this: launch your application and make small incremental changes as you go. There is no 'Tadaaa! It works and will never break or be changed!' moment.
If you were simply coding as though it was on your machine all through and waiting for the 'perfect' opportunity, you have to STOP. Stop it. Get your application and pipelines up as soon as you start the project. Be production-ready with 'Hello world!'
Enough talk. Let us have something we can work with here. We create a virtual environment, install our dependencies, and have our boilerplate project.
mkdir launch-ready
cd launch-ready
python -m venv .venv
source .venv/bin/activate
We've made the directory called
launch-ready
, our intended application name. We've also created and activated the virtual environment. Go ahead and install Django and create the project while at it.pip install django
django-admin startproject LaunchReady .
Our directory should now look like this:
(.venv) launch-ready ➤ ls
LaunchReady manage.py
(.venv) launch-ready ➤ ls LaunchReady
asgi.py __init__.py settings.py urls.py wsgi.py
(.venv) launch-ready ➤
Running
python manage.py runserver
will give the default launch page for Django.Our application is up and running. To prepare for production, however, we have a couple of things to configure.
The
LaunchReady
directory has one setting file. Within this directory, we create yet another directory named settings
and move our settings file into it, renaming it in the process to base.py
.mkdir settings
mv -i settings.py settings/base.py
Create two additional files within the
settings
directory and name them dev.py
- to contain development settings - and prod.py
- to have production settings. Our directory ends up looking as below:(.venv) settings ➤ ls
base.py dev.py prod.py
Locally, on our development machine, we will opt for our local installation of MySQL. Go ahead and create a database and user then head over back for the next step.
As with best practice in terms of version control, never store your passwords in code. Opt at all times, for the environment variables. To manage this, we create a
.env
file that will never make it past our local machine. Just to be sure, we add it to the .gitignore
file.echo .env >> .gitignore
We then, go ahead and install
python-dotenv
(to read the variables from the .evn file )and mysqlclient
(a wrapper to interact with MySQL from Django). pip install python-dotenv mysqlclient
Our development settings will look as below.
dev.py
from dotenv import load_dotenv
from LaunchReady.settings.base import *
load_dotenv()
# since it's running on my machine, show me the errors
DEBUG = True
SECRET_KEY = os.getenv("SECRET_KEY")
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"NAME": os.getenv("DATABASE_NAME"),
"USER": os.getenv("DATABASE_USER"),
"PASSWORD": os.getenv("DATABASE_PASSWORD"),
"HOST": os.getenv("DATABASE_HOST")
}
}
# show mail messages on the terminal
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# run on every host.
ALLOWED_HOSTS = ["*"]
So we have our environment variables being read from within the development settings, but where do we set these items. Simple. We create a
.env
file next to our dev
settings. (.venv) settings ➤ ls -a
base.py dev.py prod.py .env
The file content?
SECRET_KEY='django-generated-secret-key'
DATABASE_NAME=database_name
DATABASE_USER=database_user
DATABASE_PASSWORD=database_password
DATABASE_HOST=localhost
Note: We can have multiple ways with the database and secret key configuration. It is not linear.
For our production application, we take the example off of Heroku. This works the same as with any other server we might get, with one or two tweaks.
We install
dj-database-url
- a python package that lets us read and perform actions on the database in the event it was on a separate platform or other from our main application.pip install dj-database-url
This, we use in our production-ready settings as below:
prod.py
import dj_database_url
from LaunchReady.settings.base import *
ADMINS = (("Developer name", "Developer email"),)
# always set this to false in production
DEBUG = False
SECRET_KEY = os.environ["SECRET_KEY"]
ALLOWED_HOSTS = ["launch-ready-domain.com", "server-ip-address"]
DATABASES = {}
DATABASES["default"] = dj_database_url.config(conn_max_age=600, ssl_require=True)
# ToDo: get an email host provider
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.gmail.com"
EMAIL_HOST_USER = os.environ["EMAIL_HOST_USER"]
EMAIL_HOST_PASSWORD = os.environ["EMAIL_PASSWORD"]
EMAIL_PORT = 587
EMAIL_USE_TLS = True
As our
DEBUG
is set to False, we, instead of getting the not found error, want to get a notification via mail. This is especially so when one of our views returns an exception. In this case, we set the developer(s) responsible.ADMINS = (("Developer name", "Developer email"),)
As we have our Linux server up. We create persistent environment variables, this time, read from the bash configuration files, setting them in either,
/etc/environment
(for system-wide environment configuration), or in ~/.bashrc
- for per-user configuration in case we have multiple./etc/environment
SECRET_KEY=''
DATABASE_URL=''
# this is common with heroku and can be set via the console or dashboard interface
DJANGO_SETTINGS_MODULE=''
EMAIL_HOST_USER=''
EMAIL_PASSWORD=''
The only major difference from the previous development setting would be how we read our database configuration.
DATABASES = {}
DATABASES["default"] = dj_database_url.config(conn_max_age=600, ssl_require=True)
This would be how we configure our application to read from an external database from our application server location.
Let's try this run. Go to your shell and run your development server. Remember, we nested and split our settings. So to run anything while in development, we have to remind Django which settings we want to use.
python manage.py runserver --settings=LaunchReady.settings.dev
Our application default template is back up.
For production, we would have it run as below.
python manage.py runserver 0.0.0.0:8000 --settings=LaunchReady.settings.prod
Launch your browser and head to http://launch-ready-domain.com:8000. You should get the very same page as with your development environment.
Of course, there's more in terms of deployment server settings and configuration, especially in terms of serving static files. This, however, can be left for the next hour. We touch base with this rather lengthy piece as we fuel up and do this further in the next, and yes, the code can be accessed right off TheGreenCodes repository.
Till next time,
TheGreenCodes