This guide starts after the first boot of an Ubuntu 20.04 LTS Server system. I assume you're already familiar with Django (this is not a Hello World Django tutorial).
Run the following commands as root, or prepend them with sudo
if that's more your speed.
# Install SSH and enable SSH apt install ssh -y # Update the system apt update apt upgrade -y reboot # Install Python, Git, and Nginx (Postgres is more complicated). NOTE: openldap-devel is optional if you don't need it for your project. apt install python3 python3-dev git nginx gcc -y # Enable the official Postgres repo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - apt update # Install the latest version of Postgres (13.1 at time of writing) apt install postgresql postgresql-server-dev-13 -y # Enable and run Postgres and Nginx systemctl enable postgresql nginx --now # OPTIONAL packages I use for my own project (for LDAP authentication, and PostGIS for geographic support) apt install libldap-dev libsasl2-dev postgresql-13-postgis-2.5 -y
Now some Django-specific stuff
# Make a user. Your Django app will run as this user. adduser --system --group --home=/opt/django django # Install virtualenv apt install python3-virtualenv -y # You can use sudo to switch to the django user now if you're inclined. sudo -u django bash # Create new virtual environment for your Django app virtualenv /opt/django/venv # Activate your virtual environment source /opt/django/venv/bin/activate
At this point, we take a break to set up a starter Django project to (re)familiarize ourselves with the Nginx/Gunicorn/Django stack. We can trash the starter project when we're done.
# Install Django and gunicorn pip install Django gunicorn # change to django directory cd /opt/django # Create a test app django-admin startproject djangotest # change to test project directory cd djangotest # Run migrations ./manage.py migrate # Run Django's built-in server (figure out what your external IP address is with ``ip addr``) ./manage.py runserver 0.0.0.0:8000
Now go to http://192.168.10.205:8000 (change the IP address obviously). You should get a DisallowedHost at /
error. This is normal at this point, but it proves that Django is working.
Edit the djangotest/settings.py
file to add the IP address to the ALLOWED_HOSTS
list.
Try running the Django test project through gunicorn:
gunicorn djangotest.wsgi -b 192.168.10.205
You should be able to get to the Django test project in your browser. This time it's gunicorn serving up the site instead of Django's built-in server.
Now we'll add Nginx into the mix. First, make sure Nginx is accessible by going to http://192.168.10.205 (default port of 80). You should see the default Nginx Redhat splash page.
Create a file called /etc/nginx/conf.d/test.conf
and add the following to it:
server { listen 80; server_name 192.168.10.205; location / { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # This is the path to the UNIX socket that gunicorn listens to requests on proxy_pass http://unix:/opt/django/run/socket; } }
Now we restart Nginx and also run gunicorn so we can test our new configuration:
# Restart nginx to apply the config systemctl restart nginx # Create a folder for gunicorn's socket mkdir /opt/django/run # Make sure owner is django user chown django /opt/django/run # Give "group" and "others" read/execute permission on the /opt/django folder so that nginx can access the socket file chmod go+rx /opt/django /opt/django/run # Change to the test folder cd /opt/django/djangotest gunicorn --workers 3 --bind unix:/opt/django/run/socket djangotest.wsgi
Now visit the website and you should see the default Django page. After we confirm this all works, we can return to getting our real Django site working.
Here are some assumptions I'm making:
First, let's address Postgres' configuration:
# Switch to the postgres user. su - postgres # Create the database user for your Django site. When prompted about making the new user a superuser, I recommend answering yes. You can remove superuser permissions later. createuser --interactive --pwprompt # Create the database, assigning the owner to our djangodb user createdb -O djangouser djangodb
Edit the /etc/postgresql/13/main/pg_hba.conf
file. Underneath the local all all peer
line add host djangodb djangouser 127.0.0.1/32 md5
. Comment out the two lines that start with host all
. In the end your file should look something like this:
# TYPE DATABASE USER ADDRESS METHOD # "local" is for Unix domain socket connections only local all all peer host djangodb djangouser 127.0.0.1/32 md5 # IPv4 local connections: #host all all 127.0.0.1/32 ident # IPv6 local connections: #host all all ::1/128 ident # Allow replication connections from localhost, by a user with the # replication privilege. local replication all peer host replication all 127.0.0.1/32 ident host replication all ::1/128 ident
Reload Postgres to apply the changes, and try to connect with your Django database user account.
# Reload Postgres systemctl reload postgresql@13-main.service # Try to log in psql -U djangouser -h 127.0.0.1 djangodb
Assumptions:
myproject
. It will live in /opt/django/myproject
.main
.main.settings_for_prod_centos8
(so the full path will be /opt/django/myproject/main/settings_for_prod_centos8.py
).Time to get our real Django app running.
# Change to Django directory cd /opt/django # Clone your project's repo git clone https://github/yourname/myproject # We need pg_config so that we can build PostgreSQL support for Django (the pscyopg2 module) export PATH=/lib/postgresql/13/bin:$PATH # Install your requirements pip install -r myproject/requirements.txt # Change into your project directory cd myproject
I assume your projects uses separate files for customizing the settings between development, testing, and production.
Update your myproject/settings_for_prod.py
and customize it with your database settings. The DATABASES
section will look like this:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'djangoudb', 'USER': 'djangouser', 'PASSWORD': 'whatever_you_set_earlier', 'HOST': '127.0.0.1', 'PORT': 5432, } }
Now we can try running Django's built-in server with your real project.
# Create the static folder mkdir /opt/django/static # Create the media folder mkdir /opt/django/media # Set DJANGO_SETTINGS_MODULE to your project' export DJANGO_SETTINGS_MODULE=main.settings_for_prod_centos8 # Run the migrations with the ''--plan'' parameter to see what Django will do to the database. ./manage.py migrate --plan # If you saw no errors, run the migrations for real ./manage.py migrate # Run collectstatic ./manage.py collectstatic # Test your app with the Django's runserver command ./manage.py runserver 0.0.0.0:8000
If you can access your app, congratulations! You did it! But you're not finished yet. Let's get Nginx working with our Django app.
# Set the owner of the directory to django chown django:django /opt/django/run # Make a copy of our Nginx test.conf file cp /etc/nginx/conf.d/test.conf /etc/nginx/conf.d/django.conf # Rename the test.conf file so Nginx doesn't load it (alternatively, delete it) mv /etc/nginx/conf.d/test.conf /etc/nginx/conf.d/test.unused
Update the /etc/nginx/conf.d/django.conf
file to include the /static/ files config:
server { listen 80; server_name 192.168.10.205; location /static/ { root /opt/django; } location / { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://unix:/opt/django/run/socket; } }
Try running Nginx and Django with this configuration:
# Change to the folder where your app's manage.py lives before running the following commands. cd /opt/django/myproject # Restart Nginx systemctl restart nginx # Run Gunicorn gunicorn --workers 3 --bind unix:/opt/django/run/socket myproject.wsgi
The site should now load up and work! If it doesn't, try to do it better next time. Stop the gunicorn process.
To make Gunicorn/Django start automatically we have to create a Systemd service definition file /etc/systemd/system/django-gunicorn.service
.
[Unit] Description=Django Gunicorn service After=network.target [Service] User=django Group=www-data WorkingDirectory=/opt/django/myproject ExecStart=/opt/django/venv/bin/gunicorn --workers 3 --bind unix:/opt/django/run/socket myproject.wsgi Environment=DJANGO_SETTINGS_MODULE=myproject.settings_for_prod [Install] WantedBy=multi-user.target
Start the service:
systemctl enable django-gunicorn.service --now
And your site should be available! You did it! But you're NOT FINISHED. Time to secure everything!
If you were to simply turn firewall on, you wouldn't be able to access your site because:
# Reset incoming/outgoing to defaults ufw default deny incoming ufw default allow outgoing # Allow SSH (or ufw allow <portnumber> if you've changed the SSH port) ufw allow ssh # Allow HTTP and TLS ufw allow http ufw allow https # Turn on the firewall ufw enable # If you were running some commands as root, make sure to reset the permissions on the project folder to the django user. chown -Rv django:django /opt/django/myproject # Remove "group" and "other" permissions from the project folder. We don't want others to see our settings file! chmod go-rwx /opt/django/myproject # If you want to check permissions, run the following: sudo -u nginx cat /opt/django/myproject/myproject/settings_for_prod_centos8.py # the above command should fail with a "Permission denied" error. # Remove the PostgreSQL superuser permission from the django database user: su - postgres psql -d djangodb -c "ALTER USER djangouser WITH NOSUPERUSER;"
Your site should be accessible and somewhat secure. Somewhat? Yeah, you didn't secure SSH, you didn't install fail2ban, you didn't enable TLS, and you didn't set up any backups. You fool. You moron. Go learn how to do that somewhere else.
Reboot. If your site comes up and works without any further intervention, congratulations! You did it!