DEV Community

Cover image for How to Deploy Django on a Budget with Hetzner and Dokku
Developer Service
Developer Service

Posted on • Originally published at developer-service.blog

How to Deploy Django on a Budget with Hetzner and Dokku

Deploying a Django application can be challenging, especially when choosing the right infrastructure.

Hetzner, combined with Dokku, provides a strong and flexible solution that makes the deployment process easier.

Dokku, a platform-as-a-service (PaaS) built on Docker, lets you deploy, manage, and scale applications with ease.

This guide will show you how to deploy a Django application to Hetzner using Dokku.


What is Hetzner and Dokku?

Hetzner is a German company that offers various web hosting services, such as dedicated servers, cloud hosting, virtual private servers (VPS), and colocation services.

They are known for providing high-performance infrastructure at affordable prices, making them popular among developers, businesses, and tech enthusiasts.

Hetzner's data centers are mainly located in Germany and Finland (but also the USA and now Singapore), offering strong security, reliability, and scalable solutions.

They are particularly favored for their cost-effective cloud hosting options, making them a strong competitor in the European hosting market.

Dokku

Dokku is an open-source tool that makes it easy to deploy and manage web applications.

It uses Docker and lets you deploy apps with Git, similar to Heroku. Dokku works with many programming languages and frameworks, and it can handle databases, SSL certificates, and more.

It's lightweight and simple to set up on a server, making it a popular choice for developers who want a self-hosted solution like Heroku but with more control and flexibility.


Are you tired of writing the same old Python code? Want to take your programming skills to the next level? Look no further! This book is the ultimate resource for beginners and experienced Python developers alike.

Get "Python's Magic Methods - Beyond init and str"

Magic methods are not just syntactic sugar, they're powerful tools that can significantly improve the functionality and performance of your code. With this book, you'll learn how to use these tools correctly and unlock the full potential of Python.


Pre-Requisites

Before we start the deployment process, make sure you have the following:

  • A Django Application: Your Django app should be ready for deployment. It should include a requirements.txt file and a Procfile.
  • A Hetzner Cloud Account: If you don't have one, you can sign up at Hetzner.
  • A GitHub Account: To be able to deploy your application.
  • Basic Command Line Knowledge: You should be comfortable using the terminal.

Step 1 - Create a VPS in Hetzner

Start by creating a VPS (Virtual Private Server) in the Hetzner Cloud. If you don't have an account, you can sign up here.

Once on the cloud console:

  • Create a new project, let's call it Hetzner Dokku. Enter the project and Add Server.
  • For Location, let's choose Helsinki (eu-central).
  • For Image, let's choose Ubuntu 22.04 (Dokku doesn't support 24.04 at the time of writing)
  • For Type, we choose Shared CPU, x86 and CX22 (2vCPUS, 4GB RAM, 40GB SSD).
  • For Networking, we leave both IPv4 and IPv6 on.
  • For SSH Keys, you should add your SSH key. If you don't have one, you can generate it with ssh-keygen.
  • The other options you can leave off for now. Later on you can active a firewall if needed.
  • Finally, give it a name, in this case, let's called dokku-server.

Then click on Create & Buy now. After a couple of seconds, your VPS should be up and running and accessible with SSH in the indicated IP address:

ssh root@<ip_address>
Enter fullscreen mode Exit fullscreen mode

Step 2- Install Dokku

Now that the VPS is configured and running, you can install Dokku. Before that, it is a good policy to update the VPS, which you can do with:

apt update && apt upgrade -y
Enter fullscreen mode Exit fullscreen mode

If necessary, restart the VPS.

Installing Dokku

You can now install Dokku:

wget -NP . https://dokku.com/install/v0.34.8/bootstrap.sh
sudo DOKKU_TAG=v0.34.8 bash bootstrap.sh
Enter fullscreen mode Exit fullscreen mode

At the time of writing the latest version was v0.34.8. Check the Dokku website for the latest version.

This might take a couple of minutes since it also needs to pull a couple of Docker images that support the deployment process.

You should restart the server after installing it to make sure all packages are active and running.

Setup SSH key and Virtualhost Settings

For accessing applications, it is advisable to have a domain name. But you can also use the IP address with sub-domain support with https://sslip.io/.

First, you need to copy your SSH key to the Dokku SSH Admin key:

cat ~/.ssh/authorized_keys | dokku ssh-keys:add admin
Enter fullscreen mode Exit fullscreen mode

Then, you can set your domain or IP address:

dokku domains:set-global <ip_address>.sslip.io
Enter fullscreen mode Exit fullscreen mode

In this case, it defines the IP address, but with support for sub-domains with sslip.io.


Step 3 - Creating the application in Dokku

With Dokku installed and configured, you can now create the application in Dokku that will receive your Django application:

dokku apps:create django-app
Enter fullscreen mode Exit fullscreen mode

This command creates an app called django-app.

Django requires a database and normally there is two choices, either a SQLite or Postgres database.

On Dokku, databases are part of the plugin packages and need to be installed separately:

dokku plugin:install https://github.com/dokku/dokku-postgres.git
Enter fullscreen mode Exit fullscreen mode

This command will install Postgres by downloading the supporting Docker images.

To actually create a database that you can use with the recently created application, you can use the command:

dokku postgres:create django-app-db
Enter fullscreen mode Exit fullscreen mode

Once the the database is created, you will need to link it to the Dokku application:

dokku postgres:link django-app-db django-app
Enter fullscreen mode Exit fullscreen mode

Linked the database to the application means that the database URL is added to the environment variables of the application definitions in Dokku.

You can check that with the command:

dokku config:show django-app
Enter fullscreen mode Exit fullscreen mode

Which should give a similar response to this:

=====> django-app env vars
DATABASE_URL:  postgres://postgres:bca0d7f59caf98c78a74d58457111a1e@dokku-postgres-django-app-db:5432/django_app_db
Enter fullscreen mode Exit fullscreen mode

Step 4 - Creating the Django application

With Dokku installed, configured and an application created, you are now ready to create the actual Django application.

There are several guides on the Internet on how to create a Django application from scratch, so the focus on this article is on deploying the application to Django.

For this tutorial, I created a Django application in PyCharm using the default settings called DjangoAppDokku.

To be able to deploy the application to Dokku, there is some preparation steps that are necessary.

Let's start with making sure there is a requirements file with the command:

pip freeze > requirements.txt
Enter fullscreen mode Exit fullscreen mode

Then you can install some necessary packages to prepare for the Dokku deploying:

pip install python-decouple dj-database-url gunicorn whitenoise psycopg2-binary
Enter fullscreen mode Exit fullscreen mode

This installs three useful packages:

  • python-decouple for managing configuration settings.
  • dj-database-url for simplifying database configuration in Django.
  • gunicorn for serving Python web applications in production.
  • whitenoise to manage access to static files.
  • psycopg2-binary to allow access to Postgres databases.

Now you can create the environment variable file on a file called .env:

DATABASE_URL=sqlite:///db.sqlite3
Enter fullscreen mode Exit fullscreen mode

This configures (for local use) a database URL using SQLite. When the application is deployed to Django it will use the URL defined in Dokku, as we have seen before.

For that to be possible, you need to make some changes in the settings.py file:

import dj_database_url
import os
from decouple import config
from django.conf.global_settings import DATABASES

...

ALLOWED_HOSTS = ['*']

...

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    "whitenoise.middleware.WhiteNoiseMiddleware",
    ... all others middleware ...
]

...

DATABASES['default'] = dj_database_url.parse(config('DATABASE_URL'), conn_max_age=600)

...

STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / "static"
STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'staticfiles'),
)
STATICFILES_STORAGE = 'whitenoise.storage.CompressedStaticFilesStorage'

Enter fullscreen mode Exit fullscreen mode

This code snippet configures various settings for a Django application:

  • Database Configuration: Uses dj_database_url and decouple to parse the database URL from environment variables.
  • Allowed Hosts: Allows the application to be served from any host (useful for development), but should not be used in Production.
  • Middleware: Includes Django's security middleware and WhiteNoise middleware for serving static files.
  • Static Files: Configures the URL, root directory, additional directories, and storage backend for static files using WhiteNoise.

For Dokku to know how to run the Django application and how to migrate the database, yo will need to create a Procfile:

web: gunicorn DjangoAppDokku.wsgi

release: python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

The Procfile defines two important processes for your Django application, which Dokku will execute:

  • web: gunicorn DjangoAppDokku.wsgi: Starts the Gunicorn web server to handle incoming HTTP requests using the WSGI application defined in DjangoAppDokku.wsgi.
  • release: python manage.py migrate: Runs Django's database migrations as part of the release process, ensuring that the database schema is up-to-date with the latest code changes.

The last set is to make sure that the requirements file is up to date:

pip freeze > requirements.txt
Enter fullscreen mode Exit fullscreen mode

Step 5 - Deploying the Django application to Dokku

With the Django application and the Dokku application prepared, you can now deploy the application to Dokku.

First you will need create a new repository on GitHub then you can add the Django application to it.

So, on your Django project directory, open a terminal and execute these commands:

echo "# DjangoAppDokku" >> README.md
git init
git add .
git commit -m "First Commit"
git branch -M main
git remote add origin [Your GitHub repository URL]
git push -u origin main

Enter fullscreen mode Exit fullscreen mode

Now you add a new git remote for the Dokku server:

git remote add dokku dokku@<your_server_ip>:<your_dokku_app_name>
Enter fullscreen mode Exit fullscreen mode

And finally deploy the application with:

git push dokku
Enter fullscreen mode Exit fullscreen mode

This will produce an output similar to this:

Enumerating objects: 23, done.
Counting objects: 100% (23/23), done.
Delta compression using up to 8 threads
Compressing objects: 100% (21/21), done.
Writing objects: 100% (23/23), 5.48 KiB | 160.00 KiB/s, done.
Total 23 (delta 8), reused 0 (delta 0), pack-reused 0
-----> Cleaning up...
-----> Building django-app from herokuish
-----> Adding BUILD_ENV to build environment...
       BUILD_ENV added successfully
-----> Python app detected
-----> No Python version was specified. Using the buildpack default: python-3.12.5
       To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes
-----> Requirements file has been changed, clearing cached dependencies
-----> Installing python-3.12.5
-----> Installing pip 24.0, setuptools 70.3.0 and wheel 0.43.0
-----> Installing SQLite3
-----> Installing requirements with pip
       Collecting asgiref==3.8.1 (from -r requirements.txt (line 1))
       Downloading asgiref-3.8.1-py3-none-any.whl.metadata (9.3 kB)
       Collecting dj-database-url==2.2.0 (from -r requirements.txt (line 2))
       Downloading dj_database_url-2.2.0-py3-none-any.whl.metadata (12 kB)
       Collecting Django==5.1 (from -r requirements.txt (line 3))
       Downloading Django-5.1-py3-none-any.whl.metadata (4.2 kB)
       Collecting psycopg2-binary==2.9.9 (from -r requirements.txt (line 4))
       Downloading psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (4.4 kB)
       Collecting python-decouple==3.8 (from -r requirements.txt (line 5))
       Downloading python_decouple-3.8-py3-none-any.whl.metadata (14 kB)
       Collecting sqlparse==0.5.1 (from -r requirements.txt (line 6))
       Downloading sqlparse-0.5.1-py3-none-any.whl.metadata (3.9 kB)
       Collecting typing_extensions==4.12.2 (from -r requirements.txt (line 7))
       Downloading typing_extensions-4.12.2-py3-none-any.whl.metadata (3.0 kB)
       Collecting tzdata==2024.1 (from -r requirements.txt (line 8))
       Downloading tzdata-2024.1-py2.py3-none-any.whl.metadata (1.4 kB)
       Collecting whitenoise==6.7.0 (from -r requirements.txt (line 9))
       Downloading whitenoise-6.7.0-py3-none-any.whl.metadata (3.7 kB)
       Downloading asgiref-3.8.1-py3-none-any.whl (23 kB)
       Downloading dj_database_url-2.2.0-py3-none-any.whl (7.8 kB)
       Downloading Django-5.1-py3-none-any.whl (8.2 MB)
       Downloading psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.0 MB)
       Downloading python_decouple-3.8-py3-none-any.whl (9.9 kB)
       Downloading sqlparse-0.5.1-py3-none-any.whl (44 kB)
       Downloading typing_extensions-4.12.2-py3-none-any.whl (37 kB)
       Downloading tzdata-2024.1-py2.py3-none-any.whl (345 kB)
       Downloading whitenoise-6.7.0-py3-none-any.whl (19 kB)
       Installing collected packages: python-decouple, whitenoise, tzdata, typing_extensions, sqlparse, psycopg2-binary, asgiref, Django, dj-database-url
       Successfully installed Django-5.1 asgiref-3.8.1 dj-database-url-2.2.0 psycopg2-binary-2.9.9 python-decouple-3.8 sqlparse-0.5.1 typing_extensions-4.12.2 tzdata-2024.1 whitenoise-6.7.0
-----> $ python manage.py collectstatic --noinput
       System check identified some issues:
       WARNINGS:
       ?: (staticfiles.W004) The directory '/tmp/build/staticfiles' in the STATICFILES_DIRS setting does not exist.
       127 static files copied to '/tmp/build/static'.

-----> Discovering process types
       Procfile declares types -> release, web
-----> Releasing django-app...
-----> Checking for predeploy task
       No predeploy task found, skipping
-----> Checking for release task
       Executing release task from Procfile in ephemeral container: python manage.py migrate
=====> Start of django-app release task (ae9dc2b83) output
remote:  !     System check identified some issues:
remote:  !     WARNINGS:
remote:  !     ?: (staticfiles.W004) The directory '/app/staticfiles' in the STATICFILES_DIRS setting does not exist.
       Operations to perform:
         Apply all migrations: admin, auth, contenttypes, sessions
       Running migrations:
         Applying contenttypes.0001_initial... OK
         Applying auth.0001_initial... OK
         Applying admin.0001_initial... OK
         Applying admin.0002_logentry_remove_auto_add... OK
         Applying admin.0003_logentry_add_action_flag_choices... OK
         Applying contenttypes.0002_remove_content_type_name... OK
         Applying auth.0002_alter_permission_name_max_length... OK
         Applying auth.0003_alter_user_email_max_length... OK
         Applying auth.0004_alter_user_username_opts... OK
         Applying auth.0005_alter_user_last_login_null... OK
         Applying auth.0006_require_contenttypes_0002... OK
         Applying auth.0007_alter_validators_add_error_messages... OK
         Applying auth.0008_alter_user_username_max_length... OK
         Applying auth.0009_alter_user_last_name_max_length... OK
         Applying auth.0010_alter_group_name_max_length... OK
         Applying auth.0011_update_proxy_permissions... OK
         Applying auth.0012_alter_user_first_name_max_length... OK
         Applying sessions.0001_initial... OK
=====> End of django-app release task (ae9dc2b83) output
-----> Checking for first deploy postdeploy task
       No first deploy postdeploy task found, skipping
=====> Processing deployment checks
remote:  !     No healthchecks found in app.json for web process type
       No web healthchecks found in app.json. Simple container checks will be performed.
       For more efficient zero downtime deployments, add healthchecks to your app.json. See https://dokku.com/docs/deployment/zero-downtime-deploys/ for examples
-----> Deploying django-app via the docker-local scheduler...
-----> Deploying web (count=1)
       Attempting pre-flight checks (web.1)
-----> Executing 2 healthchecks
       Running healthcheck name='default' type='uptime' uptime=10
       Running healthcheck name='port listening check' attempts=3 port=8000 retries=2 timeout=5 type='listening' wait=5
       Healthcheck succeeded name='port listening check'
       Healthcheck succeeded name='default'
       All checks successful (web.1)
=====> Start of django-app container output (8d55f0f3b481 web.1)
       Python buildpack: Couldn't determine available memory. Skipping automatic configuration of WEB_CONCURRENCY.
       [2024-08-29 08:32:56 +0000] [14] [INFO] Starting gunicorn 23.0.0
       [2024-08-29 08:32:56 +0000] [14] [INFO] Listening at: http://0.0.0.0:8000 (14)
       [2024-08-29 08:32:56 +0000] [14] [INFO] Using worker: sync
       [2024-08-29 08:32:56 +0000] [153] [INFO] Booting worker with pid: 153
=====> End of django-app container output (8d55f0f3b481 web.1)
=====> Triggering early nginx proxy rebuild
-----> Ensuring network configuration is in sync for django-app
-----> Configuring django-app.<ip_address>.sslip.io...(using built-in template)
-----> Creating http nginx.conf
       Reloading nginx
-----> Running post-deploy
-----> Ensuring network configuration is in sync for django-app
-----> Configuring django-app.<ip_address>.sslip.io...(using built-in template)
-----> Creating http nginx.conf
       Reloading nginx

-----> Checking for postdeploy task
       No postdeploy task found, skipping
=====> Application deployed:
       http://django-app.<ip_address>.sslip.io

To <ip_address>:django-app
 * [new branch]      master -> master

Enter fullscreen mode Exit fullscreen mode

Dokku has performed the following actions to deploy the application:

  • Installed the latest Python version.
  • Installed the requirements from your requirements.txt file.
  • Collected static with python manage.py collectstatic --noinput.
  • Migrated the database with python manage.py migrate (defined in the Procfile).
  • Started the gunicorn server.

If you now go to the indicated URL, you should see the familiar Django default page:

Django default page on Dokku

And that is it. That is all that is need to deploy a Django application to Dokku.

These settings are good to test an application and to provide access to beta users, but for production use you should add a domain name to your application and enable SSL, check the Dokku documentation for more info.


Conclusion

You have successfully deployed your Django application to Hetzner using Dokku.

This setup makes it easy to scale and manage your application, thanks to Dokku's simple yet powerful features.

Now that your application is deployed, you can focus on improving and expanding it, knowing that it is running on a reliable infrastructure.

Top comments (2)

Collapse
 
king_triton profile image
King Triton

This guide is fantastic! As someone who's been struggling with deploying Django apps efficiently, the combination of Hetzner and Dokku seems like a game-changer. The step-by-step instructions are clear and detailed, making it easy to follow along, even for those who might be new to server management. I love how you covered everything from setting up the VPS to linking the database and even deploying the app via GitHub. This is definitely going to help streamline my future deployments. Thanks for sharing such a comprehensive guide!

Collapse
 
devasservice profile image
Developer Service

Thanks