Jekyll2022-12-13T07:41:16+00:00https://saasitive.com/React and Django TutorialReact and Django Tutorial{"name"=>nil, "email"=>nil, "twitter"=>nil}Docker compose with Django 4, Celery, Redis and Postgres2022-11-17T00:00:00+00:002022-11-17T00:00:00+00:00https://saasitive.com/tutorial/django-celery-redis-postgres-docker-compose<p><img src="/tutorial/django-celery-redis-postgres-docker-compose/banner.jpg" alt="Setup docker compose with Django 4, Celery, Redis and Postgres" /> Deploying Django application that is using Celery and Redis might be challenging. Thanks to <code class="language-plaintext highlighter-rouge">docker-compose</code> tool we can prepare docker containers locally and make deployment much easier. I would like to show you my approach for constructing <code class="language-plaintext highlighter-rouge">docker-compose</code> configuration that can be reused in other web applications. I will be using Django version <code class="language-plaintext highlighter-rouge">4.1.3</code> and Celery <code class="language-plaintext highlighter-rouge">5.2.7</code>. The nginx server will be used as reverse proxy for Django and to serve static files. Postgres database and Redis will be in the <code class="language-plaintext highlighter-rouge">docker-compose</code>.</p> <p>The <code class="language-plaintext highlighter-rouge">docker-compose</code> architecture created in this article is presented in the diagram below:</p> <p><img src="/tutorial/django-celery-redis-postgres-docker-compose/docker-compose-django-celery-redis-postgres-nginx-v2.png" alt="" /></p> <p>All code from this article is available in the <a href="https://github.com/saasitive/docker-compose-django-celery-redis-postgres">GitHub repository</a>. In case of any problems or questions, please add a GitHub issue there. Good luck!</p> <h2 id="example-project">Example project</h2> <p>We will build a simple web application that will add two numbers in the background. We will use <a href="https://www.django-rest-framework.org/">Django Rest Framework</a> API viewer as our User Interface. There will be an <code class="language-plaintext highlighter-rouge">Assignment</code> model in the database. It will have following fields:</p> <ul> <li><code class="language-plaintext highlighter-rouge">first_term</code>,</li> <li><code class="language-plaintext highlighter-rouge">second_term</code>,</li> <li><code class="language-plaintext highlighter-rouge">sum</code>.</li> </ul> <p>The <code class="language-plaintext highlighter-rouge">first_term</code> and <code class="language-plaintext highlighter-rouge">second_term</code> will be provided by REST API. The <code class="language-plaintext highlighter-rouge">sum</code> will be computed in the backgorund by Celery after object creation. Below is a screenshot from application view:</p> <p><img src="/tutorial/django-celery-redis-postgres-docker-compose/app-view.png" alt="" /></p> <p>The whole project will be packed with <code class="language-plaintext highlighter-rouge">docker-compose</code>. It will contain following containers: <code class="language-plaintext highlighter-rouge">ngnix</code>, <code class="language-plaintext highlighter-rouge">server</code>, <code class="language-plaintext highlighter-rouge">worker</code>, <code class="language-plaintext highlighter-rouge">redis</code>, <code class="language-plaintext highlighter-rouge">db</code>.</p> <h2 id="setup-environment">Setup environment</h2> <p>Let’s start by configuring local environment. I’m using Python <code class="language-plaintext highlighter-rouge">3.8</code> and Ubuntu <code class="language-plaintext highlighter-rouge">20.04</code>. Please create a new directory for the project or ideally a new git repository and start a new virtual environment there:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># create a new virtual environemnt</span> virtualenv venv <span class="c"># activate virtual environment</span> <span class="nb">source </span>venv/bin/activate </code></pre></div></div> <p>Please create a <code class="language-plaintext highlighter-rouge">requirements.txt</code> file with following packages:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>django djangorestframework markdown django-filter celery[redis] psycopg2-binary </code></pre></div></div> <p>We install Celery with all required packages to work with Redis. The <code class="language-plaintext highlighter-rouge">psycopg2-binary</code> is needed to connect Django with Postgres database. Please install required packages:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install</span> <span class="nt">-r</span> requirements.txt </code></pre></div></div> <p>Please make sure that you have Redis available locally (<a href="https://redis.io/docs/getting-started/">link</a> to official Redis documentation on how to install it). You can check if you have Redis installed correctly by typing:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>redis-cli </code></pre></div></div> <p>You should see connection open to Redis server. We will not use Postgres locally, we will stick to SQLite. However, you can use Postgres locally (<code class="language-plaintext highlighter-rouge">docker-compose</code> configuration will be the same).</p> <p>Please make sure that you have <code class="language-plaintext highlighter-rouge">docker</code> and <code class="language-plaintext highlighter-rouge">docker-compose</code> installed.</p> <h2 id="start-django-project">Start Django project</h2> <p>We need to bootstrap a Django project with <code class="language-plaintext highlighter-rouge">django-admin</code> tool (it is provided with Django package):</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>django-admin startproject backend </code></pre></div></div> <p>The above command will initialize an empty Django project. You should see directory structure like below:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>backend/ ├── backend │   ├── asgi.py │   ├── __init__.py │   ├── settings.py │   ├── urls.py │   └── wsgi.py └── manage.py </code></pre></div></div> <p>I call all my Django projects as <code class="language-plaintext highlighter-rouge">backend</code>. I usually use <a href="https://saasitive.com">Django with React</a>, so I have <code class="language-plaintext highlighter-rouge">frontend</code> and <code class="language-plaintext highlighter-rouge">backend</code> directories. You can name it as you want, but for similicity you can stick now with <code class="language-plaintext highlighter-rouge">backend</code> :)</p> <p>The next step is to change working directory to the <code class="language-plaintext highlighter-rouge">backend</code>. We will add a new Django app:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python manage.py startapp assignments </code></pre></div></div> <p>The new app name is <code class="language-plaintext highlighter-rouge">assignments</code>. You should see directory structure like below:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>backend/ ├── assignments │   ├── admin.py │   ├── apps.py │   ├── __init__.py │   ├── migrations │   │   └── __init__.py │   ├── models.py │   ├── tests.py │   └── views.py ├── backend │   ├── asgi.py │   ├── __init__.py │   ├── settings.py │   ├── urls.py │   └── wsgi.py └── manage.py </code></pre></div></div> <p>We can start a development server and see a launching rocket at <code class="language-plaintext highlighter-rouge">127.0.0.1:8000</code>:</p> <p><img src="/tutorial/django-celery-redis-postgres-docker-compose/django-empty-project.png" alt="" /></p> <h2 id="assignment-database-model">Assignment database model</h2> <p>Please update <code class="language-plaintext highlighter-rouge">INSTALLED_APPS</code> variable in the <code class="language-plaintext highlighter-rouge">backend/backend/settings.py</code>:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># rest of the code ... </span> <span class="n">INSTALLED_APPS</span> <span class="o">=</span> <span class="p">[</span> <span class="s">"django.contrib.admin"</span><span class="p">,</span> <span class="s">"django.contrib.auth"</span><span class="p">,</span> <span class="s">"django.contrib.contenttypes"</span><span class="p">,</span> <span class="s">"django.contrib.sessions"</span><span class="p">,</span> <span class="s">"django.contrib.messages"</span><span class="p">,</span> <span class="s">"django.contrib.staticfiles"</span><span class="p">,</span> <span class="s">"rest_framework"</span><span class="p">,</span> <span class="c1"># DRF </span> <span class="s">"assignments"</span><span class="p">,</span> <span class="c1"># new app </span><span class="p">]</span> <span class="c1"># rest of the code ... </span></code></pre></div></div> <p>Let’s create a database model for <code class="language-plaintext highlighter-rouge">Assignment</code> objects. Please edit <code class="language-plaintext highlighter-rouge">backend/assignments/models.py</code> file:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span> <span class="k">class</span> <span class="nc">Assignment</span><span class="p">(</span><span class="n">models</span><span class="p">.</span><span class="n">Model</span><span class="p">):</span> <span class="n">first_term</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="n">DecimalField</span><span class="p">(</span> <span class="n">max_digits</span><span class="o">=</span><span class="mi">5</span><span class="p">,</span> <span class="n">decimal_places</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="bp">False</span> <span class="p">)</span> <span class="n">second_term</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="n">DecimalField</span><span class="p">(</span> <span class="n">max_digits</span><span class="o">=</span><span class="mi">5</span><span class="p">,</span> <span class="n">decimal_places</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="bp">False</span> <span class="p">)</span> <span class="c1"># sum should be equal to first_term + second_term </span> <span class="c1"># its value will be computed in Celery </span> <span class="nb">sum</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="n">DecimalField</span><span class="p">(</span><span class="n">max_digits</span><span class="o">=</span><span class="mi">5</span><span class="p">,</span> <span class="n">decimal_places</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> </code></pre></div></div> <p>We need to create and apply migrations:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># create migrations</span> python manage.py makemigrations <span class="c"># apply migrations</span> python manage.py migrate </code></pre></div></div> <p>We will be using DRF <code class="language-plaintext highlighter-rouge">ModelViewSet</code> for creating a CRUD REST API for our models. We will need to add <code class="language-plaintext highlighter-rouge">serializers.py</code> file in the <code class="language-plaintext highlighter-rouge">backend/assignments</code> directory:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">rest_framework</span> <span class="kn">import</span> <span class="n">serializers</span> <span class="kn">from</span> <span class="nn">assignments.models</span> <span class="kn">import</span> <span class="n">Assignment</span> <span class="k">class</span> <span class="nc">AssignmentSerializer</span><span class="p">(</span><span class="n">serializers</span><span class="p">.</span><span class="n">ModelSerializer</span><span class="p">):</span> <span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span> <span class="n">model</span> <span class="o">=</span> <span class="n">Assignment</span> <span class="n">read_only_fields</span> <span class="o">=</span> <span class="p">(</span><span class="s">"id"</span><span class="p">,</span> <span class="s">"sum"</span><span class="p">)</span> <span class="n">fields</span> <span class="o">=</span> <span class="p">(</span><span class="s">"id"</span><span class="p">,</span> <span class="s">"first_term"</span><span class="p">,</span> <span class="s">"second_term"</span><span class="p">,</span> <span class="s">"sum"</span><span class="p">)</span> </code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">id</code> and <code class="language-plaintext highlighter-rouge">sum</code> fields are read-only. The <code class="language-plaintext highlighter-rouge">id</code> is automatically set in the database. The <code class="language-plaintext highlighter-rouge">sum</code> value will be computed in the Celery.</p> <p>The <code class="language-plaintext highlighter-rouge">backend/assignments/views.py</code> file content:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">transaction</span> <span class="kn">from</span> <span class="nn">rest_framework</span> <span class="kn">import</span> <span class="n">viewsets</span> <span class="kn">from</span> <span class="nn">rest_framework.exceptions</span> <span class="kn">import</span> <span class="n">APIException</span> <span class="kn">from</span> <span class="nn">assignments.models</span> <span class="kn">import</span> <span class="n">Assignment</span> <span class="kn">from</span> <span class="nn">assignments.serializers</span> <span class="kn">import</span> <span class="n">AssignmentSerializer</span> <span class="kn">from</span> <span class="nn">assignments.tasks</span> <span class="kn">import</span> <span class="n">task_execute</span> <span class="k">class</span> <span class="nc">AssignmentViewSet</span><span class="p">(</span><span class="n">viewsets</span><span class="p">.</span><span class="n">ModelViewSet</span><span class="p">):</span> <span class="n">serializer_class</span> <span class="o">=</span> <span class="n">AssignmentSerializer</span> <span class="n">queryset</span> <span class="o">=</span> <span class="n">Assignment</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="nb">all</span><span class="p">()</span> <span class="k">def</span> <span class="nf">perform_create</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">serializer</span><span class="p">):</span> <span class="k">try</span><span class="p">:</span> <span class="k">with</span> <span class="n">transaction</span><span class="p">.</span><span class="n">atomic</span><span class="p">():</span> <span class="c1"># save instance </span> <span class="n">instance</span> <span class="o">=</span> <span class="n">serializer</span><span class="p">.</span><span class="n">save</span><span class="p">()</span> <span class="n">instance</span><span class="p">.</span><span class="n">save</span><span class="p">()</span> <span class="c1"># create task params </span> <span class="n">job_params</span> <span class="o">=</span> <span class="p">{</span><span class="s">"db_id"</span><span class="p">:</span> <span class="n">instance</span><span class="p">.</span><span class="nb">id</span><span class="p">}</span> <span class="c1"># submit task for background execution </span> <span class="n">transaction</span><span class="p">.</span><span class="n">on_commit</span><span class="p">(</span><span class="k">lambda</span><span class="p">:</span> <span class="n">task_execute</span><span class="p">.</span><span class="n">delay</span><span class="p">(</span><span class="n">job_params</span><span class="p">))</span> <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span> <span class="k">raise</span> <span class="n">APIException</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">e</span><span class="p">))</span> </code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">ModelViewSet</code> is doing the job here with basic CRUD implementation - not much to code for us :) We overwrite the <code class="language-plaintext highlighter-rouge">perform_create</code> function to submit a background task after object creation. Please notice that object creation and submitting a background tasks are inside <code class="language-plaintext highlighter-rouge">transaction</code>.</p> <p>We need to implement the <code class="language-plaintext highlighter-rouge">task_execute</code> function. Please add <code class="language-plaintext highlighter-rouge">tasks.py</code> file in the <code class="language-plaintext highlighter-rouge">backend/assignments</code> directory:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">celery</span> <span class="kn">import</span> <span class="n">shared_task</span> <span class="kn">from</span> <span class="nn">assignments.models</span> <span class="kn">import</span> <span class="n">Assignment</span> <span class="o">@</span><span class="n">shared_task</span><span class="p">()</span> <span class="k">def</span> <span class="nf">task_execute</span><span class="p">(</span><span class="n">job_params</span><span class="p">):</span> <span class="n">assignment</span> <span class="o">=</span> <span class="n">Assignment</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">pk</span><span class="o">=</span><span class="n">job_params</span><span class="p">[</span><span class="s">"db_id"</span><span class="p">])</span> <span class="n">assignment</span><span class="p">.</span><span class="nb">sum</span> <span class="o">=</span> <span class="n">assignment</span><span class="p">.</span><span class="n">first_term</span> <span class="o">+</span> <span class="n">assignment</span><span class="p">.</span><span class="n">second_term</span> <span class="n">assignment</span><span class="p">.</span><span class="n">save</span><span class="p">()</span> </code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">task_execute</code> is a shared task, it will be discovered by Celery. It accepts one argument <code class="language-plaintext highlighter-rouge">job_params</code> that has <code class="language-plaintext highlighter-rouge">db_id</code>, which is <code class="language-plaintext highlighter-rouge">id</code> of <code class="language-plaintext highlighter-rouge">Assignment</code> object. We simply get the object by <code class="language-plaintext highlighter-rouge">id</code>, compute <code class="language-plaintext highlighter-rouge">sum</code> and save the object.</p> <p>One more thing to do, we need to wire <code class="language-plaintext highlighter-rouge">assignments</code> URL endpoints. Please add a new file <code class="language-plaintext highlighter-rouge">urls.py</code> in the <code class="language-plaintext highlighter-rouge">backend/assignments</code>:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">django.urls</span> <span class="kn">import</span> <span class="n">re_path</span> <span class="kn">from</span> <span class="nn">rest_framework.routers</span> <span class="kn">import</span> <span class="n">DefaultRouter</span> <span class="kn">from</span> <span class="nn">assignments.views</span> <span class="kn">import</span> <span class="n">AssignmentViewSet</span> <span class="n">router</span> <span class="o">=</span> <span class="n">DefaultRouter</span><span class="p">()</span> <span class="n">router</span><span class="p">.</span><span class="n">register</span><span class="p">(</span><span class="s">r"assignments"</span><span class="p">,</span> <span class="n">AssignmentViewSet</span><span class="p">,</span> <span class="n">basename</span><span class="o">=</span><span class="s">"assignments"</span><span class="p">)</span> <span class="n">assignments_urlpatterns</span> <span class="o">=</span> <span class="n">router</span><span class="p">.</span><span class="n">urls</span> </code></pre></div></div> <p>We are using DRF <code class="language-plaintext highlighter-rouge">DefaultRouter</code> to generate CRUD API endpoints. We need to add <code class="language-plaintext highlighter-rouge">assignments_urlpatterns</code> in the main <code class="language-plaintext highlighter-rouge">urls.py</code>. Please edit the file <code class="language-plaintext highlighter-rouge">backend/backend/urls.py</code>:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">django.contrib</span> <span class="kn">import</span> <span class="n">admin</span> <span class="kn">from</span> <span class="nn">django.urls</span> <span class="kn">import</span> <span class="n">path</span> <span class="kn">from</span> <span class="nn">assignments.urls</span> <span class="kn">import</span> <span class="n">assignments_urlpatterns</span> <span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span> <span class="n">path</span><span class="p">(</span><span class="s">"admin/"</span><span class="p">,</span> <span class="n">admin</span><span class="p">.</span><span class="n">site</span><span class="p">.</span><span class="n">urls</span><span class="p">),</span> <span class="p">]</span> <span class="c1"># add new urls </span><span class="n">urlpatterns</span> <span class="o">+=</span> <span class="n">assignments_urlpatterns</span> </code></pre></div></div> <h2 id="configure-celery">Configure Celery</h2> <p>We need to setup Celery to work with Django. Please add a new file <code class="language-plaintext highlighter-rouge">celery.py</code> in the <code class="language-plaintext highlighter-rouge">backend/backend</code> directory:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">os</span> <span class="kn">from</span> <span class="nn">celery</span> <span class="kn">import</span> <span class="n">Celery</span> <span class="kn">from</span> <span class="nn">django.conf</span> <span class="kn">import</span> <span class="n">settings</span> <span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">setdefault</span><span class="p">(</span><span class="s">"DJANGO_SETTINGS_MODULE"</span><span class="p">,</span> <span class="s">"backend.settings"</span><span class="p">)</span> <span class="n">app</span> <span class="o">=</span> <span class="n">Celery</span><span class="p">(</span><span class="s">"backend"</span><span class="p">)</span> <span class="n">app</span><span class="p">.</span><span class="n">config_from_object</span><span class="p">(</span><span class="s">"django.conf:settings"</span><span class="p">,</span> <span class="n">namespace</span><span class="o">=</span><span class="s">"CELERY"</span><span class="p">)</span> <span class="n">app</span><span class="p">.</span><span class="n">autodiscover_tasks</span><span class="p">()</span> </code></pre></div></div> <p>The next thing is to update <code class="language-plaintext highlighter-rouge">backend/backend/__init__.py</code> file:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">.celery</span> <span class="kn">import</span> <span class="n">app</span> <span class="k">as</span> <span class="n">celery_app</span> <span class="n">__all__</span> <span class="o">=</span> <span class="p">(</span><span class="s">"celery_app"</span><span class="p">,)</span> </code></pre></div></div> <p>We need to add configuration variables at the end of <code class="language-plaintext highlighter-rouge">backend/backend/settings.py</code>:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># rest of the code ... </span> <span class="c1"># celery broker and result </span><span class="n">CELERY_BROKER_URL</span> <span class="o">=</span> <span class="s">"redis://localhost:6379/0"</span> <span class="n">CELERY_RESULT_BACKEND</span> <span class="o">=</span> <span class="s">"redis://localhost:6379/0"</span> </code></pre></div></div> <h2 id="running-locally">Running locally</h2> <p>We need two terminals open to run the project locally. Please run the Django development server in the first terminal:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python manage.py runserver </code></pre></div></div> <p>In the second terminal, please start a Celery worker:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># please run in the backend directory (the same dir as runserver command)</span> celery <span class="nt">-A</span> backend worker <span class="nt">--loglevel</span><span class="o">=</span>info <span class="nt">--concurrency</span> 1 <span class="nt">-E</span> </code></pre></div></div> <p>Please open A web browser at <code class="language-plaintext highlighter-rouge">127.0.0.1:8000/assignments</code> and create a new <code class="language-plaintext highlighter-rouge">Assignment</code> object by clicking <code class="language-plaintext highlighter-rouge">POST</code> button. After object creation the <code class="language-plaintext highlighter-rouge">sum</code> should be <code class="language-plaintext highlighter-rouge">null</code>, like in the image below:</p> <p><img src="/tutorial/django-celery-redis-postgres-docker-compose/create-assignment.png" alt="" /></p> <p>Please click <code class="language-plaintext highlighter-rouge">GET</code> in the right upper corner, to get list of all assignments. You should see the <code class="language-plaintext highlighter-rouge">sum</code> value computed:</p> <p><img src="/tutorial/django-celery-redis-postgres-docker-compose/sum-computed.png" alt="" /></p> <p>It was computed in the background with Celery framework. We are using here super simple example, that is fast. In real world, the background task can take even hours to complete - depends on the project.</p> <h2 id="create-dockerfile">Create Dockerfile</h2> <p>It’s time to create a <code class="language-plaintext highlighter-rouge">Dockerfile</code> for server and worker. Server and worker will run in separate containers, but they will have the same <code class="language-plaintext highlighter-rouge">Dockerfile</code>. Please add new directories:</p> <ul> <li><code class="language-plaintext highlighter-rouge">docker</code></li> <li><code class="language-plaintext highlighter-rouge">docker/backend</code>.</li> </ul> <p>Please add a new file <code class="language-plaintext highlighter-rouge">Dockerfile</code> in <code class="language-plaintext highlighter-rouge">docker/backend</code>:</p> <figure class="highlight"><pre><code class="language-dockerfile" data-lang="dockerfile"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 </pre></td><td class="code"><pre><span class="k">FROM</span><span class="s"> python:3.8.15-alpine</span> <span class="k">RUN </span>apk update <span class="o">&amp;&amp;</span> apk add python3-dev gcc libc-dev <span class="k">WORKDIR</span><span class="s"> /app</span> <span class="k">RUN </span>pip <span class="nb">install</span> <span class="nt">--upgrade</span> pip <span class="k">RUN </span>pip <span class="nb">install </span>gunicorn <span class="k">ADD</span><span class="s"> ./requirements.txt /app/</span> <span class="k">RUN </span>pip <span class="nb">install</span> <span class="nt">-r</span> requirements.txt <span class="k">ADD</span><span class="s"> ./backend /app/backend</span> <span class="k">ADD</span><span class="s"> ./docker /app/docker</span> <span class="k">RUN </span><span class="nb">chmod</span> +x /app/docker/backend/server-entrypoint.sh <span class="k">RUN </span><span class="nb">chmod</span> +x /app/docker/backend/worker-entrypoint.sh </pre></td></tr></tbody></table></code></pre></figure> <ul> <li><strong>line 1</strong>: we are using <code class="language-plaintext highlighter-rouge">python:3.8.15-alpine</code> image as base.</li> <li><strong>line 3</strong>: we install <code class="language-plaintext highlighter-rouge">gcc</code> and python development libraries to install and build new packages.</li> <li><strong>line 5</strong>: we add <code class="language-plaintext highlighter-rouge">app</code> directory and set it as working directory.</li> <li><strong>line 6</strong>: copy <code class="language-plaintext highlighter-rouge">requirements.txt</code> file to image.</li> <li><strong>lines 8-10</strong>: update <code class="language-plaintext highlighter-rouge">pip</code>, install <code class="language-plaintext highlighter-rouge">gunicorn</code> and required packages.</li> <li><strong>lines 12-13</strong>: copy <code class="language-plaintext highlighter-rouge">backend</code> and <code class="language-plaintext highlighter-rouge">docker</code> directories to the docker image.</li> <li><strong>lines 15-16</strong>: add execution rights to entrypoint scripts.</li> </ul> <h2 id="entrypoint-scripts">Entrypoint scripts</h2> <p>I prefer to keep execution commands in separate scripts (they can be defined in <code class="language-plaintext highlighter-rouge">docker-compose</code>). Let’s add script that will start gunicorn server. Please add <code class="language-plaintext highlighter-rouge">server-entrypoint.sh</code> file in the <code class="language-plaintext highlighter-rouge">docker/backend</code> directory:</p> <figure class="highlight"><pre><code class="language-bash" data-lang="bash"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 </pre></td><td class="code"><pre><span class="c">#!/bin/sh</span> <span class="k">until </span><span class="nb">cd</span> /app/backend <span class="k">do </span><span class="nb">echo</span> <span class="s2">"Waiting for server volume..."</span> <span class="k">done until </span>python manage.py migrate <span class="k">do </span><span class="nb">echo</span> <span class="s2">"Waiting for db to be ready..."</span> <span class="nb">sleep </span>2 <span class="k">done </span>python manage.py collectstatic <span class="nt">--noinput</span> <span class="c"># python manage.py createsuperuser --noinput</span> gunicorn backend.wsgi <span class="nt">--bind</span> 0.0.0.0:8000 <span class="nt">--workers</span> 4 <span class="nt">--threads</span> 4 <span class="c"># for debug</span> <span class="c">#python manage.py runserver 0.0.0.0:8000</span> </pre></td></tr></tbody></table></code></pre></figure> <p>Explanations for <code class="language-plaintext highlighter-rouge">server-entrypoint.sh</code> script:</p> <ul> <li><strong>lines 3-6</strong>: we wait till <code class="language-plaintext highlighter-rouge">/app/backend</code> directory is ready.</li> <li><strong>lines 9-13</strong>: we wait till database is ready and run migration.</li> <li><strong>line 16</strong>: collect static files.</li> <li><strong>line 20</strong>: start gunicorn server.</li> <li><strong>line 23</strong>: I left the command to start development server. It is sometimes needed for debugging. When using development server please comment out line 20.</li> </ul> <p>The worker will have separate script in <code class="language-plaintext highlighter-rouge">worker-entrypoint.sh</code>:</p> <figure class="highlight"><pre><code class="language-bash" data-lang="bash"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1 2 3 4 5 6 7 8 9 </pre></td><td class="code"><pre><span class="c">#!/bin/sh</span> <span class="k">until </span><span class="nb">cd</span> /app/backend <span class="k">do </span><span class="nb">echo</span> <span class="s2">"Waiting for server volume..."</span> <span class="k">done</span> <span class="c"># run a worker :)</span> celery <span class="nt">-A</span> backend worker <span class="nt">--loglevel</span><span class="o">=</span>info <span class="nt">--concurrency</span> 1 <span class="nt">-E</span> </pre></td></tr></tbody></table></code></pre></figure> <p>The script just start a single worker. You can increase number of workers by changing <code class="language-plaintext highlighter-rouge">--concurrency</code> parameter.</p> <h2 id="nginx-configuration">Nginx configuration</h2> <p>We will use Nginx server to proxy requests to gunicron (Django) server and to serve static files. We will use default docker image (<code class="language-plaintext highlighter-rouge">nging:1.23-alpine</code>). We need to provide configuration file to customize server work. Please create a new directory <code class="language-plaintext highlighter-rouge">docker/nginx</code> and add there <code class="language-plaintext highlighter-rouge">default.conf</code> file there:</p> <figure class="highlight"><pre><code class="language-nginx" data-lang="nginx"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 </pre></td><td class="code"><pre><span class="k">server</span> <span class="p">{</span> <span class="kn">listen</span> <span class="mi">80</span><span class="p">;</span> <span class="kn">server_name</span> <span class="s">_</span><span class="p">;</span> <span class="kn">server_tokens</span> <span class="no">off</span><span class="p">;</span> <span class="kn">client_max_body_size</span> <span class="mi">20M</span><span class="p">;</span> <span class="kn">location</span> <span class="n">/</span> <span class="p">{</span> <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="s">@proxy_api</span><span class="p">;</span> <span class="p">}</span> <span class="kn">location</span> <span class="n">/admin</span> <span class="p">{</span> <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="s">@proxy_api</span><span class="p">;</span> <span class="p">}</span> <span class="kn">location</span> <span class="s">@proxy_api</span> <span class="p">{</span> <span class="kn">proxy_set_header</span> <span class="s">Host</span> <span class="nv">$http_host</span><span class="p">;</span> <span class="kn">proxy_redirect</span> <span class="no">off</span><span class="p">;</span> <span class="kn">proxy_pass</span> <span class="s">http://server:8000</span><span class="p">;</span> <span class="p">}</span> <span class="kn">location</span> <span class="n">/django_static/</span> <span class="p">{</span> <span class="kn">autoindex</span> <span class="no">on</span><span class="p">;</span> <span class="kn">alias</span> <span class="n">/app/backend/django_static/</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> </pre></td></tr></tbody></table></code></pre></figure> <p>The server will formard all <code class="language-plaintext highlighter-rouge">/</code> and <code class="language-plaintext highlighter-rouge">/admin</code> requests to the <code class="language-plaintext highlighter-rouge">@proxy_api</code>, which is our <code class="language-plaintext highlighter-rouge">gunicorn</code> serving Django. The <code class="language-plaintext highlighter-rouge">/django_static/</code> requests will be served from <code class="language-plaintext highlighter-rouge">/app/backend/django_static/</code> - it is a path where static files will be collected by Django.</p> <h2 id="create-docker-compose">Create <code class="language-plaintext highlighter-rouge">docker-compose</code></h2> <p>We have <code class="language-plaintext highlighter-rouge">Dockerfile</code> for server and worker ready. The Nginx configuration is waiting for requests. Let’s add a <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> to our project:</p> <figure class="highlight"><pre><code class="language-yml" data-lang="yml"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 </pre></td><td class="code"><pre><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">2'</span> <span class="na">services</span><span class="pi">:</span> <span class="na">nginx</span><span class="pi">:</span> <span class="na">restart</span><span class="pi">:</span> <span class="s">always</span> <span class="na">image</span><span class="pi">:</span> <span class="s">nginx:1.23-alpine</span> <span class="na">ports</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">80:80</span> <span class="na">volumes</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf</span> <span class="pi">-</span> <span class="s">static_volume:/app/backend/django_static</span> <span class="na">server</span><span class="pi">:</span> <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span> <span class="na">build</span><span class="pi">:</span> <span class="na">context</span><span class="pi">:</span> <span class="s">.</span> <span class="na">dockerfile</span><span class="pi">:</span> <span class="s">./docker/backend/Dockerfile</span> <span class="na">entrypoint</span><span class="pi">:</span> <span class="s">/app/docker/backend/server-entrypoint.sh</span> <span class="na">volumes</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">static_volume:/app/backend/django_static</span> <span class="na">expose</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">8000</span> <span class="na">environment</span><span class="pi">:</span> <span class="na">DEBUG</span><span class="pi">:</span> <span class="s2">"</span><span class="s">True"</span> <span class="na">CELERY_BROKER_URL</span><span class="pi">:</span> <span class="s2">"</span><span class="s">redis://redis:6379/0"</span> <span class="na">CELERY_RESULT_BACKEND</span><span class="pi">:</span> <span class="s2">"</span><span class="s">redis://redis:6379/0"</span> <span class="na">DJANGO_DB</span><span class="pi">:</span> <span class="s">postgresql</span> <span class="na">POSTGRES_HOST</span><span class="pi">:</span> <span class="s">db</span> <span class="na">POSTGRES_NAME</span><span class="pi">:</span> <span class="s">postgres</span> <span class="na">POSTGRES_USER</span><span class="pi">:</span> <span class="s">postgres</span> <span class="na">POSTGRES_PASSWORD</span><span class="pi">:</span> <span class="s">postgres</span> <span class="na">POSTGRES_PORT</span><span class="pi">:</span> <span class="m">5432</span> <span class="na">worker</span><span class="pi">:</span> <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span> <span class="na">build</span><span class="pi">:</span> <span class="na">context</span><span class="pi">:</span> <span class="s">.</span> <span class="na">dockerfile</span><span class="pi">:</span> <span class="s">./docker/backend/Dockerfile</span> <span class="na">entrypoint</span><span class="pi">:</span> <span class="s">/app/docker/backend/worker-entrypoint.sh</span> <span class="na">volumes</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">static_volume:/app/backend/django_static</span> <span class="na">environment</span><span class="pi">:</span> <span class="na">DEBUG</span><span class="pi">:</span> <span class="s2">"</span><span class="s">True"</span> <span class="na">CELERY_BROKER_URL</span><span class="pi">:</span> <span class="s2">"</span><span class="s">redis://redis:6379/0"</span> <span class="na">CELERY_RESULT_BACKEND</span><span class="pi">:</span> <span class="s2">"</span><span class="s">redis://redis:6379/0"</span> <span class="na">DJANGO_DB</span><span class="pi">:</span> <span class="s">postgresql</span> <span class="na">POSTGRES_HOST</span><span class="pi">:</span> <span class="s">db</span> <span class="na">POSTGRES_NAME</span><span class="pi">:</span> <span class="s">postgres</span> <span class="na">POSTGRES_USER</span><span class="pi">:</span> <span class="s">postgres</span> <span class="na">POSTGRES_PASSWORD</span><span class="pi">:</span> <span class="s">postgres</span> <span class="na">POSTGRES_PORT</span><span class="pi">:</span> <span class="m">5432</span> <span class="na">depends_on</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">server</span> <span class="pi">-</span> <span class="s">redis</span> <span class="na">redis</span><span class="pi">:</span> <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span> <span class="na">image</span><span class="pi">:</span> <span class="s">redis:7.0.5-alpine</span> <span class="na">expose</span><span class="pi">:</span> <span class="pi">-</span> <span class="m">6379</span> <span class="na">db</span><span class="pi">:</span> <span class="na">image</span><span class="pi">:</span> <span class="s">postgres:13.0-alpine</span> <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span> <span class="na">volumes</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">postgres_data:/var/lib/postgresql/data/</span> <span class="na">environment</span><span class="pi">:</span> <span class="na">POSTGRES_DB</span><span class="pi">:</span> <span class="s">postgres</span> <span class="na">POSTGRES_USER</span><span class="pi">:</span> <span class="s">postgres</span> <span class="na">POSTGRES_PASSWORD</span><span class="pi">:</span> <span class="s">postgres</span> <span class="na">expose</span><span class="pi">:</span> <span class="pi">-</span> <span class="m">5432</span> <span class="na">volumes</span><span class="pi">:</span> <span class="na">static_volume</span><span class="pi">:</span> <span class="pi">{}</span> <span class="na">postgres_data</span><span class="pi">:</span> <span class="pi">{}</span> </pre></td></tr></tbody></table></code></pre></figure> <ul> <li><strong>lines 4-11</strong> - it is a definition of <code class="language-plaintext highlighter-rouge">nginx</code> container. It is using standard <code class="language-plaintext highlighter-rouge">nginx:1.23-alpine</code> image. We copy configuration file in <strong>line 10</strong>. We add <code class="language-plaintext highlighter-rouge">static_volume</code> at <strong>line 11</strong>. The same <code class="language-plaintext highlighter-rouge">static_volume</code> will be mounted for <code class="language-plaintext highlighter-rouge">server</code> container. All static files will go there. There is port <code class="language-plaintext highlighter-rouge">80</code> available outside the docker, it can be reached from external world.</li> <li><strong>lines 12-31</strong> - it is our Django server. It is created using <code class="language-plaintext highlighter-rouge">/docker/backend/Dockerfile</code> (<strong>line 16</strong>). It has <code class="language-plaintext highlighter-rouge">static_volume</code> mounted at line <strong>line 19</strong>. It exposes port <code class="language-plaintext highlighter-rouge">8000</code> - it can be reached only <em>internally</em> in <code class="language-plaintext highlighter-rouge">docker-compose</code>. We have envrionment variables set in <strong>lines 22-31</strong>. The container will execute <code class="language-plaintext highlighter-rouge">server-netrypoint.sh</code> script (<strong>line 17</strong>).</li> <li><strong>lines 32-52</strong> - it is a worker container. It doesn’t expose any ports. It depends on <code class="language-plaintext highlighter-rouge">redis</code> and <code class="language-plaintext highlighter-rouge">server</code> containers (<strong>lines 50-52</strong>).</li> <li><strong>lines 53-57</strong> - it is a Redis container. It exposes internally port <code class="language-plaintext highlighter-rouge">6379</code> (can’t be reached from outside of <code class="language-plaintext highlighter-rouge">docker-compose</code>).</li> <li><strong>lines 58-68</strong> - it is Postgres database. It exposes internally port <code class="language-plaintext highlighter-rouge">5432</code>. It has environment variables in <strong>lines 63-66</strong> to initialize database at start. All database files are stored in <code class="language-plaintext highlighter-rouge">postgres_data</code> volume.</li> <li><strong>lines 70-72</strong> - definition of volumes used in the <code class="language-plaintext highlighter-rouge">docker-compose</code>.</li> </ul> <p>We are almost ready to start using the <code class="language-plaintext highlighter-rouge">docker-compose</code>. We need to update Django configuration to read envrionment variables. Please edit <code class="language-plaintext highlighter-rouge">backend/backend/settings.py</code> file:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># new import </span><span class="kn">import</span> <span class="nn">os</span> <span class="c1"># # rest of the code ... # </span> <span class="c1"># set variables </span><span class="n">SECRET_KEY</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">get</span><span class="p">(</span> <span class="s">"SECRET_KEY"</span><span class="p">,</span> <span class="s">"django-insecure-6hdy-)5o6k6it_6x%s#u0#guc3(au!=v%%qb674(upu6rrht7b"</span> <span class="p">)</span> <span class="n">DEBUG</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"DEBUG"</span><span class="p">,</span> <span class="bp">True</span><span class="p">)</span> <span class="n">ALLOWED_HOSTS</span> <span class="o">=</span> <span class="p">[</span><span class="s">"127.0.0.1"</span><span class="p">,</span> <span class="s">"0.0.0.0"</span><span class="p">]</span> <span class="k">if</span> <span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"ALLOWED_HOSTS"</span><span class="p">)</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span> <span class="k">try</span><span class="p">:</span> <span class="n">ALLOWED_HOSTS</span> <span class="o">+=</span> <span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"ALLOWED_HOSTS"</span><span class="p">).</span><span class="n">split</span><span class="p">(</span><span class="s">","</span><span class="p">)</span> <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span> <span class="k">print</span><span class="p">(</span><span class="s">"Cant set ALLOWED_HOSTS, using default instead"</span><span class="p">)</span> <span class="c1"># # rest of the code ... # </span> <span class="c1"># set database, it can be set to SQLite or Postgres </span> <span class="n">DB_SQLITE</span> <span class="o">=</span> <span class="s">"sqlite"</span> <span class="n">DB_POSTGRESQL</span> <span class="o">=</span> <span class="s">"postgresql"</span> <span class="n">DATABASES_ALL</span> <span class="o">=</span> <span class="p">{</span> <span class="n">DB_SQLITE</span><span class="p">:</span> <span class="p">{</span> <span class="s">"ENGINE"</span><span class="p">:</span> <span class="s">"django.db.backends.sqlite3"</span><span class="p">,</span> <span class="s">"NAME"</span><span class="p">:</span> <span class="n">BASE_DIR</span> <span class="o">/</span> <span class="s">"db.sqlite3"</span><span class="p">,</span> <span class="p">},</span> <span class="n">DB_POSTGRESQL</span><span class="p">:</span> <span class="p">{</span> <span class="s">"ENGINE"</span><span class="p">:</span> <span class="s">"django.db.backends.postgresql"</span><span class="p">,</span> <span class="s">"HOST"</span><span class="p">:</span> <span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"POSTGRES_HOST"</span><span class="p">,</span> <span class="s">"localhost"</span><span class="p">),</span> <span class="s">"NAME"</span><span class="p">:</span> <span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"POSTGRES_NAME"</span><span class="p">,</span> <span class="s">"postgres"</span><span class="p">),</span> <span class="s">"USER"</span><span class="p">:</span> <span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"POSTGRES_USER"</span><span class="p">,</span> <span class="s">"postgres"</span><span class="p">),</span> <span class="s">"PASSWORD"</span><span class="p">:</span> <span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"POSTGRES_PASSWORD"</span><span class="p">,</span> <span class="s">"postgres"</span><span class="p">),</span> <span class="s">"PORT"</span><span class="p">:</span> <span class="nb">int</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"POSTGRES_PORT"</span><span class="p">,</span> <span class="s">"5432"</span><span class="p">)),</span> <span class="p">},</span> <span class="p">}</span> <span class="n">DATABASES</span> <span class="o">=</span> <span class="p">{</span><span class="s">"default"</span><span class="p">:</span> <span class="n">DATABASES_ALL</span><span class="p">[</span><span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"DJANGO_DB"</span><span class="p">,</span> <span class="n">DB_SQLITE</span><span class="p">)]}</span> <span class="c1"># # rest of the code ... # </span> <span class="c1"># set static URL address and path where to store static files </span><span class="n">STATIC_URL</span> <span class="o">=</span> <span class="s">"/django_static/"</span> <span class="n">STATIC_ROOT</span> <span class="o">=</span> <span class="n">BASE_DIR</span> <span class="o">/</span> <span class="s">"django_static"</span> <span class="c1"># # rest of the code ... # </span> <span class="c1"># celery broker and result </span><span class="n">CELERY_BROKER_URL</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"BROKER_URL"</span><span class="p">,</span> <span class="s">"redis://localhost:6379/0"</span><span class="p">)</span> <span class="n">CELERY_RESULT_BACKEND</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"RESULT_BACKEND"</span><span class="p">,</span> <span class="s">"redis://localhost:6379/0"</span><span class="p">)</span> </code></pre></div></div> <p>The project structure at this stage should look like:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>├── backend │   ├── assignments │   │   ├── admin.py │   │   ├── apps.py │   │   ├── __init__.py │   │   ├── migrations │   │   │   ├── 0001_initial.py │   │   │   └── __init__.py │   │   ├── models.py │   │   ├── serializers.py │   │   ├── tasks.py │   │   ├── tests.py │   │   ├── urls.py │   │   └── views.py │   ├── backend │   │   ├── asgi.py │   │   ├── celery.py │   │   ├── __init__.py │   │   ├── settings.py │   │   ├── urls.py │   │   └── wsgi.py │   ├── db.sqlite3 │   └── manage.py ├── docker │   ├── backend │   │   ├── Dockerfile │   │   ├── server-entrypoint.sh │   │   └── worker-entrypoint.sh │   └── nginx │   └── default.conf ├── docker-compose.yml ├── LICENSE ├── README.md └── requirements.txt </code></pre></div></div> <h2 id="docker-compose-commands"><code class="language-plaintext highlighter-rouge">docker-compose</code> commands</h2> <p>We are ready to build our <code class="language-plaintext highlighter-rouge">docker-compose</code>:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># run in main project directory</span> <span class="nb">sudo </span>docker-compose build </code></pre></div></div> <p>After build you can run all containers with:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>docker-compose up </code></pre></div></div> <p>I’m using <code class="language-plaintext highlighter-rouge">Ctrl+C</code> to stop containers.</p> <p>When deploying to the production I’m using:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>docker-compose up <span class="nt">--build</span> <span class="nt">-d</span> </code></pre></div></div> <p>The above one command is building all contaners and running them in detached mode. I can close the SSH connection to the VPS machine and the service will run. The command to stop the <code class="language-plaintext highlighter-rouge">docker-compose</code>:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>docker-compose down </code></pre></div></div> <p>One more command that is useful. It can be used to login to the running container:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>docker <span class="nb">exec</span> <span class="nt">-it</span> &lt;container_name&gt; bash </code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">docker-compose</code> is running at <code class="language-plaintext highlighter-rouge">0.0.0.0</code>. Just enter this address in your web broswer to play with your web application.</p> <h2 id="summary">Summary</h2> <p>We created a simple web application with Django and Celery. We used Postgres database and Redis to have connectivity between Django and Celery. The project was put to docker containers thanks to <code class="language-plaintext highlighter-rouge">docker-compose</code>. All code is available at our <a href="https://github.com/saasitive/docker-compose-django-celery-redis-postgres">GitHub repository</a>. Please create GitHub issue there if you have problems or need help. We will try to help you!</p> <p>You have a lot information from today article. You can start deploying your own application.</p> <p>If you are looking for more advanced topics (for example, deploying with Let’s encrypt) please subscribe to our <a href="https://forms.gle/rgAG9gkhUEH2wUVt5">newsletter</a> and check our <a href="https://saasitive.com">React and Django course</a> on how to build SaaS web application from scratch.</p>Aleksandra Płońska, Piotr PłońskiDeploying Django application that is using Celery and Redis might be challenging. Thanks to docker-compose tool we can prepare docker containers locally and make deployment much easier. I would like to show you my approach for constructing docker-compose configuration that can be reused in other web applications. I will be using Django version 4.1.3 and Celery 5.2.7. The nginx server will be used as reverse proxy for Django and to serve static files. Postgres database and Redis will be in the docker-compose.Dynamically update periodic tasks in Celery and Django2022-10-17T00:00:00+00:002022-10-17T00:00:00+00:00https://saasitive.com/tutorial/dynamically-update-periodic-tasks-celery<p><img src="/tutorial/dynamically-update-periodic-tasks-celery/banner.jpg" alt="Dynamically Update Periodic tasks in Celery banner" /><a href="https://github.com/celery/celery"><strong>Celery</strong></a> is a popular distributed tasks queue. It is often used with <a href="https://www.djangoproject.com/"><strong>Django</strong></a> framework to process heavy computational tasks in the background. You can add a single task to the queue or define periodic tasks. Periodic tasks are automatically scheduled for execution based on set time. The most common approach is to define the periodic tasks before the Celery worker is started. For example, it can be daily cleaning of the database. What if we would like to define periodic tasks dynamically? Recently, I’ve been working on a web app for <a href="https://monitor-uptime.com"><strong>uptime monitoring</strong></a>. The service continuously monitors a server and send email notification when the server is down. In the web app, the user can add a server address to be monitored and select the interval between requests to the server. How to dynamically add periodic tasks in Celery? I want to describe my approach in this article.</p> <h2 id="overview">Overview</h2> <p>The uptime monitoring idea is simple. The user defines the server address, for example <code class="language-plaintext highlighter-rouge">https://github.com</code> and interval. Every time period, a request is made to the server. We store in the database the information about response time and status. There can be monitored multiple servers, each at a different interval. Moreover, the user can add or delete monitored addresses at any time. Here is a sequence diagram of my approach.</p> <p><img src="/tutorial/dynamically-update-periodic-tasks-celery/sequence-diagram.png" alt="" /></p> <h2 id="start-django-project">Start Django project</h2> <p>In this article, I will create a Django application with simple monitoring functionality to show you my approach for dynamic periodic tasks in Celery and Django. All code for this article is available in public <a href="https://github.com/saasitive/dynamically-update-periodic-tasks">GitHub repository</a>.</p> <p>Please set up a new Python virtual environment:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>virtualenv dynenv --python=python3.8 source dynenv/bin/activate </code></pre></div></div> <p>We need to install the required packages:</p> <ul> <li><code class="language-plaintext highlighter-rouge">django</code> for web app development,</li> <li><code class="language-plaintext highlighter-rouge">djangorestframework</code>, <code class="language-plaintext highlighter-rouge">markdown</code>, <code class="language-plaintext highlighter-rouge">django-filter</code> for Django Rest Framework,</li> <li><code class="language-plaintext highlighter-rouge">celery</code>, <code class="language-plaintext highlighter-rouge">django-celery-beat</code>, <code class="language-plaintext highlighter-rouge">gevent</code>, <code class="language-plaintext highlighter-rouge">sqlalchemy</code> for background tasks processing,</li> <li><code class="language-plaintext highlighter-rouge">requests</code> to make HTTP requests.</li> </ul> <p>Please install the following packages:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip install django djangorestframework markdown django-filter celery django-celery-beat gevent sqlalchemy requests </code></pre></div></div> <blockquote> <p>It is a good practice to add required packages into the <code class="language-plaintext highlighter-rouge">requirements.txt</code> file.</p> </blockquote> <p>We need to initialize the Django project with the <code class="language-plaintext highlighter-rouge">django-admin</code> command line tool:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>django-admin startproject backend </code></pre></div></div> <p>Please change the directory to the <code class="language-plaintext highlighter-rouge">backend</code> and create a new app:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python manage.py startup monitors </code></pre></div></div> <p>We need to setup the Django to use a newly generated <code class="language-plaintext highlighter-rouge">monitors</code> app. Please update the <code class="language-plaintext highlighter-rouge">INSTALLED_APPS</code> in the <code class="language-plaintext highlighter-rouge">backend/settings.py</code> file:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># the rest of the code ... </span> <span class="n">INSTALLED_APPS</span> <span class="o">=</span> <span class="p">[</span> <span class="s">"django.contrib.admin"</span><span class="p">,</span> <span class="s">"django.contrib.auth"</span><span class="p">,</span> <span class="s">"django.contrib.contenttypes"</span><span class="p">,</span> <span class="s">"django.contrib.sessions"</span><span class="p">,</span> <span class="s">"django.contrib.messages"</span><span class="p">,</span> <span class="s">"django.contrib.staticfiles"</span><span class="p">,</span> <span class="c1"># 3rd party </span> <span class="s">"rest_framework"</span><span class="p">,</span> <span class="s">"django_celery_beat"</span><span class="p">,</span> <span class="c1"># apps </span> <span class="s">"monitors"</span><span class="p">,</span> <span class="p">]</span> <span class="c1"># the rest of the code ... </span></code></pre></div></div> <p>We added to the <code class="language-plaintext highlighter-rouge">INSTALLED_APPS</code>:</p> <ul> <li><code class="language-plaintext highlighter-rouge">rest_framework</code> - package for faster REST API development,</li> <li><code class="language-plaintext highlighter-rouge">django_celery_beat</code> - package that provides <code class="language-plaintext highlighter-rouge">PeriodicTask</code> model,</li> <li><code class="language-plaintext highlighter-rouge">monitors</code> - our new package.</li> </ul> <h2 id="monitor-database-model">Monitor database model</h2> <p>We will need two database models:</p> <ul> <li><code class="language-plaintext highlighter-rouge">Monitor</code> - a model for storing information about the monitored address and time interval between checks,</li> <li><code class="language-plaintext highlighter-rouge">MonitorRequest</code> - model to keep response time and status.</li> </ul> <p>The <code class="language-plaintext highlighter-rouge">backend/monitors/models.py</code> file content:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span> <span class="kn">from</span> <span class="nn">django_celery_beat.models</span> <span class="kn">import</span> <span class="n">PeriodicTask</span> <span class="k">class</span> <span class="nc">Monitor</span><span class="p">(</span><span class="n">models</span><span class="p">.</span><span class="n">Model</span><span class="p">):</span> <span class="c1"># monitored endpoint </span> <span class="n">endpoint</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">1024</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span> <span class="c1"># interval in seconds </span> <span class="c1"># enpoint will be checked every specified interval time period </span> <span class="n">interval</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="n">IntegerField</span><span class="p">(</span><span class="n">blank</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span> <span class="n">task</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="n">OneToOneField</span><span class="p">(</span> <span class="n">PeriodicTask</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">on_delete</span><span class="o">=</span><span class="n">models</span><span class="p">.</span><span class="n">SET_NULL</span> <span class="p">)</span> <span class="n">created_at</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="n">DateTimeField</span><span class="p">(</span><span class="n">auto_now_add</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="k">class</span> <span class="nc">MonitorRequest</span><span class="p">(</span><span class="n">models</span><span class="p">.</span><span class="n">Model</span><span class="p">):</span> <span class="c1"># endpoint response time in miliseconds </span> <span class="n">response_time</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="n">IntegerField</span><span class="p">(</span><span class="n">blank</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span> <span class="n">response_status</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="n">IntegerField</span><span class="p">(</span><span class="n">blank</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span> <span class="n">monitor</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="n">Monitor</span><span class="p">,</span> <span class="n">on_delete</span><span class="o">=</span><span class="n">models</span><span class="p">.</span><span class="n">CASCADE</span><span class="p">)</span> <span class="n">created_at</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="n">DateTimeField</span><span class="p">(</span><span class="n">auto_now_add</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> </code></pre></div></div> <p>Please notice that <code class="language-plaintext highlighter-rouge">Monitor</code> has one-to-one relationship with <code class="language-plaintext highlighter-rouge">PeriodicTask</code>.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">task</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="n">OneToOneField</span><span class="p">(</span> <span class="n">PeriodicTask</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">on_delete</span><span class="o">=</span><span class="n">models</span><span class="p">.</span><span class="n">SET_NULL</span> <span class="p">)</span> </code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">PeriodicTask</code> will be used to inform Celery about task execution and its frequency.</p> <p>When the end-user adds a new server address for monitoring the <code class="language-plaintext highlighter-rouge">Monitor</code> object will be created in the database. The server address will be stored in the <code class="language-plaintext highlighter-rouge">endpoint</code> field. The <code class="language-plaintext highlighter-rouge">interval</code> field is in seconds.</p> <p>Each request to the server will be saved in the database as a <code class="language-plaintext highlighter-rouge">MonitorRequest</code> object. We will store response time (in milliseconds) and status.</p> <p>We will need to add serializers, views and URLs to have REST API available for <code class="language-plaintext highlighter-rouge">Monitor</code> and <code class="language-plaintext highlighter-rouge">MonitorRequest</code>.</p> <p>Please add a new file <code class="language-plaintext highlighter-rouge">serializers.py</code> in the <code class="language-plaintext highlighter-rouge">backend/monitors</code> directory:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">rest_framework</span> <span class="kn">import</span> <span class="n">serializers</span> <span class="kn">from</span> <span class="nn">monitors.models</span> <span class="kn">import</span> <span class="n">Monitor</span><span class="p">,</span> <span class="n">MonitorRequest</span> <span class="k">class</span> <span class="nc">MonitorSerializer</span><span class="p">(</span><span class="n">serializers</span><span class="p">.</span><span class="n">ModelSerializer</span><span class="p">):</span> <span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span> <span class="n">model</span> <span class="o">=</span> <span class="n">Monitor</span> <span class="n">read_only_fields</span> <span class="o">=</span> <span class="p">(</span><span class="s">"id"</span><span class="p">,</span> <span class="s">"created_at"</span><span class="p">)</span> <span class="n">fields</span> <span class="o">=</span> <span class="p">(</span> <span class="s">"id"</span><span class="p">,</span> <span class="s">"created_at"</span><span class="p">,</span> <span class="s">"endpoint"</span><span class="p">,</span> <span class="s">"interval"</span><span class="p">,</span> <span class="p">)</span> <span class="k">class</span> <span class="nc">MonitorRequestSerializer</span><span class="p">(</span><span class="n">serializers</span><span class="p">.</span><span class="n">ModelSerializer</span><span class="p">):</span> <span class="n">monitor_endpoint</span> <span class="o">=</span> <span class="n">serializers</span><span class="p">.</span><span class="n">SerializerMethodField</span><span class="p">()</span> <span class="k">def</span> <span class="nf">get_monitor_endpoint</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">obj</span><span class="p">):</span> <span class="k">return</span> <span class="n">obj</span><span class="p">.</span><span class="n">monitor</span><span class="p">.</span><span class="n">endpoint</span> <span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span> <span class="n">model</span> <span class="o">=</span> <span class="n">MonitorRequest</span> <span class="n">read_only_fields</span> <span class="o">=</span> <span class="p">(</span><span class="s">"id"</span><span class="p">,</span> <span class="s">"created_at"</span><span class="p">)</span> <span class="n">fields</span> <span class="o">=</span> <span class="p">(</span> <span class="s">"id"</span><span class="p">,</span> <span class="s">"created_at"</span><span class="p">,</span> <span class="s">"response_time"</span><span class="p">,</span> <span class="s">"response_status"</span><span class="p">,</span> <span class="s">"monitor_endpoint"</span><span class="p">,</span> <span class="p">)</span> </code></pre></div></div> <p>The serializers will just return available fields. The next step is to edit <code class="language-plaintext highlighter-rouge">backend/monitors/views.py</code>:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">json</span> <span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">transaction</span> <span class="kn">from</span> <span class="nn">django.shortcuts</span> <span class="kn">import</span> <span class="n">render</span> <span class="kn">from</span> <span class="nn">django_celery_beat.models</span> <span class="kn">import</span> <span class="n">IntervalSchedule</span><span class="p">,</span> <span class="n">PeriodicTask</span> <span class="kn">from</span> <span class="nn">rest_framework</span> <span class="kn">import</span> <span class="n">viewsets</span> <span class="kn">from</span> <span class="nn">rest_framework.exceptions</span> <span class="kn">import</span> <span class="n">APIException</span> <span class="kn">from</span> <span class="nn">monitors.models</span> <span class="kn">import</span> <span class="n">Monitor</span><span class="p">,</span> <span class="n">MonitorRequest</span> <span class="kn">from</span> <span class="nn">monitors.serializers</span> <span class="kn">import</span> <span class="n">MonitorRequestSerializer</span><span class="p">,</span> <span class="n">MonitorSerializer</span> <span class="k">class</span> <span class="nc">MonitorViewSet</span><span class="p">(</span><span class="n">viewsets</span><span class="p">.</span><span class="n">ModelViewSet</span><span class="p">):</span> <span class="n">serializer_class</span> <span class="o">=</span> <span class="n">MonitorSerializer</span> <span class="n">queryset</span> <span class="o">=</span> <span class="n">Monitor</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="nb">all</span><span class="p">()</span> <span class="k">def</span> <span class="nf">perform_create</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">serializer</span><span class="p">):</span> <span class="k">try</span><span class="p">:</span> <span class="k">with</span> <span class="n">transaction</span><span class="p">.</span><span class="n">atomic</span><span class="p">():</span> <span class="n">instance</span> <span class="o">=</span> <span class="n">serializer</span><span class="p">.</span><span class="n">save</span><span class="p">()</span> <span class="n">schedule</span><span class="p">,</span> <span class="n">created</span> <span class="o">=</span> <span class="n">IntervalSchedule</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="n">get_or_create</span><span class="p">(</span> <span class="n">every</span><span class="o">=</span><span class="n">instance</span><span class="p">.</span><span class="n">interval</span><span class="p">,</span> <span class="n">period</span><span class="o">=</span><span class="n">IntervalSchedule</span><span class="p">.</span><span class="n">SECONDS</span><span class="p">,</span> <span class="p">)</span> <span class="n">task</span> <span class="o">=</span> <span class="n">PeriodicTask</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="n">create</span><span class="p">(</span> <span class="n">interval</span><span class="o">=</span><span class="n">schedule</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s">f"Monitor: </span><span class="si">{</span><span class="n">instance</span><span class="p">.</span><span class="n">endpoint</span><span class="si">}</span><span class="s">"</span><span class="p">,</span> <span class="n">task</span><span class="o">=</span><span class="s">"monitors.tasks.task_monitor"</span><span class="p">,</span> <span class="n">kwargs</span><span class="o">=</span><span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span> <span class="p">{</span> <span class="s">"monitor_id"</span><span class="p">:</span> <span class="n">instance</span><span class="p">.</span><span class="nb">id</span><span class="p">,</span> <span class="p">}</span> <span class="p">),</span> <span class="p">)</span> <span class="n">instance</span><span class="p">.</span><span class="n">task</span> <span class="o">=</span> <span class="n">task</span> <span class="n">instance</span><span class="p">.</span><span class="n">save</span><span class="p">()</span> <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span> <span class="k">raise</span> <span class="n">APIException</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">e</span><span class="p">))</span> <span class="k">def</span> <span class="nf">perform_destroy</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">instance</span><span class="p">):</span> <span class="k">if</span> <span class="n">instance</span><span class="p">.</span><span class="n">task</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span> <span class="n">instance</span><span class="p">.</span><span class="n">task</span><span class="p">.</span><span class="n">delete</span><span class="p">()</span> <span class="k">return</span> <span class="nb">super</span><span class="p">().</span><span class="n">perform_destroy</span><span class="p">(</span><span class="n">instance</span><span class="p">)</span> <span class="k">class</span> <span class="nc">MonitorRequestViewSet</span><span class="p">(</span><span class="n">viewsets</span><span class="p">.</span><span class="n">ModelViewSet</span><span class="p">):</span> <span class="n">serializer_class</span> <span class="o">=</span> <span class="n">MonitorRequestSerializer</span> <span class="n">queryset</span> <span class="o">=</span> <span class="n">MonitorRequest</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="nb">all</span><span class="p">()</span> </code></pre></div></div> <p>We have two views. The <code class="language-plaintext highlighter-rouge">MonitorRequestViewSet</code> derives from <code class="language-plaintext highlighter-rouge">ModelViewSet</code> and doesn’t overwrite any functions. It is simple CRUD for <code class="language-plaintext highlighter-rouge">MonitorRequest</code> objects.</p> <p>The <code class="language-plaintext highlighter-rouge">MonitorViewSet</code> overwrites <code class="language-plaintext highlighter-rouge">perform_create(self, serializer)</code> and <code class="language-plaintext highlighter-rouge">perform_destroy(self, instance)</code>. During monitor creation, a <code class="language-plaintext highlighter-rouge">PeriodicTask</code> instance is created. The <code class="language-plaintext highlighter-rouge">PeriodicTask</code> instance requires an <code class="language-plaintext highlighter-rouge">IntervalSchedule</code> object. The <code class="language-plaintext highlighter-rouge">IntervalSchedule</code> defines the time period between every task execution.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">with</span> <span class="n">transaction</span><span class="p">.</span><span class="n">atomic</span><span class="p">():</span> <span class="n">instance</span> <span class="o">=</span> <span class="n">serializer</span><span class="p">.</span><span class="n">save</span><span class="p">()</span> <span class="c1"># create `IntervalSchedule` obejct </span> <span class="n">schedule</span><span class="p">,</span> <span class="n">created</span> <span class="o">=</span> <span class="n">IntervalSchedule</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="n">get_or_create</span><span class="p">(</span> <span class="n">every</span><span class="o">=</span><span class="n">instance</span><span class="p">.</span><span class="n">interval</span><span class="p">,</span> <span class="n">period</span><span class="o">=</span><span class="n">IntervalSchedule</span><span class="p">.</span><span class="n">SECONDS</span><span class="p">,</span> <span class="p">)</span> <span class="c1"># create `PeriodicTask` object </span> <span class="n">task</span> <span class="o">=</span> <span class="n">PeriodicTask</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="n">create</span><span class="p">(</span> <span class="n">interval</span><span class="o">=</span><span class="n">schedule</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s">f"Monitor: </span><span class="si">{</span><span class="n">instance</span><span class="p">.</span><span class="n">endpoint</span><span class="si">}</span><span class="s">"</span><span class="p">,</span> <span class="n">task</span><span class="o">=</span><span class="s">"monitors.tasks.task_monitor"</span><span class="p">,</span> <span class="n">kwargs</span><span class="o">=</span><span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span> <span class="p">{</span> <span class="s">"monitor_id"</span><span class="p">:</span> <span class="n">instance</span><span class="p">.</span><span class="nb">id</span><span class="p">,</span> <span class="p">}</span> <span class="p">),</span> <span class="p">)</span> <span class="c1"># save task in monitor object </span> <span class="n">instance</span><span class="p">.</span><span class="n">task</span> <span class="o">=</span> <span class="n">task</span> <span class="n">instance</span><span class="p">.</span><span class="n">save</span><span class="p">()</span> </code></pre></div></div> <p>During the <code class="language-plaintext highlighter-rouge">PeriodicTask</code> object creation, we pass the task function signature <code class="language-plaintext highlighter-rouge">monitors.tasks.task_monitor</code> and define <code class="language-plaintext highlighter-rouge">kwargs</code>. We will implement the <code class="language-plaintext highlighter-rouge">task_monitor</code> in a moment.</p> <p>The last step is to define <code class="language-plaintext highlighter-rouge">urls.py</code> in the <code class="language-plaintext highlighter-rouge">backend/monitors</code>:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kn">from</span> <span class="nn">django.urls</span> <span class="kn">import</span> <span class="n">re_path</span> <span class="kn">from</span> <span class="nn">rest_framework.routers</span> <span class="kn">import</span> <span class="n">DefaultRouter</span> <span class="kn">from</span> <span class="nn">monitors.views</span> <span class="kn">import</span> <span class="n">MonitorRequestViewSet</span><span class="p">,</span> <span class="n">MonitorViewSet</span> <span class="n">router</span> <span class="o">=</span> <span class="n">DefaultRouter</span><span class="p">()</span> <span class="n">router</span><span class="p">.</span><span class="n">register</span><span class="p">(</span><span class="s">r"monitors"</span><span class="p">,</span> <span class="n">MonitorViewSet</span><span class="p">)</span> <span class="n">router</span><span class="p">.</span><span class="n">register</span><span class="p">(</span><span class="s">r"requests"</span><span class="p">,</span> <span class="n">MonitorRequestViewSet</span><span class="p">)</span> <span class="n">monitors_urlpatterns</span> <span class="o">=</span> <span class="n">router</span><span class="p">.</span><span class="n">urls</span> </code></pre></div></div> <p>We used <code class="language-plaintext highlighter-rouge">DefaultRouter</code> from Django Rest Framework. A basic viewer for REST API for <code class="language-plaintext highlighter-rouge">monitors</code> and <code class="language-plaintext highlighter-rouge">requests</code> will be generated by DRF.</p> <p>We need to add <code class="language-plaintext highlighter-rouge">monitors_urlpatterns</code> in the <code class="language-plaintext highlighter-rouge">backend/backend/urls.py</code> to make them available in the Django application.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">django.contrib</span> <span class="kn">import</span> <span class="n">admin</span> <span class="kn">from</span> <span class="nn">django.urls</span> <span class="kn">import</span> <span class="n">path</span> <span class="kn">from</span> <span class="nn">monitors.urls</span> <span class="kn">import</span> <span class="n">monitors_urlpatterns</span> <span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span> <span class="n">path</span><span class="p">(</span><span class="s">"admin/"</span><span class="p">,</span> <span class="n">admin</span><span class="p">.</span><span class="n">site</span><span class="p">.</span><span class="n">urls</span><span class="p">),</span> <span class="p">]</span> <span class="n">urlpatterns</span> <span class="o">+=</span> <span class="n">monitors_urlpatterns</span> </code></pre></div></div> <p>Please make migrations and apply them:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># please run in the backend directory</span> python manage.py makemigrations python manage.py migrate </code></pre></div></div> <h2 id="celery-configuration">Celery configuration</h2> <p>We need to configure the Celery framework. Please add a new file <code class="language-plaintext highlighter-rouge">celery.py</code> in the <code class="language-plaintext highlighter-rouge">backend/backend</code> directory:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">os</span> <span class="kn">import</span> <span class="nn">sys</span> <span class="kn">from</span> <span class="nn">celery</span> <span class="kn">import</span> <span class="n">Celery</span> <span class="n">CURRENT_DIR</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">dirname</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">dirname</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">abspath</span><span class="p">(</span><span class="n">__file__</span><span class="p">)))</span> <span class="n">sys</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">insert</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">CURRENT_DIR</span><span class="p">)</span> <span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">setdefault</span><span class="p">(</span><span class="s">"DJANGO_SETTINGS_MODULE"</span><span class="p">,</span> <span class="s">"backend.settings"</span><span class="p">)</span> <span class="n">app</span> <span class="o">=</span> <span class="n">Celery</span><span class="p">(</span><span class="s">"backend"</span><span class="p">)</span> <span class="n">app</span><span class="p">.</span><span class="n">config_from_object</span><span class="p">(</span><span class="s">"django.conf:settings"</span><span class="p">,</span> <span class="n">namespace</span><span class="o">=</span><span class="s">"CELERY"</span><span class="p">)</span> <span class="n">app</span><span class="p">.</span><span class="n">autodiscover_tasks</span><span class="p">()</span> </code></pre></div></div> <p>In the <code class="language-plaintext highlighter-rouge">backend/backend/settings.py</code> please add the Celery configuration:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c1"># the rest of the code ... </span> <span class="c1"># celery broker and results in sqlite </span><span class="n">CELERY_BROKER_URL</span> <span class="o">=</span> <span class="s">"sqla+sqlite:///celery.sqlite"</span> <span class="n">CELERY_RESULT_BACKEND</span> <span class="o">=</span> <span class="s">"db+sqlite:///celery.sqlite"</span> </code></pre></div></div> <p>We will use SQLite as a broker and results backend.</p> <blockquote> <p>It is an example project showing how to use <code class="language-plaintext highlighter-rouge">PeriodicTask</code>, thus broker and results backend performance is out of the scope of this article.</p> </blockquote> <p>We have the Celery configuration completed.</p> <h2 id="background-task">Background task</h2> <p>Please add the <code class="language-plaintext highlighter-rouge">tasks.py</code> file in the <code class="language-plaintext highlighter-rouge">backend/monitors</code> directory:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span><span class="p">,</span> <span class="n">timedelta</span> <span class="kn">from</span> <span class="nn">decimal</span> <span class="kn">import</span> <span class="n">Decimal</span> <span class="kn">import</span> <span class="nn">requests</span> <span class="kn">from</span> <span class="nn">celery</span> <span class="kn">import</span> <span class="n">shared_task</span> <span class="kn">from</span> <span class="nn">monitors.models</span> <span class="kn">import</span> <span class="n">Monitor</span><span class="p">,</span> <span class="n">MonitorRequest</span> <span class="o">@</span><span class="n">shared_task</span><span class="p">(</span><span class="n">bind</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="k">def</span> <span class="nf">task_monitor</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">monitor_id</span><span class="p">):</span> <span class="k">try</span><span class="p">:</span> <span class="n">monitor</span> <span class="o">=</span> <span class="n">Monitor</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">pk</span><span class="o">=</span><span class="n">monitor_id</span><span class="p">)</span> <span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">monitor</span><span class="p">.</span><span class="n">endpoint</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">60</span><span class="p">)</span> <span class="n">MonitorRequest</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="n">create</span><span class="p">(</span> <span class="n">response_time</span><span class="o">=</span><span class="nb">int</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">elapsed</span><span class="p">.</span><span class="n">total_seconds</span><span class="p">()</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">),</span> <span class="n">response_status</span><span class="o">=</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">monitor</span><span class="o">=</span><span class="n">monitor</span><span class="p">,</span> <span class="p">)</span> <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span> <span class="k">print</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">e</span><span class="p">),</span> <span class="nb">type</span><span class="p">(</span><span class="n">e</span><span class="p">))</span> </code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">task_monitor(self, monitor_id)</code> function has <code class="language-plaintext highlighter-rouge">@shared_task</code> decorator. It accepts <code class="language-plaintext highlighter-rouge">monitor_id</code> as an argument - it is passed when <code class="language-plaintext highlighter-rouge">PeriodicTask</code> is created as a <code class="language-plaintext highlighter-rouge">kwargs</code> field.</p> <p>The <code class="language-plaintext highlighter-rouge">task_monitor</code> sends a GET request to the monitored server and saves response time and status. It is a simplified version of <code class="language-plaintext highlighter-rouge">task_monitor</code> used in my <a href="https://monitor-uptime.com"><strong>uptime monitoring service</strong></a>.</p> <h2 id="run-django-and-celery">Run Django and Celery</h2> <p>We are ready to play with our web application. You will need three terminals. Please start the Django development server in the first one:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python manage.py runserver </code></pre></div></div> <p>In the second terminal, please start the Celery worker:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>celery <span class="nt">-A</span> backend worker <span class="nt">--loglevel</span><span class="o">=</span>info <span class="nt">-P</span> gevent <span class="nt">--concurrency</span> 1 <span class="nt">-E</span> </code></pre></div></div> <p>In the third terminal, please start Celery beat:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>celery <span class="nt">-A</span> backend beat <span class="nt">-l</span> INFO <span class="nt">--scheduler</span> django_celery_beat.schedulers:DatabaseScheduler <span class="nt">--max-interval</span> 10 </code></pre></div></div> <blockquote> <p>Celery beat service uses <code class="language-plaintext highlighter-rouge">DatabaseScheduler</code> from <code class="language-plaintext highlighter-rouge">django-celery-beat</code> package. The beat service checks scheduled tasks from the database. Tasks defined with <code class="language-plaintext highlighter-rouge">PeriodicTask</code> are persistent. Tasks will be available even after the Celery worker and beat restart.</p> </blockquote> <p>Please open your (favorite) web browser and enter the address <code class="language-plaintext highlighter-rouge">http://127.0.0.1:8000</code>. You should see the REST API viewer automatically generated by DRF:</p> <p><img src="/tutorial/dynamically-update-periodic-tasks-celery/drf-rest-api-viewer.png" alt="" /></p> <blockquote> <p>If you have problems or need help, please create a <a href="https://github.com/saasitive/dynamically-update-periodic-tasks">GitHub issue</a>. We will try to help you! You won’t be alone.</p> </blockquote> <p>Please open the monitors API at <code class="language-plaintext highlighter-rouge">http://127.0.0.1:8000/monitors/1/</code>. The list of monitors should be empty; let’s add the first monitor. Please fill out the form and click the <code class="language-plaintext highlighter-rouge">POST</code> button.</p> <p><img src="/tutorial/dynamically-update-periodic-tasks-celery/create-monitor.png" alt="" /></p> <p>Please wait some time to have some results, and you should see in the Celery beat terminal:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">[</span>2022-10-17 12:05:59,887: INFO/MainProcess] Scheduler: Sending due task Monitor: https://github.com <span class="o">(</span>monitors.tasks.task_monitor<span class="o">)</span> <span class="o">[</span>2022-10-17 12:06:59,887: INFO/MainProcess] Scheduler: Sending due task Monitor: https://github.com <span class="o">(</span>monitors.tasks.task_monitor<span class="o">)</span> <span class="o">[</span>2022-10-17 12:07:59,909: INFO/MainProcess] Scheduler: Sending due task Monitor: https://github.com <span class="o">(</span>monitors.tasks.task_monitor<span class="o">)</span> <span class="o">[</span>2022-10-17 12:08:59,909: INFO/MainProcess] Scheduler: Sending due task Monitor: https://github.com <span class="o">(</span>monitors.tasks.task_monitor<span class="o">)</span> <span class="o">[</span>2022-10-17 12:09:59,914: INFO/MainProcess] Scheduler: Sending due task Monitor: https://github.com <span class="o">(</span>monitors.tasks.task_monitor<span class="o">)</span> </code></pre></div></div> <p>Example output for Celery worker terminal:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">[</span>2022-10-17 12:08:00,644: INFO/MainProcess] Task monitors.tasks.task_monitor[e8ea92d4-e834-4bdf-8f4a-af6ee0601b55] received <span class="o">[</span>2022-10-17 12:08:01,304: INFO/MainProcess] Task monitors.tasks.task_monitor[e8ea92d4-e834-4bdf-8f4a-af6ee0601b55] succeeded <span class="k">in </span>0.65664959300193s: None <span class="o">[</span>2022-10-17 12:09:00,101: INFO/MainProcess] Task monitors.tasks.task_monitor[8c597c74-4917-4f3e-894f-a6ae117fc0f3] received <span class="o">[</span>2022-10-17 12:09:01,909: INFO/MainProcess] Task monitors.tasks.task_monitor[8c597c74-4917-4f3e-894f-a6ae117fc0f3] succeeded <span class="k">in </span>1.8063794959998631s: None <span class="o">[</span>2022-10-17 12:10:00,604: INFO/MainProcess] Task monitors.tasks.task_monitor[739f9227-4b63-409d-9d72-57720c2da5f0] received <span class="o">[</span>2022-10-17 12:10:00,873: INFO/MainProcess] Task monitors.tasks.task_monitor[739f9227-4b63-409d-9d72-57720c2da5f0] succeeded <span class="k">in </span>0.2661438309987716s: None </code></pre></div></div> <p>Please open requests API at <code class="language-plaintext highlighter-rouge">http://127.0.0.1:8000/requests/</code> at the beginnig you will see only an empty list:</p> <p><img src="/tutorial/dynamically-update-periodic-tasks-celery/empty-requests.png" alt="" /></p> <p>After some time, it will be filled with requests data:</p> <p><img src="/tutorial/dynamically-update-periodic-tasks-celery/requests-list.png" alt="" /></p> <p>You can stop the monitoring task by deleting the monitor. Please open the <code class="language-plaintext highlighter-rouge">http://127.0.0.1:8000/monitors/1/</code> (where <code class="language-plaintext highlighter-rouge">1</code> is monitor ID) and click the <code class="language-plaintext highlighter-rouge">Delete</code> button.</p> <p><img src="/tutorial/dynamically-update-periodic-tasks-celery/delete-monitor.png" alt="" /></p> <p>You should see that no more requests are produced. The monitor and an associated <code class="language-plaintext highlighter-rouge">PeriodicTask</code> object have been removed. The code for this article is available in the <a href="https://github.com/saasitive/dynamically-update-periodic-tasks">GitHub repository</a>.</p> <blockquote> <p>To check periodic tasks, you can open the Django Admin Panel at <code class="language-plaintext highlighter-rouge">http://127.0.0.1:8000/admin</code>.</p> </blockquote> <h2 id="summary">Summary</h2> <p>Celery is a great task queue. You can dynamically create or delete periodic tasks with the <code class="language-plaintext highlighter-rouge">django-celery-beat</code> package without Celery restart. What is more, the <code class="language-plaintext highlighter-rouge">PeriodicTask</code> object allows you to dynamically change the interval values and pause the task (it was not described in this article). These features were very helpful for me while implementing the uptime monitoring service.</p>Aleksandra Płońska, Piotr PłońskiCelery is a popular distributed tasks queue. It is often used with Django framework to process heavy computational tasks in the background. You can add a single task to the queue or define periodic tasks. Periodic tasks are automatically scheduled for execution based on set time. The most common approach is to define the periodic tasks before the Celery worker is started. For example, it can be daily cleaning of the database. What if we would like to define periodic tasks dynamically? Recently, I’ve been working on a web app for uptime monitoring. The service continuously monitors a server and send email notification when the server is down. In the web app, the user can add a server address to be monitored and select the interval between requests to the server. How to dynamically add periodic tasks in Celery? I want to describe my approach in this article.Email Verification View in React for DRF backend2022-10-06T00:00:00+00:002022-10-06T00:00:00+00:00https://saasitive.com/tutorial/email-verification-react-django<p><img src="/tutorial/email-verification-react-django/banner.jpg" alt="DRF register new user with email verification banner" />After registration, a user will get an email with a verification link. Why is there such a procedure? We would like to have users with valid emails only to be able to contact them. (If we don’t care about emails, we could use the username for login)</p> <p>When a user clicks the verification link, then the website for email verification will be opened. The token from the link will be used to send a POST request to the server. The verification status will be displayed in the website.</p> <blockquote> <p><strong>This article is part of <a href="https://saasitive.com">SaaSitive paid course</a></strong>.</p> </blockquote> <h2 id="email-verification-view">Email Verification View</h2> <p>Below is the email with a verification link:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: [example.com] Please Confirm Your E-mail Address From: webmaster@localhost To: [email protected] Date: Thu, 25 Aug 2022 13:53:15 -0000 Message-ID: &lt;166143559536.50748.16793504971950475831@komp2&gt; Hello from example.com! You're receiving this e-mail because user pepek has given your e-mail address to register an account on example.com. To confirm this is correct, go to http://127.0.0.1:8000/verify-email/MQ:1oRDIJ:EliIs1rQ4mAr_jch8LuC-T98gIoKBhirwSMlygiINkU/ Thank you for using example.com! example.com ------------------------------------------------------------------------------- </code></pre></div></div> <p>The verification link:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://127.0.0.1:8000/verify-email/MQ:1oRDIJ:EliIs1rQ4mAr_jch8LuC-T98gIoKBhirwSMlygiINkU/ /-----domain--------/--view-url--/--------------token-----------------------------------/ </code></pre></div></div> <p>We will change the domain later, let’s take the link and paste it into the web browser with domain <code class="language-plaintext highlighter-rouge">localhost:3000</code>:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://localhost:3000/verify-email/MQ:1oRDIJ:EliIs1rQ4mAr_jch8LuC-T98gIoKBhirwSMlygiINkU/ </code></pre></div></div> <p>We should see an empty page.</p> <p>Let’s add a new view <code class="language-plaintext highlighter-rouge">VerifyEmailView.tsx</code> in the <code class="language-plaintext highlighter-rouge">frontend/src/views</code> directory:</p> <div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nx">VerifyEmailView</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="p">(</span> <span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"container"</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"row"</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"col-md-10 offset-md-1"</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>Verify Email<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span> <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> <span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>This view will be displayed for the verification link. Let’s add a new <code class="language-plaintext highlighter-rouge">Route</code> in <code class="language-plaintext highlighter-rouge">frontend/src/AppRoutes.txs</code> file:</p> <div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">VerifyEmailView</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./views/VerifyEmailView</span><span class="dl">"</span><span class="p">;</span> <span class="c1">// the rest of the code ...</span> <span class="p">&lt;</span><span class="nc">Route</span> <span class="na">path</span><span class="p">=</span><span class="s">"/"</span> <span class="na">element</span><span class="p">=</span><span class="si">{</span><span class="p">&lt;</span><span class="nc">WebLayout</span> <span class="p">/&gt;</span><span class="si">}</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nc">Route</span> <span class="na">path</span><span class="p">=</span><span class="s">"/verify-email/:key"</span> <span class="na">element</span><span class="p">=</span><span class="si">{</span><span class="p">&lt;</span><span class="nc">VerifyEmailView</span> <span class="p">/&gt;</span><span class="si">}</span> <span class="p">/&gt;</span> <span class="si">{</span><span class="cm">/* the rest of the code ... */</span><span class="si">}</span> <span class="p">&lt;/</span><span class="nc">Route</span><span class="p">&gt;</span> <span class="c1">// the rest of the code ...</span> </code></pre></div></div> <p>Please note that the <code class="language-plaintext highlighter-rouge">path</code> is <code class="language-plaintext highlighter-rouge">"/verify-email/:key"</code>. The <code class="language-plaintext highlighter-rouge">:key</code> is for obtaining a token from the URL address.</p> <p>When we enter the verification link we will see only <code class="language-plaintext highlighter-rouge">Verify Email</code> header. We need to perform a POST request with the token in the view. We will need a new variable in the <code class="language-plaintext highlighter-rouge">auth</code> store to keep information about email verification status.</p> <h2 id="update-authslice">Update <code class="language-plaintext highlighter-rouge">authSlice</code></h2> <p>Let’s update the <code class="language-plaintext highlighter-rouge">frontend/src/slices/authSlice.ts</code>:</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c1">// the rest of the code ...</span> <span class="kd">const</span> <span class="nx">initialState</span> <span class="o">=</span> <span class="p">{</span> <span class="na">usernameRegisterError</span><span class="p">:</span> <span class="dl">""</span><span class="p">,</span> <span class="na">emailRegisterError</span><span class="p">:</span> <span class="dl">""</span><span class="p">,</span> <span class="na">password1RegisterError</span><span class="p">:</span> <span class="dl">""</span><span class="p">,</span> <span class="na">verifyEmailStatus</span><span class="p">:</span> <span class="dl">"</span><span class="s2">unknown</span><span class="dl">"</span><span class="p">,</span> <span class="c1">// new variable in the store</span> <span class="p">};</span> <span class="kd">const</span> <span class="nx">authSlice</span> <span class="o">=</span> <span class="nx">createSlice</span><span class="p">({</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">auth</span><span class="dl">'</span><span class="p">,</span> <span class="nx">initialState</span><span class="p">,</span> <span class="na">reducers</span><span class="p">:</span> <span class="p">{</span> <span class="nx">setUsernameRegisterError</span><span class="p">(</span><span class="nx">state</span><span class="p">,</span> <span class="na">action</span><span class="p">:</span> <span class="nx">PayloadAction</span><span class="o">&lt;</span><span class="kr">string</span><span class="o">&gt;</span><span class="p">)</span> <span class="p">{</span> <span class="nx">state</span><span class="p">.</span><span class="nx">usernameRegisterError</span> <span class="o">=</span> <span class="nx">action</span><span class="p">.</span><span class="nx">payload</span><span class="p">;</span> <span class="p">},</span> <span class="nx">setEmailRegisterError</span><span class="p">(</span><span class="nx">state</span><span class="p">,</span> <span class="na">action</span><span class="p">:</span> <span class="nx">PayloadAction</span><span class="o">&lt;</span><span class="kr">string</span><span class="o">&gt;</span><span class="p">)</span> <span class="p">{</span> <span class="nx">state</span><span class="p">.</span><span class="nx">emailRegisterError</span> <span class="o">=</span> <span class="nx">action</span><span class="p">.</span><span class="nx">payload</span><span class="p">;</span> <span class="p">},</span> <span class="nx">setPassword1RegisterError</span><span class="p">(</span><span class="nx">state</span><span class="p">,</span> <span class="na">action</span><span class="p">:</span> <span class="nx">PayloadAction</span><span class="o">&lt;</span><span class="kr">string</span><span class="o">&gt;</span><span class="p">)</span> <span class="p">{</span> <span class="nx">state</span><span class="p">.</span><span class="nx">password1RegisterError</span> <span class="o">=</span> <span class="nx">action</span><span class="p">.</span><span class="nx">payload</span><span class="p">;</span> <span class="p">},</span> <span class="c1">//</span> <span class="c1">// new function to set variable value</span> <span class="c1">//</span> <span class="nx">setVerifyEmailStatus</span><span class="p">(</span><span class="nx">state</span><span class="p">,</span> <span class="na">action</span><span class="p">:</span> <span class="nx">PayloadAction</span><span class="o">&lt;</span><span class="kr">string</span><span class="o">&gt;</span><span class="p">)</span> <span class="p">{</span> <span class="nx">state</span><span class="p">.</span><span class="nx">verifyEmailStatus</span> <span class="o">=</span> <span class="nx">action</span><span class="p">.</span><span class="nx">payload</span><span class="p">;</span> <span class="p">},</span> <span class="p">},</span> <span class="p">});</span> <span class="k">export</span> <span class="k">default</span> <span class="nx">authSlice</span><span class="p">.</span><span class="nx">reducer</span><span class="p">;</span> <span class="k">export</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">setUsernameRegisterError</span><span class="p">,</span> <span class="nx">setEmailRegisterError</span><span class="p">,</span> <span class="nx">setPassword1RegisterError</span><span class="p">,</span> <span class="c1">//</span> <span class="nx">setVerifyEmailStatus</span><span class="p">,</span> <span class="c1">// export new function</span> <span class="c1">//</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">authSlice</span><span class="p">.</span><span class="nx">actions</span><span class="p">;</span> <span class="c1">// ...</span> <span class="c1">// new getter function</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">getVerifyEmailStatus</span> <span class="o">=</span> <span class="p">(</span><span class="nx">state</span><span class="p">:</span> <span class="nx">RootState</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">state</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nx">verifyEmailStatus</span><span class="p">;</span> <span class="c1">// the rest of the code ...</span> </code></pre></div></div> <p>Updates in the <code class="language-plaintext highlighter-rouge">authSlice</code>:</p> <ul> <li>we added a new variable in the <code class="language-plaintext highlighter-rouge">initialState</code> with an <code class="language-plaintext highlighter-rouge">unknown</code> value,</li> <li>we added a new function <code class="language-plaintext highlighter-rouge">setVerifyEmailStatus</code> to set status,</li> <li>we added a new function <code class="language-plaintext highlighter-rouge">getVerifyEmailStatus</code> to get email verification status from the store.</li> </ul> <p>The store is ready. The next step is to write a function to do POST request with a verification token.</p> <h2 id="post-request-with-a-verification-token">POST request with a verification token</h2> <p>Please add a new function at the end of the <code class="language-plaintext highlighter-rouge">authSlice.ts</code> file:</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">const</span> <span class="nx">verifyEmail</span> <span class="o">=</span> <span class="p">(</span><span class="nx">key</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="k">async</span> <span class="p">(</span><span class="nx">dispatch</span><span class="p">:</span> <span class="nx">TypedDispatch</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">try</span> <span class="p">{</span> <span class="c1">// set status to started </span> <span class="nx">dispatch</span><span class="p">(</span><span class="nx">setVerifyEmailStatus</span><span class="p">(</span><span class="dl">"</span><span class="s2">started</span><span class="dl">"</span><span class="p">));</span> <span class="c1">// send POST request</span> <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">/api/auth/register/verify-email/</span><span class="dl">"</span><span class="p">;</span> <span class="k">await</span> <span class="nx">axios</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="p">{</span> <span class="nx">key</span> <span class="p">});</span> <span class="c1">// set verify email status to ok</span> <span class="nx">dispatch</span><span class="p">(</span><span class="nx">setVerifyEmailStatus</span><span class="p">(</span><span class="dl">"</span><span class="s2">ok</span><span class="dl">"</span><span class="p">));</span> <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// set status to error</span> <span class="nx">dispatch</span><span class="p">(</span><span class="nx">setVerifyEmailStatus</span><span class="p">(</span><span class="dl">"</span><span class="s2">error</span><span class="dl">"</span><span class="p">));</span> <span class="p">}</span> <span class="p">};</span> </code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">verifyEmail</code> function gets the <code class="language-plaintext highlighter-rouge">key</code> as the argument. The <code class="language-plaintext highlighter-rouge">key</code> value is a verification token. It is sent to the backend to the <code class="language-plaintext highlighter-rouge">/api/auth/register/verify-email/</code> endpoint. The <code class="language-plaintext highlighter-rouge">verifyEmailStatus</code> is set to <code class="language-plaintext highlighter-rouge">"ok"</code> for successful response and <code class="language-plaintext highlighter-rouge">"error"</code> otherwise.</p> <h2 id="update-verifyemailviewtsx">Update <code class="language-plaintext highlighter-rouge">VerifyEmailView.tsx</code></h2> <p>We will use the <code class="language-plaintext highlighter-rouge">verifyEmailStatus</code> variable to display proper messages for our users.</p> <p>The <code class="language-plaintext highlighter-rouge">frontend/src/views/VerifyEmailView.tsx</code>:</p> <div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">useEffect</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">react</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">useSelector</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">react-redux</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">useNavigate</span><span class="p">,</span> <span class="nx">useParams</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">react-router-dom</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">getVerifyEmailStatus</span><span class="p">,</span> <span class="nx">verifyEmail</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">../slices/authSlice</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">useAppDispatch</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">../store</span><span class="dl">"</span><span class="p">;</span> <span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nx">VerifyEmailView</span><span class="p">()</span> <span class="p">{</span> <span class="kd">let</span> <span class="p">{</span> <span class="nx">key</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">useParams</span><span class="p">();</span> <span class="c1">// get key (token) from URL</span> <span class="kd">let</span> <span class="nx">navigate</span> <span class="o">=</span> <span class="nx">useNavigate</span><span class="p">();</span> <span class="c1">// create navigate variable </span> <span class="kd">const</span> <span class="nx">dispatch</span> <span class="o">=</span> <span class="nx">useAppDispatch</span><span class="p">();</span> <span class="kd">const</span> <span class="nx">emailVerifyStatus</span> <span class="o">=</span> <span class="nx">useSelector</span><span class="p">(</span><span class="nx">getVerifyEmailStatus</span><span class="p">);</span> <span class="c1">//get emailVerifyStatus</span> <span class="c1">// after view load send POST request</span> <span class="nx">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="nx">key</span><span class="p">)</span> <span class="p">{</span> <span class="nx">dispatch</span><span class="p">(</span><span class="nx">verifyEmail</span><span class="p">(</span><span class="nx">key</span><span class="p">));</span> <span class="p">}</span> <span class="p">},</span> <span class="p">[</span><span class="nx">dispatch</span><span class="p">,</span> <span class="nx">key</span><span class="p">]);</span> <span class="k">return</span> <span class="p">(</span> <span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"container"</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"row"</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"col-md-10 offset-md-1"</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>Verify Email<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span> <span class="si">{</span><span class="p">(</span><span class="nx">emailVerifyStatus</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">unknown</span><span class="dl">"</span> <span class="o">||</span> <span class="nx">emailVerifyStatus</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">error</span><span class="dl">"</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="p">(</span> <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span> We can't verify your email. Please try to register again or contact us by email [email protected] <span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span> <span class="p">)</span><span class="si">}</span> <span class="si">{</span><span class="nx">emailVerifyStatus</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">started</span><span class="dl">"</span> <span class="o">&amp;&amp;</span> <span class="p">(</span> <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>Email verification started, please wait a while ...<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span> <span class="p">)</span><span class="si">}</span> <span class="si">{</span><span class="nx">emailVerifyStatus</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">ok</span><span class="dl">"</span> <span class="o">&amp;&amp;</span> <span class="p">(</span> <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span> Successfull email verification🎉 Please login to start monitoring! <span class="p">&lt;</span><span class="nt">br</span> <span class="p">/&gt;</span> <span class="p">&lt;</span><span class="nt">button</span> <span class="na">className</span><span class="p">=</span><span class="s">"btn btn-lg btn-primary my-2"</span> <span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">navigate</span><span class="p">(</span><span class="dl">"</span><span class="s2">/login</span><span class="dl">"</span><span class="p">)</span><span class="si">}</span> <span class="p">&gt;</span> Login <span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span> <span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span> <span class="p">)</span><span class="si">}</span> <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> <span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>We will use <code class="language-plaintext highlighter-rouge">useParams</code> from <code class="language-plaintext highlighter-rouge">react-router-dom</code> to get the token value from the URL address.</p> <div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="p">{</span> <span class="nx">key</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">useParams</span><span class="p">();</span> <span class="c1">// get key (token) from URL</span> </code></pre></div></div> <p>We will get the <code class="language-plaintext highlighter-rouge">emailVerifyStatus</code> from the store:</p> <div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">emailVerifyStatus</span> <span class="o">=</span> <span class="nx">useSelector</span><span class="p">(</span><span class="nx">getVerifyEmailStatus</span><span class="p">);</span> <span class="c1">//get emailVerifyStatus</span> </code></pre></div></div> <p>The POST request to the server will be sent after the <code class="language-plaintext highlighter-rouge">VerifyEmailView</code> component is loaded. We will use the <code class="language-plaintext highlighter-rouge">useEffect</code> React’s hook for this:</p> <div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="nx">key</span><span class="p">)</span> <span class="p">{</span> <span class="nx">dispatch</span><span class="p">(</span><span class="nx">verifyEmail</span><span class="p">(</span><span class="nx">key</span><span class="p">));</span> <span class="p">}</span> <span class="p">},</span> <span class="p">[</span><span class="nx">dispatch</span><span class="p">,</span> <span class="nx">key</span><span class="p">]);</span> </code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">useEffect</code> hook is called only once after loading the component. It dispatches the <code class="language-plaintext highlighter-rouge">verifyEmail(key)</code>.</p> <p>We use the <code class="language-plaintext highlighter-rouge">emailVerifyStatus</code> value to display proper information for the user.</p> <div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{(</span><span class="nx">emailVerifyStatus</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">unknown</span><span class="dl">"</span> <span class="o">||</span> <span class="nx">emailVerifyStatus</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">error</span><span class="dl">"</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="p">(</span> <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span> We can't verify your email. Please try to register again or contact us by email [email protected] <span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span> <span class="p">)}</span> <span class="p">{</span><span class="nx">emailVerifyStatus</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">started</span><span class="dl">"</span> <span class="o">&amp;&amp;</span> <span class="p">(</span> <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>Email verification started, please wait a while ...<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span> <span class="p">)}</span> <span class="p">{</span><span class="nx">emailVerifyStatus</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">ok</span><span class="dl">"</span> <span class="o">&amp;&amp;</span> <span class="p">(</span> <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span> Successfull email verification🎉 Please login to start monitoring! <span class="p">&lt;</span><span class="nt">br</span> <span class="p">/&gt;</span> <span class="p">&lt;</span><span class="nt">button</span> <span class="na">className</span><span class="p">=</span><span class="s">"btn btn-lg btn-primary my-2"</span> <span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">navigate</span><span class="p">(</span><span class="dl">"</span><span class="s2">/login</span><span class="dl">"</span><span class="p">)</span><span class="si">}</span> <span class="p">&gt;</span> Login <span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span> <span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span> <span class="p">)}</span> </code></pre></div></div> <p>The login button is displayed after successful verification. The user will be redirected to log in view after clicking it.</p> <h2 id="summary">Summary</h2> <p>We created a view for email verification. It displays information about verification status for the user and makes a POST request with a token value from the URL. We will add toasts and <code class="language-plaintext highlighter-rouge">BlockUI</code> in the next post (in the course).</p> <blockquote> <p><strong>This article is part of <a href="https://saasitive.com">SaaSitive paid course</a></strong>.</p> </blockquote>Aleksandra Płońska, Piotr PłońskiAfter registration, a user will get an email with a verification link. Why is there such a procedure? We would like to have users with valid emails only to be able to contact them. (If we don’t care about emails, we could use the username for login)Django Rest Framework Register New User with Email Verification2022-10-06T00:00:00+00:002022-10-06T00:00:00+00:00https://saasitive.com/tutorial/django-rest-framework-register-user-email-verification<p><img src="/tutorial/django-rest-framework-register-user-email-verification/banner.jpg" alt="DRF register new user with email verification banner" />In this step, we will look closely at how the new user registration process looks and write a unit test for registration.</p> <blockquote> <p><strong>This article is part of <a href="https://saasitive.com">SaaSitive paid course</a></strong>.</p> </blockquote> <p>Below is a sequence diagram of what the registration looks like:</p> <p><img src="/tutorial/django-rest-framework-register-user-email-verification/register-new-user-sequence.png" alt="DRF register new user with email verification sequence" /></p> <p>We will focus only on the backend part. The registration is available at <code class="language-plaintext highlighter-rouge">/api/auth/register/</code> endpoint. You can manually register a new user by filling out the form on <code class="language-plaintext highlighter-rouge">http://127.0.0.1:8000/api/auth/register/</code>. After the successful user creation, the server will send an email to the user. Right now, the email sending is not configured. The server will print the email in the terminal where the development server is running. There is a verification link sent in the verification email. We need to provide a custom URL address for it. You can read more about it in <code class="language-plaintext highlighter-rouge">dj-rest-auth</code> <a href="https://dj-rest-auth.readthedocs.io/en/latest/faq.html">documentation</a>.</p> <p>Let’s edit the <code class="language-plaintext highlighter-rouge">backend/accounts/urls.py</code> file:</p> <div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">django.conf.urls</span> <span class="kn">import</span> <span class="n">include</span> <span class="kn">from</span> <span class="nn">django.urls</span> <span class="kn">import</span> <span class="n">path</span><span class="p">,</span> <span class="n">re_path</span> <span class="kn">from</span> <span class="nn">django.views.generic.base</span> <span class="kn">import</span> <span class="n">TemplateView</span> <span class="n">accounts_urlpatterns</span> <span class="o">=</span> <span class="p">[</span> <span class="n">path</span><span class="p">(</span><span class="s">"api/auth/"</span><span class="p">,</span> <span class="n">include</span><span class="p">(</span><span class="s">"dj_rest_auth.urls"</span><span class="p">)),</span> <span class="n">path</span><span class="p">(</span><span class="s">"api/auth/register/"</span><span class="p">,</span> <span class="n">include</span><span class="p">(</span><span class="s">"dj_rest_auth.registration.urls"</span><span class="p">)),</span> <span class="c1"># path to set verify email in the frontend </span> <span class="c1"># fronted will do POST request to server with key </span> <span class="c1"># this is empty view, just to make reverse works </span> <span class="n">re_path</span><span class="p">(</span> <span class="s">r"^verify-email/(?P&lt;key&gt;[-:\w]+)/$"</span><span class="p">,</span> <span class="n">TemplateView</span><span class="p">.</span><span class="n">as_view</span><span class="p">(),</span> <span class="n">name</span><span class="o">=</span><span class="s">"account_confirm_email"</span><span class="p">,</span> <span class="p">),</span> <span class="p">]</span> </code></pre></div></div> <p>I’ve added a line:</p> <div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">re_path</span><span class="p">(</span> <span class="s">r"^verify-email/(?P&lt;key&gt;[-:\w]+)/$"</span><span class="p">,</span> <span class="n">TemplateView</span><span class="p">.</span><span class="n">as_view</span><span class="p">(),</span> <span class="n">name</span><span class="o">=</span><span class="s">"account_confirm_email"</span><span class="p">,</span> <span class="p">),</span> </code></pre></div></div> <p>It will allow the <code class="language-plaintext highlighter-rouge">dj-rest-auth</code> package to construct a verification link in the email in the form like:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>server-address/verify-email/some-key-here </code></pre></div></div> <p>Please remember to add imports with <code class="language-plaintext highlighter-rouge">re_path</code> and <code class="language-plaintext highlighter-rouge">TemplateView</code>.</p> <p>The address <code class="language-plaintext highlighter-rouge">/verify-email/</code> will be available in the frontend. The verification view will be displayed when the user clicks the verification link. This view will parse the verification key from the URL address and send it in the POST request to the server (endpoint <code class="language-plaintext highlighter-rouge">/api/auth/register/verify-email/</code>).</p> <h2 id="unit-test">Unit test</h2> <p>Let’s create a unit test that will check how registration is working. I like writing unit tests for the backend because it helps me create frontend code. From unit tests, I see precisely how the backend works, what status codes are returned, and how response data looks.</p> <p>Please add the following code in the <code class="language-plaintext highlighter-rouge">backend/accounts/tests.py</code>:</p> <div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">django.core</span> <span class="kn">import</span> <span class="n">mail</span> <span class="kn">from</span> <span class="nn">rest_framework</span> <span class="kn">import</span> <span class="n">status</span> <span class="kn">from</span> <span class="nn">rest_framework.test</span> <span class="kn">import</span> <span class="n">APITestCase</span> <span class="k">class</span> <span class="nc">AccountsTestCase</span><span class="p">(</span><span class="n">APITestCase</span><span class="p">):</span> <span class="n">register_url</span> <span class="o">=</span> <span class="s">"/api/auth/register/"</span> <span class="n">verify_email_url</span> <span class="o">=</span> <span class="s">"/api/auth/register/verify-email/"</span> <span class="n">login_url</span> <span class="o">=</span> <span class="s">"/api/auth/login/"</span> <span class="k">def</span> <span class="nf">test_register</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="c1"># register data </span> <span class="n">data</span> <span class="o">=</span> <span class="p">{</span> <span class="s">"email"</span><span class="p">:</span> <span class="s">"[email protected]"</span><span class="p">,</span> <span class="s">"password1"</span><span class="p">:</span> <span class="s">"verysecret"</span><span class="p">,</span> <span class="s">"password2"</span><span class="p">:</span> <span class="s">"verysecret"</span><span class="p">,</span> <span class="p">}</span> <span class="c1"># send POST request to "/api/auth/register/" </span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">register_url</span><span class="p">,</span> <span class="n">data</span><span class="p">)</span> <span class="c1"># check the response status and data </span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_201_CREATED</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">json</span><span class="p">()[</span><span class="s">"detail"</span><span class="p">],</span> <span class="s">"Verification e-mail sent."</span><span class="p">)</span> </code></pre></div></div> <p>Let’s run the code and then dive deep into the details. Please execute the following command to run tests:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python manage.py test accounts </code></pre></div></div> <p>You should get the output like below:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Found 1 test(s). Creating test database for alias 'default'... System check identified no issues (0 silenced). . ---------------------------------------------------------------------- Ran 1 test in 0.162s OK Destroying test database for alias 'default'... </code></pre></div></div> <blockquote> <p>It is important to remember that the <code class="language-plaintext highlighter-rouge">django</code> framework for each unit test starts with an empty database.</p> </blockquote> <p>What has just happened? We created a class <code class="language-plaintext highlighter-rouge">AccountsTestCase</code> that derives from <code class="language-plaintext highlighter-rouge">APITestCase</code> (<a href="https://www.django-rest-framework.org/api-guide/testing/#api-test-cases">DRF testing class</a>). In this test class, there are defined three attributes:</p> <ul> <li><code class="language-plaintext highlighter-rouge">register_url = "/api/auth/register/"</code></li> <li><code class="language-plaintext highlighter-rouge">verify_email_url = "/api/auth/register/verify-email/"</code></li> <li><code class="language-plaintext highlighter-rouge">login_url = "/api/auth/login/"</code></li> </ul> <p>There are endpoints that will be used in the test. I don’t like to use <code class="language-plaintext highlighter-rouge">reverse()</code> method for obtaining the URL address in the <code class="language-plaintext highlighter-rouge">django</code> framework because for frontend implementation, I will need complete addresses.</p> <p>Our test class has one unit test: <code class="language-plaintext highlighter-rouge">test_register()</code>.</p> <blockquote> <p>Every unit test needs to start with <code class="language-plaintext highlighter-rouge">test_</code> string.</p> </blockquote> <p>The <code class="language-plaintext highlighter-rouge">test_register()</code> method:</p> <div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">def</span> <span class="nf">test_register</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="c1"># register data </span> <span class="n">data</span> <span class="o">=</span> <span class="p">{</span> <span class="s">"email"</span><span class="p">:</span> <span class="s">"[email protected]"</span><span class="p">,</span> <span class="s">"password1"</span><span class="p">:</span> <span class="s">"verysecret"</span><span class="p">,</span> <span class="s">"password2"</span><span class="p">:</span> <span class="s">"verysecret"</span><span class="p">,</span> <span class="p">}</span> <span class="c1"># send POST request to "/api/auth/register/" </span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">register_url</span><span class="p">,</span> <span class="n">data</span><span class="p">)</span> <span class="c1"># check the response status and data </span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_201_CREATED</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">json</span><span class="p">()[</span><span class="s">"detail"</span><span class="p">],</span> <span class="s">"Verification e-mail sent."</span><span class="p">)</span> </code></pre></div></div> <p>What is the unit test doing:</p> <ol> <li>It prepares test data for registration.</li> <li>It sends the POST request with test data to register a new user.</li> <li>It checks the server response. The server responds with <code class="language-plaintext highlighter-rouge">HTTP 201 CREATED</code> status and the message <code class="language-plaintext highlighter-rouge">Verification e-mail sent.</code>.</li> </ol> <p>You can print the server response in the test by adding at the end:</p> <div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">print</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">json</span><span class="p">())</span> </code></pre></div></div> <p>and running tests again: <code class="language-plaintext highlighter-rouge">python manage.py test accounts</code>.</p> <h2 id="check-login-before-the-email-verification">Check login before the email verification</h2> <p>Let’s try to log in before the email verification is done. Please update the <code class="language-plaintext highlighter-rouge">test_register()</code> method:</p> <div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">def</span> <span class="nf">test_register</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="c1"># register data </span> <span class="n">data</span> <span class="o">=</span> <span class="p">{</span> <span class="s">"email"</span><span class="p">:</span> <span class="s">"[email protected]"</span><span class="p">,</span> <span class="s">"password1"</span><span class="p">:</span> <span class="s">"verysecret"</span><span class="p">,</span> <span class="s">"password2"</span><span class="p">:</span> <span class="s">"verysecret"</span><span class="p">,</span> <span class="p">}</span> <span class="c1"># send POST request to "/api/auth/register/" </span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">register_url</span><span class="p">,</span> <span class="n">data</span><span class="p">)</span> <span class="c1"># check the response status and data </span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_201_CREATED</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">json</span><span class="p">()[</span><span class="s">"detail"</span><span class="p">],</span> <span class="s">"Verification e-mail sent."</span><span class="p">)</span> <span class="c1"># try to login - should fail, because email is not verified </span> <span class="n">login_data</span> <span class="o">=</span> <span class="p">{</span> <span class="s">"email"</span><span class="p">:</span> <span class="n">data</span><span class="p">[</span><span class="s">"email"</span><span class="p">],</span> <span class="s">"password"</span><span class="p">:</span> <span class="n">data</span><span class="p">[</span><span class="s">"password1"</span><span class="p">],</span> <span class="p">}</span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">login_url</span><span class="p">,</span> <span class="n">login_data</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_400_BAD_REQUEST</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertTrue</span><span class="p">(</span> <span class="s">"E-mail is not verified."</span> <span class="ow">in</span> <span class="n">response</span><span class="p">.</span><span class="n">json</span><span class="p">()[</span><span class="s">"non_field_errors"</span><span class="p">]</span> <span class="p">)</span> </code></pre></div></div> <h2 id="test-email">Test email</h2> <p>The next step is to verify the email. The <code class="language-plaintext highlighter-rouge">django</code> tests run with a built-in email service. We can access emails that will be sent during tests with <code class="language-plaintext highlighter-rouge">django.core.mail</code> package.</p> <p>Please update the <code class="language-plaintext highlighter-rouge">test_register()</code>:</p> <div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">def</span> <span class="nf">test_register</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="c1"># register data </span> <span class="n">data</span> <span class="o">=</span> <span class="p">{</span> <span class="s">"email"</span><span class="p">:</span> <span class="s">"[email protected]"</span><span class="p">,</span> <span class="s">"password1"</span><span class="p">:</span> <span class="s">"verysecret"</span><span class="p">,</span> <span class="s">"password2"</span><span class="p">:</span> <span class="s">"verysecret"</span><span class="p">,</span> <span class="p">}</span> <span class="c1"># send POST request to "/api/auth/register/" </span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">register_url</span><span class="p">,</span> <span class="n">data</span><span class="p">)</span> <span class="c1"># check the response status and data </span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_201_CREATED</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">json</span><span class="p">()[</span><span class="s">"detail"</span><span class="p">],</span> <span class="s">"Verification e-mail sent."</span><span class="p">)</span> <span class="c1"># try to login - should fail, because email is not verified </span> <span class="n">login_data</span> <span class="o">=</span> <span class="p">{</span> <span class="s">"email"</span><span class="p">:</span> <span class="n">data</span><span class="p">[</span><span class="s">"email"</span><span class="p">],</span> <span class="s">"password"</span><span class="p">:</span> <span class="n">data</span><span class="p">[</span><span class="s">"password1"</span><span class="p">],</span> <span class="p">}</span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">login_url</span><span class="p">,</span> <span class="n">login_data</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_400_BAD_REQUEST</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertTrue</span><span class="p">(</span> <span class="s">"E-mail is not verified."</span> <span class="ow">in</span> <span class="n">response</span><span class="p">.</span><span class="n">json</span><span class="p">()[</span><span class="s">"non_field_errors"</span><span class="p">]</span> <span class="p">)</span> <span class="c1"># expected one email to be send </span> <span class="c1"># parse email to get token </span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">mail</span><span class="p">.</span><span class="n">outbox</span><span class="p">),</span> <span class="mi">1</span><span class="p">)</span> <span class="n">email_lines</span> <span class="o">=</span> <span class="n">mail</span><span class="p">.</span><span class="n">outbox</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">body</span><span class="p">.</span><span class="n">splitlines</span><span class="p">()</span> <span class="n">activation_line</span> <span class="o">=</span> <span class="p">[</span><span class="n">l</span> <span class="k">for</span> <span class="n">l</span> <span class="ow">in</span> <span class="n">email_lines</span> <span class="k">if</span> <span class="s">"verify-email"</span> <span class="ow">in</span> <span class="n">l</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span> <span class="n">activation_link</span> <span class="o">=</span> <span class="n">activation_line</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">"go to "</span><span class="p">)[</span><span class="mi">1</span><span class="p">]</span> <span class="n">activation_key</span> <span class="o">=</span> <span class="n">activation_link</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">"/"</span><span class="p">)[</span><span class="mi">4</span><span class="p">]</span> </code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">mail.outbox</code> returns the list of sent emails. There should be 1 email sent:</p> <div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">mail</span><span class="p">.</span><span class="n">outbox</span><span class="p">),</span> <span class="mi">1</span><span class="p">)</span> </code></pre></div></div> <p>To access the email body, we will use the variable <code class="language-plaintext highlighter-rouge">mail.outbox[0].body</code>. The following lines parse the email to get the verification link:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">email_lines</span> <span class="o">=</span> <span class="n">mail</span><span class="p">.</span><span class="n">outbox</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">body</span><span class="p">.</span><span class="n">splitlines</span><span class="p">()</span> <span class="n">activation_line</span> <span class="o">=</span> <span class="p">[</span><span class="n">l</span> <span class="k">for</span> <span class="n">l</span> <span class="ow">in</span> <span class="n">email_lines</span> <span class="k">if</span> <span class="s">"verify-email"</span> <span class="ow">in</span> <span class="n">l</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span> <span class="n">activation_link</span> <span class="o">=</span> <span class="n">activation_line</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">"go to "</span><span class="p">)[</span><span class="mi">1</span><span class="p">]</span> <span class="n">activation_key</span> <span class="o">=</span> <span class="n">activation_link</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">"/"</span><span class="p">)[</span><span class="mi">4</span><span class="p">]</span> </code></pre></div></div> <p>Please add the following code add the end of the test to check what the email looks like:</p> <div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">print</span><span class="p">(</span><span class="n">mail</span><span class="p">.</span><span class="n">outbox</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">body</span><span class="p">)</span> <span class="k">print</span><span class="p">(</span><span class="n">activation_key</span><span class="p">)</span> </code></pre></div></div> <p>After running tests (<code class="language-plaintext highlighter-rouge">python manage.py test accounts</code>) you should get output like:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Found 1 test(s). Creating test database for alias 'default'... System check identified no issues (0 silenced). Hello from example.com! You're receiving this e-mail because user user2 has given your e-mail address to register an account on example.com. To confirm this is correct, go to http://testserver/verify-email/MQ:1oEB8H:L5qGQGcPdxS8C9VoJNgBK07mIBvvaT73AYCfpaFHipc/ Thank you for using example.com! example.com MQ:1oEB8H:L5qGQGcPdxS8C9VoJNgBK07mIBvvaT73AYCfpaFHipc . ---------------------------------------------------------------------- Ran 1 test in 0.296s OK Destroying test database for alias 'default'... </code></pre></div></div> <p>Don’t worry that there is <code class="language-plaintext highlighter-rouge">example.com</code> in the email. We will set the proper domain after production deployment.</p> <p>The <code class="language-plaintext highlighter-rouge">MQ:1oEB8H:L5qGQGcPdxS8C9VoJNgBK07mIBvvaT73AYCfpaFHipc</code> is a verification key. Let’s send it in a POST request:</p> <div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># please add at the end of test_register() method </span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">verify_email_url</span><span class="p">,</span> <span class="p">{</span><span class="s">"key"</span><span class="p">:</span> <span class="n">activation_key</span><span class="p">})</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_200_OK</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">json</span><span class="p">()[</span><span class="s">"detail"</span><span class="p">],</span> <span class="s">"ok"</span><span class="p">)</span> </code></pre></div></div> <p>After successful verification, let’s try to log in:</p> <div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># please add at the end of test_register() method </span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">login_url</span><span class="p">,</span> <span class="n">login_data</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_200_OK</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertTrue</span><span class="p">(</span><span class="s">"key"</span> <span class="ow">in</span> <span class="n">response</span><span class="p">.</span><span class="n">json</span><span class="p">())</span> </code></pre></div></div> <p>The full unit test from file <code class="language-plaintext highlighter-rouge">backend/accounts/tests.py</code>:</p> <div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">django.core</span> <span class="kn">import</span> <span class="n">mail</span> <span class="kn">from</span> <span class="nn">rest_framework</span> <span class="kn">import</span> <span class="n">status</span> <span class="kn">from</span> <span class="nn">rest_framework.test</span> <span class="kn">import</span> <span class="n">APITestCase</span> <span class="k">class</span> <span class="nc">AccountsTestCase</span><span class="p">(</span><span class="n">APITestCase</span><span class="p">):</span> <span class="n">register_url</span> <span class="o">=</span> <span class="s">"/api/auth/register/"</span> <span class="n">verify_email_url</span> <span class="o">=</span> <span class="s">"/api/auth/register/verify-email/"</span> <span class="n">login_url</span> <span class="o">=</span> <span class="s">"/api/auth/login/"</span> <span class="k">def</span> <span class="nf">test_register</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="c1"># register data </span> <span class="n">data</span> <span class="o">=</span> <span class="p">{</span> <span class="s">"email"</span><span class="p">:</span> <span class="s">"[email protected]"</span><span class="p">,</span> <span class="s">"password1"</span><span class="p">:</span> <span class="s">"verysecret"</span><span class="p">,</span> <span class="s">"password2"</span><span class="p">:</span> <span class="s">"verysecret"</span><span class="p">,</span> <span class="p">}</span> <span class="c1"># send POST request to "/api/auth/register/" </span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">register_url</span><span class="p">,</span> <span class="n">data</span><span class="p">)</span> <span class="c1"># check the response status and data </span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_201_CREATED</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">json</span><span class="p">()[</span><span class="s">"detail"</span><span class="p">],</span> <span class="s">"Verification e-mail sent."</span><span class="p">)</span> <span class="c1"># try to login - should fail, because email is not verified </span> <span class="n">login_data</span> <span class="o">=</span> <span class="p">{</span> <span class="s">"email"</span><span class="p">:</span> <span class="n">data</span><span class="p">[</span><span class="s">"email"</span><span class="p">],</span> <span class="s">"password"</span><span class="p">:</span> <span class="n">data</span><span class="p">[</span><span class="s">"password1"</span><span class="p">],</span> <span class="p">}</span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">login_url</span><span class="p">,</span> <span class="n">login_data</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_400_BAD_REQUEST</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertTrue</span><span class="p">(</span> <span class="s">"E-mail is not verified."</span> <span class="ow">in</span> <span class="n">response</span><span class="p">.</span><span class="n">json</span><span class="p">()[</span><span class="s">"non_field_errors"</span><span class="p">]</span> <span class="p">)</span> <span class="c1"># expected one email to be send </span> <span class="c1"># parse email to get token </span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">mail</span><span class="p">.</span><span class="n">outbox</span><span class="p">),</span> <span class="mi">1</span><span class="p">)</span> <span class="n">email_lines</span> <span class="o">=</span> <span class="n">mail</span><span class="p">.</span><span class="n">outbox</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">body</span><span class="p">.</span><span class="n">splitlines</span><span class="p">()</span> <span class="n">activation_line</span> <span class="o">=</span> <span class="p">[</span><span class="n">l</span> <span class="k">for</span> <span class="n">l</span> <span class="ow">in</span> <span class="n">email_lines</span> <span class="k">if</span> <span class="s">"verify-email"</span> <span class="ow">in</span> <span class="n">l</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span> <span class="n">activation_link</span> <span class="o">=</span> <span class="n">activation_line</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">"go to "</span><span class="p">)[</span><span class="mi">1</span><span class="p">]</span> <span class="n">activation_key</span> <span class="o">=</span> <span class="n">activation_link</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">"/"</span><span class="p">)[</span><span class="mi">4</span><span class="p">]</span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">verify_email_url</span><span class="p">,</span> <span class="p">{</span><span class="s">"key"</span><span class="p">:</span> <span class="n">activation_key</span><span class="p">})</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_200_OK</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">json</span><span class="p">()[</span><span class="s">"detail"</span><span class="p">],</span> <span class="s">"ok"</span><span class="p">)</span> <span class="c1"># lets login after verification to get token key </span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">login_url</span><span class="p">,</span> <span class="n">login_data</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_200_OK</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertTrue</span><span class="p">(</span><span class="s">"key"</span> <span class="ow">in</span> <span class="n">response</span><span class="p">.</span><span class="n">json</span><span class="p">())</span> </code></pre></div></div> <p>Please run the test; it should execute without errors.</p> <h2 id="summary">Summary</h2> <p>Great! We have the registration process ready. The REST API is waiting for the frontend. Let’s wait a while with frontend creation. We will need to extend a <code class="language-plaintext highlighter-rouge">User</code> model to keep information about subscription. In the next step of the course, I will show you how to add <code class="language-plaintext highlighter-rouge">UserProfile</code>.</p> <blockquote> <p><strong>This article is part of <a href="https://saasitive.com">SaaSitive paid course</a></strong>.</p> </blockquote>Aleksandra Płońska, Piotr PłońskiIn this step, we will look closely at how the new user registration process looks and write a unit test for registration.Django Rest Framework Email Verification2020-12-23T00:00:00+00:002020-12-23T00:00:00+00:00https://saasitive.com/tutorial/django-rest-framework-email-verification<p>Email verification is an important part of the SaaS application. We will contact the user by email in many cases: for a password reset, announcement of new features, or for sending the invoice. During registration, a user provides the email address. We need to check if the email belongs to the user and that there are no typos/errors in it. This can be easily done by automatically sending the verification email with an activation link. Such a link contains the unique token assigned to the user. After opening the activation link in the web browser, the request is sent to the web application (Django Rest Framework). The web server compares the token from the activation URL with the token stored in the database. If they are the same, the email address is verified.</p> <p>In this tutorial you will learn:</p> <ul> <li>how to set email as a mandatory field during registration,</li> <li>how to set login with email and password,</li> <li>how to send a verification email,</li> <li>how to re-send a verification email,</li> <li>write test cases for email verification flow,</li> <li>the <a href="https://www.django-rest-framework.org/">Django Rest Framework</a> and <a href="https://djoser.readthedocs.io/">Djoser</a> packages will be used.</li> </ul> <p>The next posts will describe the user interface and production email setup. Fill out the <a href="https://forms.gle/rgAG9gkhUEH2wUVt5">form</a> to be notified about future posts.</p> <p>This article is a part of a series of articles on <a href="/django-react/boilerplate/">how to build SaaS from scratch with Django and React</a>. I will use the code from the previous article: <a href="/tutorial/docker-compose-django-react-nginx-let-s-encrypt/">Docker-Compose for Django and React with Nginx reverse-proxy and Let’s encrypt certificate</a>.</p> <hr /> <h2 id="email-required-in-registration">Email required in registration</h2> <p>The <code class="language-plaintext highlighter-rouge">email</code> field is available in <code class="language-plaintext highlighter-rouge">User</code> model in Django, but it is not mandatory. To set <code class="language-plaintext highlighter-rouge">email</code> as the required field and use it for login (<code class="language-plaintext highlighter-rouge">email</code> + <code class="language-plaintext highlighter-rouge">password</code>), we need to do the below steps.</p> <p>Please update the <code class="language-plaintext highlighter-rouge">models.py</code> file in <code class="language-plaintext highlighter-rouge">accounts</code> application. We will update the <code class="language-plaintext highlighter-rouge">email</code> field in the database to make it required.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># /backend/server/apps/accounts/models.py </span><span class="kn">from</span> <span class="nn">django.contrib.auth.models</span> <span class="kn">import</span> <span class="n">User</span> <span class="n">User</span><span class="p">.</span><span class="n">_meta</span><span class="p">.</span><span class="n">get_field</span><span class="p">(</span><span class="s">'email'</span><span class="p">).</span><span class="n">_unique</span> <span class="o">=</span> <span class="bp">True</span> <span class="n">User</span><span class="p">.</span><span class="n">_meta</span><span class="p">.</span><span class="n">get_field</span><span class="p">(</span><span class="s">'email'</span><span class="p">).</span><span class="n">blank</span> <span class="o">=</span> <span class="bp">False</span> <span class="n">User</span><span class="p">.</span><span class="n">_meta</span><span class="p">.</span><span class="n">get_field</span><span class="p">(</span><span class="s">'email'</span><span class="p">).</span><span class="n">null</span> <span class="o">=</span> <span class="bp">False</span> </code></pre></div></div> <p>We need to update Django application <code class="language-plaintext highlighter-rouge">settings.py</code>:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># backend/server/server/settings.py </span> <span class="c1"># ... # configure Djoser </span><span class="n">DJOSER</span> <span class="o">=</span> <span class="p">{</span> <span class="s">"USER_ID_FIELD"</span><span class="p">:</span> <span class="s">"username"</span><span class="p">,</span> <span class="s">"LOGIN_FIELD"</span><span class="p">:</span> <span class="s">"email"</span> <span class="p">}</span> <span class="c1"># ... </span></code></pre></div></div> <p>With the above changes, the <code class="language-plaintext highlighter-rouge">email</code> is required during the registration and login. Remember to make migrations and apply them to the database (we changed the database models).</p> <h2 id="activation-email-sending-in-the-django">Activation email sending in the Django</h2> <p>The next step will be to configure the activation email sending.</p> <p>We need to update <code class="language-plaintext highlighter-rouge">settings.py</code> file:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># backend/server/server/settings.py </span> <span class="c1"># ... # configure Djoser </span><span class="n">DJOSER</span> <span class="o">=</span> <span class="p">{</span> <span class="s">"USER_ID_FIELD"</span><span class="p">:</span> <span class="s">"username"</span><span class="p">,</span> <span class="s">"LOGIN_FIELD"</span><span class="p">:</span> <span class="s">"email"</span><span class="p">,</span> <span class="s">"SEND_ACTIVATION_EMAIL"</span><span class="p">:</span> <span class="bp">True</span><span class="p">,</span> <span class="s">"ACTIVATION_URL"</span><span class="p">:</span> <span class="s">"activate/{uid}/{token}"</span><span class="p">,</span> <span class="s">'SERIALIZERS'</span><span class="p">:</span> <span class="p">{</span> <span class="s">'token_create'</span><span class="p">:</span> <span class="s">'apps.accounts.serializers.CustomTokenCreateSerializer'</span><span class="p">,</span> <span class="p">},</span> <span class="p">}</span> <span class="n">EMAIL_BACKEND</span> <span class="o">=</span> <span class="s">'django.core.mail.backends.console.EmailBackend'</span> <span class="n">SITE_NAME</span> <span class="o">=</span> <span class="s">"SaaSitive"</span> <span class="c1"># ... </span></code></pre></div></div> <p>We added in the <a href="https://djoser.readthedocs.io/">Djoser</a> configuration:</p> <ul> <li>enabled send activation email by <code class="language-plaintext highlighter-rouge">"SEND_ACTIVATION_EMAIL": True</code>;</li> <li>setup the activation email link URL <code class="language-plaintext highlighter-rouge">"ACTIVATION_URL": "activate/{uid}/{token}"</code>. Please notice that there are <code class="language-plaintext highlighter-rouge">uid</code> and <code class="language-plaintext highlighter-rouge">token</code> in the activation URL. Both are required to activate the account. The link is pointing to the URL address in our fronted (we will add React route for it). There is no such endpoint on the backend;</li> <li>defined the custom token create serializer <code class="language-plaintext highlighter-rouge">token_create</code> - it will be needed to allow the user to log in even with an unverified email.</li> </ul> <p>We set the <code class="language-plaintext highlighter-rouge">EMAIL_BACKEND</code> as the Django <code class="language-plaintext highlighter-rouge">django.core.mail.backends.console.EmailBackend</code>. This backend is simply displaying all emails in the console. It is useful only for development. (How to set up Django email backend for production will be described in future posts.) The nice thing about Django is the switchable email backend. For testing purposes Django will automatically switch the email backend to <code class="language-plaintext highlighter-rouge">django.core.mail.backends.locmem.EmailBackend</code> and give us easy access to the email outbox.</p> <p>We also set the <code class="language-plaintext highlighter-rouge">SITE_NAME = "SaaSitive"</code>. The site name variable will be used in the activation email. You can set here your application name.</p> <p>An example of an activation email:</p> <div class="language-email highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">Subject</span><span class="o">:</span><span class="na"> Account activation on SaaSitive</span> Body: You're receiving this email because you need to finish the activation process on SaaSitive. Please go to the following page to activate account: http://testserver/activate/MQ/afc0m5-e3336fd5fa02874a588c9085d9bdf881 Thanks for using our site! The SaaSitive Team </code></pre></div></div> <p>You can overwrite the text in the activation email by setting a different email template in the Djoser configuration.</p> <h2 id="custom-create-a-token-serializer">Custom create a token serializer</h2> <p>The last thing is to add the custom create token serializer. Please add <code class="language-plaintext highlighter-rouge">serializers.py</code> file in the <code class="language-plaintext highlighter-rouge">backend/server/apps/accounts</code> directory. Our custom token will simply overwrite the <a href="https://github.com/sunscrapers/djoser/blob/2862ea4c80d7e95ad246fe646173a7c82e2a9189/djoser/serializers.py#L101"><code class="language-plaintext highlighter-rouge">TokenCreateSerializer</code></a> from the Djoser package and will allow to obtain the <code class="language-plaintext highlighter-rouge">auth_token</code> for the user without an activated account.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># backend/server/apps/accounts/serializers.py </span> <span class="kn">from</span> <span class="nn">django.contrib.auth</span> <span class="kn">import</span> <span class="n">authenticate</span><span class="p">,</span> <span class="n">get_user_model</span> <span class="kn">from</span> <span class="nn">djoser.conf</span> <span class="kn">import</span> <span class="n">settings</span> <span class="kn">from</span> <span class="nn">djoser.serializers</span> <span class="kn">import</span> <span class="n">TokenCreateSerializer</span> <span class="n">User</span> <span class="o">=</span> <span class="n">get_user_model</span><span class="p">()</span> <span class="k">class</span> <span class="nc">CustomTokenCreateSerializer</span><span class="p">(</span><span class="n">TokenCreateSerializer</span><span class="p">):</span> <span class="k">def</span> <span class="nf">validate</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">attrs</span><span class="p">):</span> <span class="n">password</span> <span class="o">=</span> <span class="n">attrs</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"password"</span><span class="p">)</span> <span class="n">params</span> <span class="o">=</span> <span class="p">{</span><span class="n">settings</span><span class="p">.</span><span class="n">LOGIN_FIELD</span><span class="p">:</span> <span class="n">attrs</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">settings</span><span class="p">.</span><span class="n">LOGIN_FIELD</span><span class="p">)}</span> <span class="bp">self</span><span class="p">.</span><span class="n">user</span> <span class="o">=</span> <span class="n">authenticate</span><span class="p">(</span> <span class="n">request</span><span class="o">=</span><span class="bp">self</span><span class="p">.</span><span class="n">context</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"request"</span><span class="p">),</span> <span class="o">**</span><span class="n">params</span><span class="p">,</span> <span class="n">password</span><span class="o">=</span><span class="n">password</span> <span class="p">)</span> <span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="p">.</span><span class="n">user</span><span class="p">:</span> <span class="bp">self</span><span class="p">.</span><span class="n">user</span> <span class="o">=</span> <span class="n">User</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="nb">filter</span><span class="p">(</span><span class="o">**</span><span class="n">params</span><span class="p">).</span><span class="n">first</span><span class="p">()</span> <span class="k">if</span> <span class="bp">self</span><span class="p">.</span><span class="n">user</span> <span class="ow">and</span> <span class="ow">not</span> <span class="bp">self</span><span class="p">.</span><span class="n">user</span><span class="p">.</span><span class="n">check_password</span><span class="p">(</span><span class="n">password</span><span class="p">):</span> <span class="bp">self</span><span class="p">.</span><span class="n">fail</span><span class="p">(</span><span class="s">"invalid_credentials"</span><span class="p">)</span> <span class="c1"># We changed only below line </span> <span class="k">if</span> <span class="bp">self</span><span class="p">.</span><span class="n">user</span><span class="p">:</span> <span class="c1"># and self.user.is_active: </span> <span class="k">return</span> <span class="n">attrs</span> <span class="bp">self</span><span class="p">.</span><span class="n">fail</span><span class="p">(</span><span class="s">"invalid_credentials"</span><span class="p">)</span> </code></pre></div></div> <h2 id="testing-email-verification-flow">Testing email verification flow</h2> <p>The email verification flow in our application is described below. Let’s write test cases to check them. We can also check this manually in the web browser by using DRF browsable API. Writing tests will take some time but have many advantages:</p> <ul> <li>we have tests for user registration flow, so we are confident that all works well,</li> <li>when developing a new feature, writing tests first might be faster than repeated manual tests.</li> </ul> <p><img src="email_verification_flow.png" alt="Email verification flow" /></p> <p>In our tests, we will use the following endpoints:</p> <ul> <li>endpoint to register the new user: <code class="language-plaintext highlighter-rouge">/api/v1/users/</code> with <code class="language-plaintext highlighter-rouge">POST</code> request,</li> <li>endpoint to verify the email: <code class="language-plaintext highlighter-rouge">/api/v1/users/activation</code> with <code class="language-plaintext highlighter-rouge">POST</code> request,</li> <li>endpoint to resend verification email: <code class="language-plaintext highlighter-rouge">/api/v1/users/resend_activation/</code> with <code class="language-plaintext highlighter-rouge">POST</code> request,</li> <li>endpoint to login: <code class="language-plaintext highlighter-rouge">/api/v1/token/login/</code> with <code class="language-plaintext highlighter-rouge">POST</code> request,</li> <li>endpoint to get user details: <code class="language-plaintext highlighter-rouge">/api/v1/users/</code> with <code class="language-plaintext highlighter-rouge">GET</code> request.</li> </ul> <p>Let’s define the empty test in <code class="language-plaintext highlighter-rouge">tests.py</code> file in the <code class="language-plaintext highlighter-rouge">backend/server/apps/accounts/</code> directory:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># backend/server/apps/accounts/tests.py </span> <span class="kn">from</span> <span class="nn">django.core</span> <span class="kn">import</span> <span class="n">mail</span> <span class="kn">from</span> <span class="nn">rest_framework</span> <span class="kn">import</span> <span class="n">status</span> <span class="kn">from</span> <span class="nn">rest_framework.test</span> <span class="kn">import</span> <span class="n">APITestCase</span> <span class="k">class</span> <span class="nc">EmailVerificationTest</span><span class="p">(</span><span class="n">APITestCase</span><span class="p">):</span> <span class="k">def</span> <span class="nf">test_register_with_email_verification</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">pass</span> </code></pre></div></div> <p>We are using <code class="language-plaintext highlighter-rouge">APITestCase</code> from the Django Rest Framework (see the <a href="https://www.django-rest-framework.org/api-guide/testing/">docs</a> for more details). To run this empty test, let’s run it in the <code class="language-plaintext highlighter-rouge">backend/server</code> directory:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./manage.py <span class="nb">test </span>apps </code></pre></div></div> <p>The expected output:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Creating test database for alias 'default'... System check identified no issues (0 silenced). . ---------------------------------------------------------------------- Ran 1 test in 0.001s OK Destroying test database for alias 'default'... </code></pre></div></div> <p>The first test will check registration with email activation before login.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># backend/server/apps/accounts/tests.py </span> <span class="kn">from</span> <span class="nn">django.core</span> <span class="kn">import</span> <span class="n">mail</span> <span class="kn">from</span> <span class="nn">rest_framework</span> <span class="kn">import</span> <span class="n">status</span> <span class="kn">from</span> <span class="nn">rest_framework.test</span> <span class="kn">import</span> <span class="n">APITestCase</span> <span class="k">class</span> <span class="nc">EmailVerificationTest</span><span class="p">(</span><span class="n">APITestCase</span><span class="p">):</span> <span class="c1"># endpoints needed </span> <span class="n">register_url</span> <span class="o">=</span> <span class="s">"/api/v1/users/"</span> <span class="n">activate_url</span> <span class="o">=</span> <span class="s">"/api/v1/users/activation/"</span> <span class="n">login_url</span> <span class="o">=</span> <span class="s">"/api/v1/token/login/"</span> <span class="n">user_details_url</span> <span class="o">=</span> <span class="s">"/api/v1/users/"</span> <span class="c1"># user infofmation </span> <span class="n">user_data</span> <span class="o">=</span> <span class="p">{</span> <span class="s">"email"</span><span class="p">:</span> <span class="s">"[email protected]"</span><span class="p">,</span> <span class="s">"username"</span><span class="p">:</span> <span class="s">"test_user"</span><span class="p">,</span> <span class="s">"password"</span><span class="p">:</span> <span class="s">"verysecret"</span> <span class="p">}</span> <span class="n">login_data</span> <span class="o">=</span> <span class="p">{</span> <span class="s">"email"</span><span class="p">:</span> <span class="s">"[email protected]"</span><span class="p">,</span> <span class="s">"password"</span><span class="p">:</span> <span class="s">"verysecret"</span> <span class="p">}</span> <span class="k">def</span> <span class="nf">test_register_with_email_verification</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="c1"># register the new user </span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">register_url</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">user_data</span><span class="p">,</span> <span class="nb">format</span><span class="o">=</span><span class="s">"json"</span><span class="p">)</span> <span class="c1"># expected response </span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_201_CREATED</span><span class="p">)</span> <span class="c1"># expected one email to be send </span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">mail</span><span class="p">.</span><span class="n">outbox</span><span class="p">),</span> <span class="mi">1</span><span class="p">)</span> <span class="c1"># parse email to get uid and token </span> <span class="n">email_lines</span> <span class="o">=</span> <span class="n">mail</span><span class="p">.</span><span class="n">outbox</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">body</span><span class="p">.</span><span class="n">splitlines</span><span class="p">()</span> <span class="c1"># you can print email to check it </span> <span class="c1"># print(mail.outbox[0].subject) </span> <span class="c1"># print(mail.outbox[0].body) </span> <span class="n">activation_link</span> <span class="o">=</span> <span class="p">[</span><span class="n">l</span> <span class="k">for</span> <span class="n">l</span> <span class="ow">in</span> <span class="n">email_lines</span> <span class="k">if</span> <span class="s">"/activate/"</span> <span class="ow">in</span> <span class="n">l</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span> <span class="n">uid</span><span class="p">,</span> <span class="n">token</span> <span class="o">=</span> <span class="n">activation_link</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">"/"</span><span class="p">)[</span><span class="o">-</span><span class="mi">2</span><span class="p">:]</span> <span class="c1"># verify email </span> <span class="n">data</span> <span class="o">=</span> <span class="p">{</span><span class="s">"uid"</span><span class="p">:</span> <span class="n">uid</span><span class="p">,</span> <span class="s">"token"</span><span class="p">:</span> <span class="n">token</span><span class="p">}</span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">activate_url</span><span class="p">,</span> <span class="n">data</span><span class="p">,</span> <span class="nb">format</span><span class="o">=</span><span class="s">"json"</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_204_NO_CONTENT</span><span class="p">)</span> <span class="c1"># login to get the authentication token </span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">login_url</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">login_data</span><span class="p">,</span> <span class="nb">format</span><span class="o">=</span><span class="s">"json"</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertTrue</span><span class="p">(</span><span class="s">"auth_token"</span> <span class="ow">in</span> <span class="n">response</span><span class="p">.</span><span class="n">json</span><span class="p">())</span> <span class="n">token</span> <span class="o">=</span> <span class="n">response</span><span class="p">.</span><span class="n">json</span><span class="p">()[</span><span class="s">"auth_token"</span><span class="p">]</span> <span class="c1"># set token in the header </span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">credentials</span><span class="p">(</span><span class="n">HTTP_AUTHORIZATION</span><span class="o">=</span><span class="s">'Token '</span> <span class="o">+</span> <span class="n">token</span><span class="p">)</span> <span class="c1"># get user details </span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">user_details_url</span><span class="p">,</span> <span class="nb">format</span><span class="o">=</span><span class="s">"json"</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_200_OK</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">json</span><span class="p">()),</span> <span class="mi">1</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">json</span><span class="p">()[</span><span class="mi">0</span><span class="p">][</span><span class="s">"email"</span><span class="p">],</span> <span class="bp">self</span><span class="p">.</span><span class="n">user_data</span><span class="p">[</span><span class="s">"email"</span><span class="p">])</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">json</span><span class="p">()[</span><span class="mi">0</span><span class="p">][</span><span class="s">"username"</span><span class="p">],</span> <span class="bp">self</span><span class="p">.</span><span class="n">user_data</span><span class="p">[</span><span class="s">"username"</span><span class="p">])</span> </code></pre></div></div> <p>The steps of the first test:</p> <ul> <li>create a new user,</li> <li>parse the verification email,</li> <li>activate the account by sending <code class="language-plaintext highlighter-rouge">token</code> and <code class="language-plaintext highlighter-rouge">uid</code> from the verification email,</li> <li>login to get <code class="language-plaintext highlighter-rouge">auth_token</code>,</li> <li>get user details.</li> </ul> <p>When you run the test the expected output is below:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;</span> ./manage.py <span class="nb">test </span>apps Creating <span class="nb">test </span>database <span class="k">for </span><span class="nb">alias</span> <span class="s1">'default'</span>... System check identified no issues <span class="o">(</span>0 silenced<span class="o">)</span><span class="nb">.</span> <span class="nb">.</span> <span class="nt">----------------------------------------------------------------------</span> Ran 1 <span class="nb">test </span><span class="k">in </span>0.330s OK Destroying <span class="nb">test </span>database <span class="k">for </span><span class="nb">alias</span> <span class="s1">'default'</span>... </code></pre></div></div> <p>Let’s add the next test with login without email verification and re-sending the verification email.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># backend/server/apps/accounts/tests.py </span> <span class="c1"># ... the rest of EmailVerificationTest code ... </span> <span class="k">def</span> <span class="nf">test_register_resend_verification</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="c1"># register the new user </span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">register_url</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">user_data</span><span class="p">,</span> <span class="nb">format</span><span class="o">=</span><span class="s">"json"</span><span class="p">)</span> <span class="c1"># expected response </span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_201_CREATED</span><span class="p">)</span> <span class="c1"># expected one email to be send </span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">mail</span><span class="p">.</span><span class="n">outbox</span><span class="p">),</span> <span class="mi">1</span><span class="p">)</span> <span class="c1"># login to get the authentication token </span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">login_url</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">login_data</span><span class="p">,</span> <span class="nb">format</span><span class="o">=</span><span class="s">"json"</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertTrue</span><span class="p">(</span><span class="s">"auth_token"</span> <span class="ow">in</span> <span class="n">response</span><span class="p">.</span><span class="n">json</span><span class="p">())</span> <span class="n">token</span> <span class="o">=</span> <span class="n">response</span><span class="p">.</span><span class="n">json</span><span class="p">()[</span><span class="s">"auth_token"</span><span class="p">]</span> <span class="c1"># set token in the header </span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">credentials</span><span class="p">(</span><span class="n">HTTP_AUTHORIZATION</span><span class="o">=</span><span class="s">'Token '</span> <span class="o">+</span> <span class="n">token</span><span class="p">)</span> <span class="c1"># try to get user details </span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">user_details_url</span><span class="p">,</span> <span class="nb">format</span><span class="o">=</span><span class="s">"json"</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_401_UNAUTHORIZED</span><span class="p">)</span> <span class="c1"># clear the auth_token in header </span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">credentials</span><span class="p">()</span> <span class="c1"># resend the verification email </span> <span class="n">data</span> <span class="o">=</span> <span class="p">{</span><span class="s">"email"</span><span class="p">:</span> <span class="bp">self</span><span class="p">.</span><span class="n">user_data</span><span class="p">[</span><span class="s">"email"</span><span class="p">]}</span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">resend_verification_url</span><span class="p">,</span> <span class="n">data</span><span class="p">,</span> <span class="nb">format</span><span class="o">=</span><span class="s">"json"</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_204_NO_CONTENT</span><span class="p">)</span> <span class="c1"># there should be two emails in the outbox </span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">mail</span><span class="p">.</span><span class="n">outbox</span><span class="p">),</span> <span class="mi">2</span><span class="p">)</span> <span class="c1"># parse the last email </span> <span class="n">email_lines</span> <span class="o">=</span> <span class="n">mail</span><span class="p">.</span><span class="n">outbox</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">body</span><span class="p">.</span><span class="n">splitlines</span><span class="p">()</span> <span class="n">activation_link</span> <span class="o">=</span> <span class="p">[</span><span class="n">l</span> <span class="k">for</span> <span class="n">l</span> <span class="ow">in</span> <span class="n">email_lines</span> <span class="k">if</span> <span class="s">"/activate/"</span> <span class="ow">in</span> <span class="n">l</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span> <span class="n">uid</span><span class="p">,</span> <span class="n">token</span> <span class="o">=</span> <span class="n">activation_link</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">"/"</span><span class="p">)[</span><span class="o">-</span><span class="mi">2</span><span class="p">:]</span> <span class="c1"># verify the email </span> <span class="n">data</span> <span class="o">=</span> <span class="p">{</span><span class="s">"uid"</span><span class="p">:</span> <span class="n">uid</span><span class="p">,</span> <span class="s">"token"</span><span class="p">:</span> <span class="n">token</span><span class="p">}</span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">activate_url</span><span class="p">,</span> <span class="n">data</span><span class="p">,</span> <span class="nb">format</span><span class="o">=</span><span class="s">"json"</span><span class="p">)</span> <span class="c1"># email verified </span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_204_NO_CONTENT</span><span class="p">)</span> </code></pre></div></div> <p>The steps of the second test:</p> <ul> <li>create a new user,</li> <li>log in the get <code class="language-plaintext highlighter-rouge">auth_token</code>,</li> <li>try to get user details and expect for <code class="language-plaintext highlighter-rouge">HTTP_401_UNAUTHORIZED</code>,</li> <li>resend the verification email,</li> <li>parse the last email to the <code class="language-plaintext highlighter-rouge">uid</code> and <code class="language-plaintext highlighter-rouge">token</code>,</li> <li>activate the account by verifying the email.</li> </ul> <p>I will additionally add two tests:</p> <ul> <li>to test the response for the wrong email address during re-sending the verification,</li> <li>to test activation with wrong <code class="language-plaintext highlighter-rouge">uid</code> and <code class="language-plaintext highlighter-rouge">token</code></li> </ul> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># backend/server/apps/accounts/tests.py </span> <span class="c1"># ... the rest of EmailVerificationTest code ... </span> <span class="k">def</span> <span class="nf">test_resend_verification_wrong_email</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="c1"># register the new user </span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">register_url</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">user_data</span><span class="p">,</span> <span class="nb">format</span><span class="o">=</span><span class="s">"json"</span><span class="p">)</span> <span class="c1"># expected response </span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_201_CREATED</span><span class="p">)</span> <span class="c1"># resend the verification email but with WRONG email </span> <span class="n">data</span> <span class="o">=</span> <span class="p">{</span><span class="s">"email"</span><span class="p">:</span> <span class="bp">self</span><span class="p">.</span><span class="n">user_data</span><span class="p">[</span><span class="s">"email"</span><span class="p">]</span><span class="o">+</span><span class="s">"_this_email_is_wrong"</span><span class="p">}</span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">resend_verification_url</span><span class="p">,</span> <span class="n">data</span><span class="p">,</span> <span class="nb">format</span><span class="o">=</span><span class="s">"json"</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_400_BAD_REQUEST</span><span class="p">)</span> <span class="k">def</span> <span class="nf">test_activate_with_wrong_uid_token</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="c1"># register the new user </span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">register_url</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">user_data</span><span class="p">,</span> <span class="nb">format</span><span class="o">=</span><span class="s">"json"</span><span class="p">)</span> <span class="c1"># expected response </span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_201_CREATED</span><span class="p">)</span> <span class="c1"># verify the email with wrong data </span> <span class="n">data</span> <span class="o">=</span> <span class="p">{</span><span class="s">"uid"</span><span class="p">:</span> <span class="s">"wrong-uid"</span><span class="p">,</span> <span class="s">"token"</span><span class="p">:</span> <span class="s">"wrong-token"</span><span class="p">}</span> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">activate_url</span><span class="p">,</span> <span class="n">data</span><span class="p">,</span> <span class="nb">format</span><span class="o">=</span><span class="s">"json"</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">status_code</span><span class="p">,</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_400_BAD_REQUEST</span><span class="p">)</span> </code></pre></div></div> <p>The expected output:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;</span> ./manage.py <span class="nb">test </span>apps Creating <span class="nb">test </span>database <span class="k">for </span><span class="nb">alias</span> <span class="s1">'default'</span>... System check identified no issues <span class="o">(</span>0 silenced<span class="o">)</span><span class="nb">.</span> .... <span class="nt">----------------------------------------------------------------------</span> Ran 4 tests <span class="k">in </span>0.963s OK Destroying <span class="nb">test </span>database <span class="k">for </span><span class="nb">alias</span> <span class="s1">'default'</span>... </code></pre></div></div> <h3 id="commit-your-changes">Commit your changes</h3> <p>Remember to commit all the code changes and add a new file:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git add apps/accounts/serializers.py git commit <span class="nt">-am</span> <span class="s2">"DRF email verification"</span> git push </code></pre></div></div> <hr /> <h2 id="summary">Summary</h2> <ul> <li>We wrote the backend for email verification with Django Rest Framework and Djoser.</li> <li>The new functionality has been tested, so we are confident that it works as expected.</li> <li>In the next post, we will write the user interface with React for email verification functionality.</li> </ul> <p>The code for this tutorial is available at <a href="https://github.com/saasitive/django-react-boilerplate">Github repository</a>.</p>Piotr PłońskiEmail verification is an important part of the SaaS application. We will contact the user by email in many cases: for a password reset, announcement of new features, or for sending the invoice. During registration, a user provides the email address. We need to check if the email belongs to the user and that there are no typos/errors in it. This can be easily done by automatically sending the verification email with an activation link. Such a link contains the unique token assigned to the user. After opening the activation link in the web browser, the request is sent to the web application (Django Rest Framework). The web server compares the token from the activation URL with the token stored in the database. If they are the same, the email address is verified.GDPR Overview2020-12-21T00:00:00+00:002020-12-21T00:00:00+00:00https://saasitive.com/tutorial/gdpr-overview<p>Equip your SaaS with legal bases so that the use of your website is safe for users and you. If you search for different sites, the Privacy Policy and Terms of Service differ from each other. The same with the cookies. Each piece of information has varying meanings for the website, and it concludes crucial statements. There’s no unique example for all websites because of the specification, the idea of webites, purpose, kind and other features, collected data.</p> <p>Usually, websites are available around the world, including Europe. Thus, you have to take care of your European users’ personal data. It is required by the law of the European Union, to be exact the Regulation (EU) 2016/679 (General Data Protection Regulation - GDPR). But it could seem challenging to differentiate such users by giving some of the privileges.</p> <p>Here’s the GDPR’s source https://eur-lex.europa.eu/eli/reg/2016/679/oj</p> <p>All these documents, amenities are nothing but SaaS and User Agreement where SaaS declares:</p> <p><em>to support some service described in Terms of Service and treat Users personal data with respect to GDPR what is mentioned in Privacy Policy,</em></p> <p>and User state that:</p> <p><em>he’s going to use your website, service according to Terms of service, and give necessary personal data, but you must use them as intended and provide adequate protection.</em></p> <p><img src="Agreement.png" alt="Agreement" /></p> <h2 id="where-to-start-with-gdpr">Where to start with GDPR</h2> <p>There’s no direct dictation to implement the Personal Data Protection Policy (mentioned in art. 24 GDPR). Still, it would be transparent to collect all rules and ways of handling data in one document. It will include a defined, very narrow catalog of mandatory documents: a register of processing activities, a record of categories of processing activities, documentation of personal data breaches, documentation related to the conduct of a data protection impact assessment, and prior consultations with the supervisory authority. After this document is ready, you can start creating your idea and figuring out how your project will affect personal data. The important stuff is to figure out the best security measures to keep your users safe.</p> <p>If you already prepared the Personal Data Protection Policy, you know that the next step (after creation) is to <strong>assess the risk</strong> for your users’ data by filling <strong>Data Protection Impact Assessment and Privacy by Design</strong>. Not every project needs to be checked by Data Protection Impact Assessment, each EU member state defines the catalog itself, but we recommend to do it for each project. Then you can start building your website.</p> <p><strong>Check the providers</strong> with whom you intend to conclude contracts, their privacy policy, the method of data protection, where the data is stored, whether in the EU or outside. Start to build <strong>Privacy Policy and Terms of Service</strong>. Check if all your documents and your SaaS contain key provisions and ensure that the principles of respecting users’ personal data are respected.</p> <p><img src="SaaS_way_to_comply_with_GDPR.png" alt="SaaS way to comply with GDPR" /></p> <p>You can check the source https://ec.europa.eu/justice/smedataprotect/index_en.htm where you can find some necessary information about GDPR for small businesses.</p> <h2 id="the-personal-data-protection-policy">The Personal Data Protection Policy</h2> <p>It will be the primary document that includes all GDPR requirements. Is it necessary? The provisions of the GDPR require the implementation of appropriate technical and organizational measures so that the data processing is compliant and can be demonstrated. It’s only an internal document which helps you to keep in order all requirements and duties. It is supposed to contain regulations about: processing of personal data, risk analysis, registers regarding the processing of personal data and records, proceedings in the event of a breach of personal data protection, the security of information systems.</p> <p><img src="Table_of_Contents.png" alt="Table of contents" /></p> <p>This document can be the basis for your workers to know how to deal with personal data. In the beginning, give some basic definitions to explain key terminology. List the rights of people, describe the role of Personal Data Protection Officer, IT System Administrator, other persons authorized to process data, and Administrator’s responsibilities. In any case, the crucial role and responsibility lie with the Administrator. Indicate the purposes and legal grounds for data processing, as (art. 6 GDPR):</p> <ul> <li>consent of the data subject,</li> <li>the necessity to perform the contract to which the data subject is a party,</li> <li>the need to fulfill the legal obligation incumbent on the Administrator,</li> <li>the legitimate interest of the Administrator.</li> </ul> <p>List how you collect data (direct contact by person, subscription, conclusion of a contract, hire an employee). What about the period of data processing and data retention? That’s important information you need to review. You can’t keep personal data too long. That’s the time limitations rule – you are obliged to store data in a form that allows the identification of persons to whom they relate, no longer than necessary to achieve the purpose of processing. Establish a time after which you remove all those data.</p> <p>Are you transferring data outside the European Economic Area (it includes processors who do it on your premise)? Write about it. The European Union has strict rules about taking care of personal data, but it’s not a priority everywhere.</p> <p>Under GDPR regulation, you should provide a register of personal data processing activities and a register of categories of processing activities if you:</p> <ul> <li>hire more than 250 employees;</li> <li>process data in a way that involves the risk of violating the rights or freedoms of data subjects;</li> <li>process data more often than sporadically;</li> <li>process information covering special categories of personal data;</li> <li>process personal data relating to criminal convictions and offenses.</li> </ul> <p><img src="Records_of_data_processing.jpg" alt="Records of Data Processing" /> (Picture from https://ec.europa.eu/justice/smedataprotect/index_en.htm)</p> <p>GDPR also requires to document or register personal data breaches.</p> <p>Finally, let’s go to System security. Describe how you protect data by using technical measures. Are you doing a backup? Do you have your own servers or use those in the cloud, using an antivirus program? Do you anonymize data? Reviewing the program architecture is key to organizing and checking data security.</p> <p>Here is some crucial thing which you should be aware of. You cannot describe the measures you implemented in every detail unless it could be <strong>a confidential attachment</strong> available only for a few people if needed!</p> <p><strong>Risk Analysis</strong></p> <p>Privacy by Design and Data Protection Impact Assessment. It’s mandatory to know where your users’ data goes to know the risk of processing. While creating a new project, you should implement presumption privacy by default. Every new project needs Privacy by Design evaluation. Some also require risk assessment by filling DPIA ( Data Protection Impact Assessment). It all helps to find vulnerabilities to support the best protection for data. DPIA is obligatory (which results from the GDPR and the European guidelines of the working party) whenever the processing is:</p> <ul> <li>Evaluation or scoring.</li> <li>Automated decision-making with legal or similarly significant effect.</li> <li>Systematic monitoring.</li> <li>Sensitive data or data of a highly personal nature.</li> <li>Data processed on a large scale.</li> <li>Matching or combining datasets.</li> <li>Data concerning vulnerable data subjects.</li> <li>Innovative use or applying new technological or organizational solutions.</li> <li>Preventing data subjects from exercising a right or using a service or contract.</li> </ul> <p><img src="DPIA.png" alt="DPIA" class="image-75width" /></p> <p><img src="Privacy_by_Design.png" alt="Privacy by Design" class="image-75width" /></p> <p>GDPR includes terminology without supporting definition. For example, high risk or large scale is relevant to describe.</p> <p><strong>High risk</strong> – a situation when DPIA is needed (in some cases, it must occur with other items to be mandatory for DPIA). EU member states will publish lists of processing types that require a DPIA in their jurisdiction, e.g., large-scale profiling, invisible processing, tracking, genetic data, biometrics, innovative technology, denial of service, data matching, targeting of children or other vulnerable individuals.</p> <p><strong>Innovative technology</strong> is processing involving innovation (as artificial intelligence, machine learning, and deep learning, market research involving neuro-measurement, internet of things applications) or the novel application of existing technologies. It’s all new developments in technological knowledge, including new ways of collecting and using data.</p> <p><strong>Large scale</strong> GDPR doesn’t define this but to decide if it’s large-scale processing or not, you should consider: the number of individuals concerned with the volume of data, the variety of data, the duration of the processing, and the geographical extent of the processing.</p> <p><strong>Vulnerable individuals</strong> - in this meaning, people who, because of their circumstances, are not aware of their data processing implications, or they can’t freely consent or reject that processing (e.g., children).</p> <p><strong>Invisible processing</strong> is a situation when you obtained data not directly from the individual, and you don’t in-form that person about processing his data. Processing is invisible because that person is unaware that you are collecting and using their personal data, even if you publish a privacy notice on your website. A DPIA is re-quired where this processing is combined with any of the criteria from the European guidelines.</p> <p><img src="GDPR_Explained.png" alt="GDPR Explained" /></p> <p>All contained in this article information is just an essence form how GDPR permeates every professional activi-ty. Although the intention to introduce the GDPR resulted from the need to protect natural persons, they im-pose additional entrepreneurs’ obligations. Anyone offering services should meet the basic requirements and take the user’s will as a benchmark.</p> <p><strong>Note from the author:</strong> Even though it contains a lot of practical information about legal stuff, you cannot treat it as legal advice. Each case is different and requires an individual approach. The purpose of this article is to help you to understand the essence of its subject.</p>Aleksandra PłońskaEquip your SaaS with legal bases so that the use of your website is safe for users and you. If you search for different sites, the Privacy Policy and Terms of Service differ from each other. The same with the cookies. Each piece of information has varying meanings for the website, and it concludes crucial statements. There’s no unique example for all websites because of the specification, the idea of webites, purpose, kind and other features, collected data.GDPR Meaning - The General Data Protection Regulation2020-12-19T00:00:00+00:002020-12-19T00:00:00+00:00https://saasitive.com/tutorial/gdpr-meaning<p>Earn users trust by respecting and securing their data. Since 2018 the European Union (EU) has imposed a new obligation on entrepreneurs to protect customers personal data. A duty for one another is a right, a tool to defend one’s rights. Each user should have the right to decide on providing information about himself, resign from using his data, delete his account. It’s a strong right that should be obeyed by everyone. Why? Because of substantial penalties, up to 20 mln euro! Be well prepared with Privacy Policy, Terms of Service, and GDPR requirements.</p> <p><img src="Flag_of_EU.png" alt="EU flag" class="image-50width" /></p> <h2 id="gdpr-meaning">GDPR meaning</h2> <p>The General Data Protection Regulation is an EU law regulation on data protection and privacy in the European Union and the European Economic Area (EEA). It also regulates how to proceed when personal data are transferred outside the EU and EEA areas.</p> <p><strong>Personal data is any information relating to an identified or identifiable person</strong>, also known as the data subject. Example of personal data:</p> <ul> <li>first name and last name,</li> <li>address,</li> <li>ID /Passport number,</li> <li>Income,</li> <li>cultural features,</li> <li>IP address.</li> </ul> <p>That law gives a strong right for people to manage their data. It protects us, physical people, before the abuse. It imposes an obligation on companies to ensure everyone the right to: get information about his data, obtain access to personal data, correct his data, erased data - to be forgotten, object to the processing of personal data for marketing purposes, request the restriction of the processing of personal data, data portability - receive personal data in a machine-readable format and send it to another controller, request that decisions based on automated processing concerning or significantly affecting the user and based on his data are made by natural persons, not only by computers and the right to be notified - if data has been a breach (a person should be informed within 72 hours of first having become aware of the violation). How to ensure that these rights are respected for SaaS users?</p> <p><img src="Facilities_for_User.png" alt="Facilities for users" /></p> <p>On the EU GDPR webpage (https://gdpr.eu/what-is-gdpr/), we can find this phrase:</p> <blockquote> <p><em>“(GDPR) is the toughest privacy and security law in the world.”</em></p> </blockquote> <p>And we fully agree. It is limited and requires small businesses to be fully committed to protecting user rights. On the other hand, there is a risk that profiling and making automated decisions on our behalf may limit our freedom.</p> <p>The GDPR provides broad protection of the individual’s rights while imposing several new obligations on entrepreneurs. Until now, the entities, confirming their due diligence, have applied for ISO certificates in the field of data security. The current regulations impose the obligation to ensure the processed data’s safety - on all entities dealing with personal data. One of these obligations is appointing a Personal Data Protection Inspector (DPO) in certain cases and keeping detailed documentation describing data processing.</p> <h2 id="who-the-gdpr-applies-to">Who the GDPR applies to?</h2> <p>GDPR relates to companies based in the European Union, citizens and people lived in the EU, and those who want to offer services for them. If you’re outside of the EU, but your customers are from the EEA (The EEA covers more countries than the EU itself), it refers to you as well.</p> <p>GDPR applies to every company, both sole proprietorships and companies - operating in the European Union, which processes personal data. It does not matter the nationality of the persons whose data is processed, where the processing takes place, or where the servers are located.</p> <p>Examples of entities covered by the GDPR:</p> <ul> <li>an entrepreneur with a headquarters outside the EU, but performing activities on its territory,</li> <li>entities that offer their services to clients outside the Union but have their offices in the Union,</li> <li>companies processing data via cloud computing - it does not matter where the servers are located,</li> <li>an entrepreneur who does not have organizational units in the EU but offers EU citizens goods and services (e.g., an online store).</li> </ul> <p><img src="RODO_stages.svg" alt="GDPR Implementation Stages" /></p> <p>The GDPR may apply to entities (controllers and processors) that do not have an organizational unit in the EU, also in the scope of the obligation to appoint a personal data protection officer, when the processing activities carried out by them are related to:</p> <ul> <li>offering goods or services to such data subjects in the EU, whether or not they are required to pay; or</li> <li>monitoring their behavior as far as this behavior occurs in the EU.</li> </ul> <h2 id="data-protection-officer-do-you-need-it">Data Protection Officer, do you need it?</h2> <p>When your company is established in the EU, you are obliged to appoint a DPO if:</p> <ol> <li> <p>You are a public authority or body and have appointed a DPO (except if you are a court acting in our judicial capacity).</p> </li> <li> <p>Your main activity is processing operations requiring regular and systematic monitoring of data subject on a large scale by their nature, scope, or purposes (e.g., you’re Google ;-)).</p> <p>Here it is crucial to explain “large-scale” meaning. The GDPR doesn’t define the concept of “large-scale” data processing. However, it is recommended to consider the following factors in order to determine whether large-scale processing takes place: the number of data subjects (a specific number or percentage of a particular group of the population), the scope of personal data processed, the period for which the data is processed, geographical scope of personal data processing. Thus, describing the process with this term is quite relative. The concept of “regular and systematic monitoring” of data subjects is also not defined in the GDPR. However, “monitoring of data subjects’ behavior” is mentioned in Recital 2415 and includes all online tracking and profiling forms, including for advertising purposes behavioral.</p> </li> <li> <p>Your main activity is the large-scale processing of special categories of personal data and personal data related to criminal convictions and offenses.</p> <p>Now, what does it mean “special categories of personal data”? Data of a special category can be included revealing racial or ethnic origin, political opinions, religious or philosophical beliefs, trade union membership, and the processing of genetic data, biometric data allowing the identification of a natural person.</p> </li> </ol> <p>As mentioned above, if your company is not an EU resident and your clients are EU residents, you should (as the data controller) have a representative registered in the EU. According to its competence, the DPO appointment should be notified to the relevant Member State’s supervisory authority.</p> <p><img src="DPO_obligation.svg" alt="Do you need a DPO?" /></p> <p>The DPO should:</p> <ul> <li>be aware of national and European data protection laws;</li> <li>be familiar with the practices in the field of personal data protection;</li> <li>have business and industry knowledge regarding the administrator’s activities;</li> <li>know the data processing processes,</li> <li>know the information systems and security measures used by the controller and its data protection needs;</li> <li>demonstrate knowledge of the entity’s administrative procedures and operations.</li> </ul> <p>The inspector is, therefore, to play a crucial role in supporting the “data protection culture” and help in the implementation of the necessary elements of the GDPR, i.e.:</p> <ul> <li>rules for the processing of personal data;</li> <li>the rights of data subjects;</li> <li>data protection by design and data protection by default;</li> <li>keep a register of processing activities;</li> <li>processing security requirements;</li> <li>reporting violations.</li> </ul> <p>DPO suppose to be easily accessible as a point of contact for employees, individuals, and Information Commissioner’s Officer (ICO). His contact details should be published on your website. If you are not obliged to appoint a DPO, you can designate a person who will track the newest trends, UE recommendations and keep a hand on your company’s personal data security.</p> <p><strong>Note from the author:</strong></p> <p>Even though it contains a lot of practical information about legal stuff, you cannot treat it as legal advice. Each case is different and requires an individual approach. The purpose of this article is to help you to understand the essence of its subject.</p>Aleksandra PłońskaEarn users trust by respecting and securing their data. Since 2018 the European Union (EU) has imposed a new obligation on entrepreneurs to protect customers personal data. A duty for one another is a right, a tool to defend one’s rights. Each user should have the right to decide on providing information about himself, resign from using his data, delete his account. It’s a strong right that should be obeyed by everyone. Why? Because of substantial penalties, up to 20 mln euro! Be well prepared with Privacy Policy, Terms of Service, and GDPR requirements.Visual Identity for SaaS2020-11-12T00:00:00+00:002020-11-12T00:00:00+00:00https://saasitive.com/tutorial/visual-identity-for-saas<p>Are you going to start your business as a SaaS? Have you got an idea and now you want to settle up your website, but you’re not a graphic designer? Start here by creating your Visual Identity Book.</p> <p>Why it is so useful? It will help you to smooth your website, make your SaaS recognizable, will be handy for preparing presentations, advertising on the Internet, or social media e.g. Facebook, Instagram, Twitter. Would it be comfortable to keep all branding graphics in one place? For sure, but is it not a waste of time?</p> <p>Too many questions above. Now let’s make some answers. Visual identification is the basic tool for creating the company image on the market. You may view thousands of websites and some stayed in your mind. That means the graphic designers done a good job. There are some rules by which you can create a consistent look for a brand. Basic visual identity consists of features like: logo, colors, font, icons, and pictures and all these elements give a consistent look to the website. Collect them in one place called Visual Identity Book. This book will save you a lot of time, and your co-workers will know how to properly engage in building a website and strengthen your brand recognition. All this will ensure that the prepared content (and tutorials, advertisements, graphics, charts) will inspire trust, positive perception of users, and the message will be uniform.</p> <p><img src="elements_of_visual_identity.png" alt="Elements of Visual Identity" /></p> <p>You can achieve effective visual identification on your website with these steps:</p> <h3 id="creative-work">Creative work</h3> <p>Some creative work to do. Try to imagine your service with a miniature or with one tool, thing, object. Scrawl a few pages to find that one you will identify (including an abstract spot) or… look for some inspirations on the Internet. The best way is to prepare your logo in vector so you can use it in many materials, advertisement, website. You just can scale it, vector graphics need less space for storage. It will make website loading faster. Search similar projects, compare or try logo generators e.g. (but for most of them you have to pay). An easy way is to use a free, unusual font and give the name of your business. An example is <a href="https://saasitive.com">saasitive.com</a>, where we used two different fonts:</p> <p><img src="logo_saasitive.png" alt="Saasitive logo" /></p> <h3 id="colors">Colors</h3> <p>Think about colors. For sure You will do some labels, templates, differ parts of your website, icons, buttons. Use a color palette to chose a few of them and to be sure it won’t be too radical or too mismatched. If you do not feel confident in choosing colors, use ready-made color palettes (take a look at <a href="https://coolors.co/">Coolors</a> where you can inspire). The aesthetic selection of matched colors to the website and its content can create a good impression and a sense of polished appearance. When mixing colors, designers use the 60-30-10 rule, which means that 60% fills up main color, 30% the second and 10% the third color, used mainly to emphasize and highlight important elements. This is only a suggestion, but it can prove useful if you want a colorful page while still being readable and clear.</p> <p>For our website we chose:</p> <p><img src="color_palette.jpg" alt="Color_Palette" class="image-50width" /></p> <p>Psychology of the colors has a big meaning. The most preferable color by woman and man is blue while disliked color is brown and orange. Men prefer saturated, darker colors, they also accept gray tones. Women like lighter and less saturated colors, gray is not their favorite shade.</p> <p>Take a look at the graph below it shows the meaning of colors. As you can notice each color gives a variety of impression and can be addressed to a different group of people (from https://www.firstdesign.eu/the-meaning-of-colors-in-design ):</p> <p><img src="color_meaning.jpg" alt="Color_Meaning" /></p> <h3 id="font">Font</h3> <p>Find a font that suits your website. Pick at most 3 fonts. You can use only one font in different sizes and types (bold, italic). If you need special signs check it out whether font provides them. Font color is also important. Usually, we use black but for example, headings can be in one of your chosen colors suitable for the logo.</p> <p>Aa Bb Cc Dd Ee Ff Gg Hh Ii Jj Kk Ll Mm Nn Oo Pp Qq Rr Ss Tt Uu Ww Xx Yy Zz 1234567890!@#$%^&amp;*()&lt;&gt;&lt;&gt;?|\,./</p> <h3 id="icons">Icons</h3> <p>Choose icons that can describe features of your SAAS. The best is to use a wide set of uniform style icon where you have a lot of variety to choose from. If you choose colored icons remember about the colors of your brand. You can check <a href="https://uxwing.com/">Uxwing</a> , <a href="https://aiconica.net/">aiconica</a>, <a href="https://www.iconfinder.com/">Iconfinder</a>, or <a href="https://www.flaticon.com/">Flaticon</a>. Remember that each icon has its license to use so read it to know how to use it for free. Sometimes it’s better to pay (the cost is not that big) to have a clear conscience and use a whole set of icons. Some platforms don’t require attribution. Examples of icons monochromatic: <img src="icons.png" alt="Icon" /></p> <h3 id="pictures">Pictures</h3> <p>Pictures used on the website should correspond with the content on the website and icons (if you chose monochromatic icons pictures can bring some pleasant impression). There’s a lot of stock of photos you can choose from. You have to find out what will arouse the curiosity of the user and will be a sneak peek of your article, tutorial, essence feature you describe, or action you encourage users to do. You can check <a href="https://undraw.co/illustrations">Undraw</a> where you can get nice vector pics and change color so it will suit your website. Example from <a href="https://saasitive.com">SaaSitive</a>:</p> <p><img src="pictures_saasitive.png" alt="Pictures_Saasitive" /></p> <h3 id="the-devil-is-in-the-details">The devil is in the details.</h3> <p>Prepare a <strong>favicon</strong>. Don’t forget about it, it’s a little stamp on your website placed in the browser. Use the first letter from your brand name, logo, or a sign which will be memorable. That’s a little detail but necessary! If your users have a few pages opened in your browser they will find your webpage with this tiny sign. It needs to be square 16x16 pixels, but you can start from 64x64, and after it’s ready then resize it.</p> <p>You can just try to use generators like <a href="https://formito.com/tools/favicon">Formito</a>.</p> <p><img src="favicon.png" alt="Favicon" class="image-25width" /></p> <p>Want to share your website on social media? You better prepare a <strong>metaimage</strong>. It consists of an image with a title, a short description, and the domain name. It should have 1200x630 px. You can check how your graphic will look at <a href="https://metatags.io/">Metatags</a>. Let your users share articles, tutorials from your website by giving a link with an expressive picture. It could looks like this:</p> <p><img src="metaimage.png" alt="MetaImage" class="image-75width image-border" /></p> <p>To be accurate prepare <strong>footer to e-mails</strong> which will be sent automatically. Use the chosen font and logo so users remember your visual identity. Include the address of your website, e-mail address, and other important information.</p> <p>All adverts, banners and graphics you can do with well prepared elements above.</p> <h2 id="summary">Summary</h2> <p>Preparing visual identification is a process in which you create a clever brand image. Using all hints above will help your SAAS to stand out of the competition and give a consistency in communication. Besides your website will give strong impression and build trust of the users and attract them.</p> <p><img src="Clever_Brand.png" alt="Clever_Brand" /></p> <p>Leading an internet business mainly based on advertisement, social media, GitHub, StackOverflow, and others. To advertise you need ready visual identification. Logo starts to be recognizable if you use it to promote, advertise your SaaS. First of all, you need to gather this information in one place to find it quickly. Create a folder and put all items listed below in it.</p> <p>The minimum what your <strong>Visual Identity Book</strong> needs:</p> <ul> <li>company symbols - logo, logotype, decorative symbols (if it’s possible to try to make it also in black-white colors, in many combinations vertical, horizontal, and in few different sizes also keep the most used logo in different formats e.g. pdf, jpg, SVG);</li> <li>colors (describe colors in many options: RGB, CMYK and Hex Code, HTML - to find it use e.g. <a href="https://www.rapidtables.com/web/color/RGB_Color.html">Rapidtables</a> );</li> <li>company typography - font type and size;</li> <li>icons or pictures you will dress features, functions, or descriptions, or the content.</li> </ul> <p>The rest you will compile with elements upstream.</p> <p>You can check Visual Identity Book of SaaSitive in our <a href="https://github.com/saasitive/visual-identity-book">repository</a>. Let us know if it was helpful.</p>Aleksandra PłońskaAre you going to start your business as a SaaS? Have you got an idea and now you want to settle up your website, but you’re not a graphic designer? Start here by creating your Visual Identity Book.How to generate Django Secret Key?2020-11-10T00:00:00+00:002020-11-10T00:00:00+00:00https://saasitive.com/tutorial/generate-django-secret-key<p><img src="/tutorial/images/banner-django-secret-key.jpg" alt="How to generate Django Secret Key?" /> Have you ever pushed to the repository a Django project with <code class="language-plaintext highlighter-rouge">SECRET_KEY</code>? Ups, it happens to me very often. Don’t worry. This can be easily fixed.</p> <p>The <a href="https://docs.djangoproject.com/en/3.1/ref/settings/#std:setting-SECRET_KEY"><code class="language-plaintext highlighter-rouge">SECRET_KEY</code></a> is used in Django for <a href="https://docs.djangoproject.com/en/3.1/topics/signing/">cryptographic signing</a>. It is used to generate tokens and hashes. If somebody will have your <code class="language-plaintext highlighter-rouge">SECRET_KEY</code> he can recreate your tokens.</p> <p>Storing <code class="language-plaintext highlighter-rouge">SECRET_KEY</code> in the repository code is not secure. It should be removed from the code and loaded from environment variables (or some configuration). You can use for example <a href="https://github.com/henriquebastos/python-decouple">python-decouple</a> to separate configuration variables from the code.</p> <p>Once <code class="language-plaintext highlighter-rouge">SECRET_KEY</code> was committed into the code repository, it needs to be generated again. Luckily, we can use Django <code class="language-plaintext highlighter-rouge">get_random_secret_key()</code> function to generate new <code class="language-plaintext highlighter-rouge">SECRET_KEY</code>.</p> <div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">django.core.management.utils</span> <span class="kn">import</span> <span class="n">get_random_secret_key</span> <span class="c1"># print new random secret key </span><span class="k">print</span><span class="p">(</span><span class="n">get_random_secret_key</span><span class="p">())</span> </code></pre></div></div> <p>This code can be run in the terminal as a command:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python <span class="nt">-c</span> <span class="s1">'from django.core.management.utils import get_random_secret_key; \ print(get_random_secret_key())'</span> </code></pre></div></div> <p>If you have new <code class="language-plaintext highlighter-rouge">SECRET_KEY</code> then you can use <code class="language-plaintext highlighter-rouge">python-decouple</code>.</p> <div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># in your settings.py file </span> <span class="kn">from</span> <span class="nn">decouple</span> <span class="kn">import</span> <span class="n">config</span> <span class="n">SECRET_KEY</span> <span class="o">=</span> <span class="n">config</span><span class="p">(</span><span class="s">"SECRET_KEY"</span><span class="p">)</span> </code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">SECRET_KEY</code> can be then set as environment variable or can be saved in <code class="language-plaintext highlighter-rouge">.env</code> file (which is not tracked in the code repository).</p>Piotr PłońskiHave you ever pushed to the repository a Django project with SECRET_KEY? Ups, it happens to me very often. Don’t worry. This can be easily fixed.Docker-Compose for Django and React with Nginx reverse-proxy and Let’s encrypt certificate2020-10-30T00:00:00+00:002020-10-30T00:00:00+00:00https://saasitive.com/tutorial/docker-compose-django-react-nginx-let-s-encrypt<p><img src="/tutorial/docker-compose-django-react-nginx-let-s-encrypt/banner.jpg" alt="Docker-Compose for Django and React with Nginx reverse-proxy and Let's encrypt certificate" /> The most exciting moment of the web application development is a deployment. Your app is going live! It can also be nerve-wracking moment. Unfortunately. There are many options, many variables and configurations. It is easy to miss something … In this article, I will show you how to pack Django and React application into containers and deploy them with <code class="language-plaintext highlighter-rouge">docker-compose</code>. The presented approach can be reused on any Cloud Provider (AWS, DigitalOcean, Linode, GCP, Heroku) - you just need a Virtual Private Server (VPS).</p> <p>In this article:</p> <ul> <li>We will create two <code class="language-plaintext highlighter-rouge">docker-compose</code> configuration files. One for development (easier version) and one for production (with SSL certificate from <a href="https://letsencrypt.org/">Let’s Encrypt</a>).</li> <li>The React static files will be served by <code class="language-plaintext highlighter-rouge">nginx</code>.</li> <li>The Django static files (from admin and DRF browsable API) will be served by <code class="language-plaintext highlighter-rouge">nginx</code>.</li> <li>The <code class="language-plaintext highlighter-rouge">nginx</code> will be reverse-proxy to the Django server (<code class="language-plaintext highlighter-rouge">gunicorn</code>).</li> <li>In the production, we will add <a href="https://certbot.eff.org/"><code class="language-plaintext highlighter-rouge">certbot</code></a> to renew the certificate. To issue a certificate we will use a bash script. You need to have a domain to issue the certificate .</li> </ul> <p>We will be using code from the previous article: <a href="https://saasitive.com/tutorial/crud-django-rest-framework-react/">CRUD in Django Rest Framework and React</a> (code with <a href="https://github.com/saasitive/django-react-boilerplate/tree/v6">tag v6</a>).</p> <h2 id="create-dockerfile-for-django-and-nginx">Create Dockerfile for Django and Nginx</h2> <p>Let’s start by adding a new directory <code class="language-plaintext highlighter-rouge">docker</code> in the main directory of the project. Your project structure should look like below:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">.</span> ├── backend ├── docker ├── frontend ├── LICENSE └── README.md </code></pre></div></div> <p>In the <code class="language-plaintext highlighter-rouge">docker</code> directory please add <code class="language-plaintext highlighter-rouge">backend</code> directory with two files:</p> <ul> <li><code class="language-plaintext highlighter-rouge">docker/backend/Dockerfile</code> - it will define how to build Django container,</li> <li><code class="language-plaintext highlighter-rouge">docker/backend/wsgi-entrypoint.sh</code> - it will be entrypoint for Django container. It will apply migrations, collect static files and run WSGI server with <a href="https://gunicorn.org/"><code class="language-plaintext highlighter-rouge">gunicorn</code></a>.</li> </ul> <h3 id="django-dockerfile">Django Dockerfile</h3> <p>The <code class="language-plaintext highlighter-rouge">Dockerfile</code> to build Django container:</p> <div class="language-docker highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># docker/backend/Dockerfile</span> <span class="k">FROM</span><span class="s"> python:3.8.3-alpine</span> <span class="k">WORKDIR</span><span class="s"> /app</span> <span class="k">ADD</span><span class="s"> ./backend/requirements.txt /app/backend/</span> <span class="k">RUN </span>pip <span class="nb">install</span> <span class="nt">--upgrade</span> pip <span class="k">RUN </span>pip <span class="nb">install </span>gunicorn <span class="k">RUN </span>pip <span class="nb">install</span> <span class="nt">-r</span> backend/requirements.txt <span class="k">ADD</span><span class="s"> ./docker /app/docker</span> <span class="k">ADD</span><span class="s"> ./backend /app/backend</span> </code></pre></div></div> <p>In this <code class="language-plaintext highlighter-rouge">Dockerfile</code> the <code class="language-plaintext highlighter-rouge">python:3.8.3-alpine</code> is used as base image. Let’s decode this image name and tag. It contains python in version <code class="language-plaintext highlighter-rouge">3.8.3</code>, running in the Linux distribution <a href="https://alpinelinux.org/"><code class="language-plaintext highlighter-rouge">Alpine</code></a> which is lightweight. The purpose of using lightweight Linux is to have small image which will result in faster builds. You can of course use different base image. For purpose of this tutorial the <code class="language-plaintext highlighter-rouge">python:3.8.3-alpine</code> is sufficient.</p> <p>We create <code class="language-plaintext highlighter-rouge">/app</code> directory in the container and copy <code class="language-plaintext highlighter-rouge">/backend/requirements.txt</code>. Then, <code class="language-plaintext highlighter-rouge">gunicorn</code> and all needed packages are installed. The backend source code is copied at the end of the container build. It is on purpose. It makes building faster. When you change something in the code (without changing the <code class="language-plaintext highlighter-rouge">requirements.txt</code> file), only the last line of the <code class="language-plaintext highlighter-rouge">Dockerfile</code> will be executed and the rest will be read from cache (of course if available).</p> <p>The <code class="language-plaintext highlighter-rouge">Dockerfile</code> is not executing any command, we will use <code class="language-plaintext highlighter-rouge">wsgi-entrypoint.sh</code> script for this purpose.</p> <h3 id="docker-entrypoint">Docker entrypoint</h3> <p>Please add a new file <code class="language-plaintext highlighter-rouge">docker/backend/wsgi-entrypoint.sh</code> and add execute rights to the file (it is a bash script):</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># add execute rights</span> <span class="nb">chmod</span> +x docker/backend/wsgi-entrypoint.sh </code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">docker/backend/wsgi-entrypoint.sh</code> file:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/sh</span> <span class="k">until </span><span class="nb">cd</span> /app/backend/server <span class="k">do </span><span class="nb">echo</span> <span class="s2">"Waiting for server volume..."</span> <span class="k">done until</span> ./manage.py migrate <span class="k">do </span><span class="nb">echo</span> <span class="s2">"Waiting for db to be ready..."</span> <span class="nb">sleep </span>2 <span class="k">done</span> ./manage.py collectstatic <span class="nt">--noinput</span> gunicorn server.wsgi <span class="nt">--bind</span> 0.0.0.0:8000 <span class="nt">--workers</span> 4 <span class="nt">--threads</span> 4 <span class="c">#####################################################################################</span> <span class="c"># Options to DEBUG Django server</span> <span class="c"># Optional commands to replace abouve gunicorn command</span> <span class="c"># Option 1:</span> <span class="c"># run gunicorn with debug log level</span> <span class="c"># gunicorn server.wsgi --bind 0.0.0.0:8000 --workers 1 --threads 1 --log-level debug</span> <span class="c"># Option 2:</span> <span class="c"># run development server</span> <span class="c"># DEBUG=True ./manage.py runserver 0.0.0.0:8000</span> </code></pre></div></div> <p>The above script is waiting till serever volume and database is ready. It runs migration on database and collect static files. It runs <code class="language-plaintext highlighter-rouge">gunicorn</code> server with IP address <code class="language-plaintext highlighter-rouge">0.0.0.0</code> and (docker default IP) and port <code class="language-plaintext highlighter-rouge">8000</code>.</p> <p>The <code class="language-plaintext highlighter-rouge">gunicorn</code> is running <code class="language-plaintext highlighter-rouge">4</code> workers with <code class="language-plaintext highlighter-rouge">4</code> threads each. You can set different numbers here. It depends on your machine. There are some heuristic rules that set number of workers as <code class="language-plaintext highlighter-rouge">4 * CPU cores</code> (see the <a href="https://docs.gunicorn.org/en/stable/settings.html#workers">gunicorn docs</a>). If you are deploying application to machine with 1 CPU core, then you can use <code class="language-plaintext highlighter-rouge">4</code> workers. (Remember, it is just heuristic not an exact rule). Then you can specify how many threads will be running in each worker. In our case, there are <code class="language-plaintext highlighter-rouge">4</code> threads (<a href="https://docs.gunicorn.org/en/stable/settings.html#threads">gunicorn docs</a>). This means that we can process <code class="language-plaintext highlighter-rouge">4 * 4 = 16</code> concurrent requests.</p> <p><strong>1st Note:</strong> We are using <a href="https://sqlite.org">SQLite</a> for our development - it doesn’t support concurrency. We will need to replace SQLite with advanced database engine like <a href="https://www.postgresql.org/">PostgreSQL</a>. I will replace database at the end of this tutorial to make final deployment. I think it is good to deploy application. We will practice deployment and code updates in the production. That’s why we are here.</p> <p><strong>2nd Note:</strong> One more thing, I added other options to run Django server in <code class="language-plaintext highlighter-rouge">wsgi-entrypoint.sh</code> which are commmented. Why? You might need them for debugging. The first option add logging to the console with debug level and run <code class="language-plaintext highlighter-rouge">gunicorn</code> with one worker and single thread. The second option runs Django development server with <code class="language-plaintext highlighter-rouge">DEBUG=True</code>. It needs setting environment variable and load it in <code class="language-plaintext highlighter-rouge">settings.py</code> with <a href="https://github.com/henriquebastos/python-decouple"><code class="language-plaintext highlighter-rouge">python-decouple</code></a> (for example), I will write about this in the future post.</p> <h4 id="django-static-files">Django static files</h4> <p>Before going further, let’s update <code class="language-plaintext highlighter-rouge">STATIC_URL</code> variable in the <code class="language-plaintext highlighter-rouge">backend/server/server/settings.py</code>:</p> <div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># backend/server/server/settings.py </span> <span class="n">MEDIA_URL</span> <span class="o">=</span> <span class="s">'/media/'</span> <span class="n">STATIC_URL</span> <span class="o">=</span> <span class="s">'/django_static/'</span> <span class="n">STATIC_ROOT</span> <span class="o">=</span> <span class="n">BASE_DIR</span> <span class="o">/</span> <span class="s">'django_static'</span> </code></pre></div></div> <p>We overwrite <code class="language-plaintext highlighter-rouge">STATIC_URL</code> with a new value. All static files will be served with <code class="language-plaintext highlighter-rouge">/django_static/</code> in the URL. We add two new variables:</p> <ul> <li><code class="language-plaintext highlighter-rouge">MEDIA_URL</code> - it is URL for serving files uploaded to Django application, we will use it in future posts.</li> <li><code class="language-plaintext highlighter-rouge">STATIC_ROOT</code> - it is a directory where static files from Django application will be stored after running <code class="language-plaintext highlighter-rouge">collectstatic</code> command. The <code class="language-plaintext highlighter-rouge">nginx</code> will point to this path.</li> </ul> <h3 id="nginx-dockerfile-and-configuration">Nginx Dockerfile and Configuration</h3> <p>Please add <code class="language-plaintext highlighter-rouge">nginx</code> directory in the <code class="language-plaintext highlighter-rouge">docker</code> directory. There will be added three files:</p> <ul> <li><code class="language-plaintext highlighter-rouge">docker/nginx/Dockerfile</code> - the instructions how to build <code class="language-plaintext highlighter-rouge">nginx</code> container image,</li> <li><code class="language-plaintext highlighter-rouge">docker/nginx/development/default.conf</code> - the configuration file for <code class="language-plaintext highlighter-rouge">nginx</code> server used in the development,</li> <li><code class="language-plaintext highlighter-rouge">docker/nginx/production/default.conf</code> - the configuration file for the production.</li> </ul> <p>The <code class="language-plaintext highlighter-rouge">Dockerfile</code> for <code class="language-plaintext highlighter-rouge">nginx</code> container will use two stage build.</p> <ul> <li>At first stage, we build the React static files.</li> <li>At second stage, the static files are copied and <code class="language-plaintext highlighter-rouge">nginx</code> server starts. We copy React static files into <code class="language-plaintext highlighter-rouge">/usr/share/nginx/html</code> directory.</li> </ul> <p>The <code class="language-plaintext highlighter-rouge">docker/nginx/Dockerfile</code> file:</p> <div class="language-docker highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># The first stage</span> <span class="c"># Build React static files</span> <span class="k">FROM</span><span class="s"> node:13.12.0-alpine as build</span> <span class="k">WORKDIR</span><span class="s"> /app/frontend</span> <span class="k">COPY</span><span class="s"> ./frontend/package.json ./</span> <span class="k">COPY</span><span class="s"> ./frontend/package-lock.json ./</span> <span class="k">RUN </span>npm ci <span class="nt">--silent</span> <span class="k">COPY</span><span class="s"> ./frontend/ ./</span> <span class="k">RUN </span>npm run build <span class="c"># The second stage</span> <span class="c"># Copy React static files and start nginx</span> <span class="k">FROM</span><span class="s"> nginx:stable-alpine</span> <span class="k">COPY</span><span class="s"> --from=build /app/frontend/build /usr/share/nginx/html</span> <span class="k">CMD</span><span class="s"> ["nginx", "-g", "daemon off;"]</span> </code></pre></div></div> <p>At the second stage, we copy only the final product of the first stage: static files. The <code class="language-plaintext highlighter-rouge">node_modules</code> (they can take a lot of space) with dependency files are not in the final image. Such two stage-build makes the size of the final image smaller.</p> <p>Let’s add <code class="language-plaintext highlighter-rouge">nginx</code> configuration for development version of <code class="language-plaintext highlighter-rouge">docker-compose</code> (without SSL certificate):</p> <div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">server</span> <span class="p">{</span> <span class="kn">listen</span> <span class="mi">80</span><span class="p">;</span> <span class="kn">server_name</span> <span class="s">_</span><span class="p">;</span> <span class="kn">server_tokens</span> <span class="no">off</span><span class="p">;</span> <span class="kn">client_max_body_size</span> <span class="mi">20M</span><span class="p">;</span> <span class="kn">location</span> <span class="n">/</span> <span class="p">{</span> <span class="kn">root</span> <span class="n">/usr/share/nginx/html</span><span class="p">;</span> <span class="kn">index</span> <span class="s">index.html</span> <span class="s">index.htm</span><span class="p">;</span> <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="nv">$uri</span><span class="n">/</span> <span class="n">/index.html</span><span class="p">;</span> <span class="p">}</span> <span class="kn">location</span> <span class="n">/api</span> <span class="p">{</span> <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="s">@proxy_api</span><span class="p">;</span> <span class="p">}</span> <span class="kn">location</span> <span class="n">/admin</span> <span class="p">{</span> <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="s">@proxy_api</span><span class="p">;</span> <span class="p">}</span> <span class="kn">location</span> <span class="s">@proxy_api</span> <span class="p">{</span> <span class="kn">proxy_set_header</span> <span class="s">X-Forwarded-Proto</span> <span class="s">https</span><span class="p">;</span> <span class="kn">proxy_set_header</span> <span class="s">X-Url-Scheme</span> <span class="nv">$scheme</span><span class="p">;</span> <span class="kn">proxy_set_header</span> <span class="s">X-Forwarded-For</span> <span class="nv">$proxy_add_x_forwarded_for</span><span class="p">;</span> <span class="kn">proxy_set_header</span> <span class="s">Host</span> <span class="nv">$http_host</span><span class="p">;</span> <span class="kn">proxy_redirect</span> <span class="no">off</span><span class="p">;</span> <span class="kn">proxy_pass</span> <span class="s">http://backend:8000</span><span class="p">;</span> <span class="p">}</span> <span class="kn">location</span> <span class="n">/django_static/</span> <span class="p">{</span> <span class="kn">autoindex</span> <span class="no">on</span><span class="p">;</span> <span class="kn">alias</span> <span class="n">/app/backend/server/django_static/</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <blockquote> <p>What do you feel when you see <code class="language-plaintext highlighter-rouge">nginx</code> configuration file?</p> </blockquote> <p>No troubles! The <code class="language-plaintext highlighter-rouge">nginx</code> is here to help us. It will do what we will ask in the configuration.</p> <div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">server</span> <span class="p">{</span> <span class="kn">listen</span> <span class="mi">80</span><span class="p">;</span> <span class="kn">server_name</span> <span class="s">_</span><span class="p">;</span> <span class="kn">server_tokens</span> <span class="no">off</span><span class="p">;</span> <span class="kn">client_max_body_size</span> <span class="mi">20M</span><span class="p">;</span> </code></pre></div></div> <p>At the begining of the configuration we define a server (<code class="language-plaintext highlighter-rouge">server</code> keyword). It will listen on port <code class="language-plaintext highlighter-rouge">80</code> (it is a default <code class="language-plaintext highlighter-rouge">HTTP</code> port). There is no name assigned to the server (<code class="language-plaintext highlighter-rouge">server_name _;</code>). We switch off option to show server version on error pages (<code class="language-plaintext highlighter-rouge">server_token off;</code> <a href="http://nginx.org/en/docs/http/ngx_http_core_module.html#server_tokens">see docs</a>). The last setting is to set maximum request size (<a href="http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size">see docs</a>) (<code class="language-plaintext highlighter-rouge">client_max_body_size 20M</code>). It means that requests larger than 20MB will result in error with HTTP 413 (Request Entity Too Large).</p> <p>We have five <code class="language-plaintext highlighter-rouge">location</code> blocks (<a href="http://nginx.org/en/docs/http/ngx_http_core_module.html#location">location docs</a>) in the config which specify configuration for each URL (routing for requests).</p> <p>The first <code class="language-plaintext highlighter-rouge">location /</code> defines what to do if the request comes from <code class="language-plaintext highlighter-rouge">/</code> (main domain or IP). The <code class="language-plaintext highlighter-rouge">index.html</code> file from <code class="language-plaintext highlighter-rouge">/usr/share/nginx/html</code> will be served as a response. It is our React static <code class="language-plaintext highlighter-rouge">index.html</code> file.</p> <div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">location</span> <span class="n">/</span> <span class="p">{</span> <span class="kn">root</span> <span class="n">/usr/share/nginx/html</span><span class="p">;</span> <span class="kn">index</span> <span class="s">index.html</span> <span class="s">index.htm</span><span class="p">;</span> <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="nv">$uri</span><span class="n">/</span> <span class="n">/index.html</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>The second and third locations <code class="language-plaintext highlighter-rouge">location /api</code> and <code class="language-plaintext highlighter-rouge">location /admin</code> redirect requests to <code class="language-plaintext highlighter-rouge">location @proxy_api</code>. It is a reverse-proxy:</p> <div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">location</span> <span class="n">/api</span> <span class="p">{</span> <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="s">@proxy_api</span><span class="p">;</span> <span class="p">}</span> <span class="k">location</span> <span class="n">/admin</span> <span class="p">{</span> <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="s">@proxy_api</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>What we have in fourth <code class="language-plaintext highlighter-rouge">location @proxy_api</code>? It is our Django application served with <code class="language-plaintext highlighter-rouge">gunicorn</code>. The <code class="language-plaintext highlighter-rouge">nginx</code> forwards all requests with <code class="language-plaintext highlighter-rouge">/api</code> and <code class="language-plaintext highlighter-rouge">/admin</code> in the URL to <code class="language-plaintext highlighter-rouge">http://backend:8000</code> which is the address of the Django application in the <code class="language-plaintext highlighter-rouge">docker-compose</code>:</p> <div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">location</span> <span class="s">@proxy_api</span> <span class="p">{</span> <span class="kn">proxy_set_header</span> <span class="s">Host</span> <span class="nv">$http_host</span><span class="p">;</span> <span class="kn">proxy_redirect</span> <span class="no">off</span><span class="p">;</span> <span class="kn">proxy_pass</span> <span class="s">http://backend:8000</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>The last <code class="language-plaintext highlighter-rouge">location /django_static/</code> serves static files from the Django application (created with <code class="language-plaintext highlighter-rouge">collectstatic</code> command):</p> <div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">location</span> <span class="n">/django_static/</span> <span class="p">{</span> <span class="kn">autoindex</span> <span class="no">on</span><span class="p">;</span> <span class="kn">alias</span> <span class="n">/app/backend/server/django_static/</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>That’s all. We have configuration file for nginx server.</p> <h2 id="docker-compose-for-django-nginx-and-react">Docker-compose for Django, Nginx and React</h2> <p>Our <code class="language-plaintext highlighter-rouge">docker-compose</code> will run two containers:</p> <ul> <li>the <code class="language-plaintext highlighter-rouge">nginx</code> container that will run <code class="language-plaintext highlighter-rouge">nginx</code> server on port 80 (default <code class="language-plaintext highlighter-rouge">HTTP</code> port). The <code class="language-plaintext highlighter-rouge">nginx</code> container has two volumes mounted, one with <code class="language-plaintext highlighter-rouge">django_static</code> and one with configuration file (from <code class="language-plaintext highlighter-rouge">docker/nginx/development</code> directory). The <code class="language-plaintext highlighter-rouge">nginx</code> container depends on <code class="language-plaintext highlighter-rouge">backend</code> container, which means that <code class="language-plaintext highlighter-rouge">nginx</code> will start after <code class="language-plaintext highlighter-rouge">backend</code> (Django app).</li> <li>the <code class="language-plaintext highlighter-rouge">backend</code> will run container with Django application. It runs the <code class="language-plaintext highlighter-rouge">/app/docker/backend/wsgi-entrypoint.sh</code> script that starts the <code class="language-plaintext highlighter-rouge">gunicorn</code> server. The <code class="language-plaintext highlighter-rouge">backend</code> has <code class="language-plaintext highlighter-rouge">django_static</code> volume with static files.</li> </ul> <p>We will save our <code class="language-plaintext highlighter-rouge">docker-compose</code> in the <code class="language-plaintext highlighter-rouge">docker-compose-dev.yml</code> file (in main project directory):</p> <div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">2'</span> <span class="na">services</span><span class="pi">:</span> <span class="na">nginx</span><span class="pi">:</span> <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span> <span class="na">build</span><span class="pi">:</span> <span class="na">context</span><span class="pi">:</span> <span class="s">.</span> <span class="na">dockerfile</span><span class="pi">:</span> <span class="s">./docker/nginx/Dockerfile</span> <span class="na">ports</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">80:80</span> <span class="na">volumes</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">static_volume:/app/backend/server/django_static</span> <span class="pi">-</span> <span class="s">./docker/nginx/development:/etc/nginx/conf.d</span> <span class="na">depends_on</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">backend</span> <span class="na">backend</span><span class="pi">:</span> <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span> <span class="na">build</span><span class="pi">:</span> <span class="na">context</span><span class="pi">:</span> <span class="s">.</span> <span class="na">dockerfile</span><span class="pi">:</span> <span class="s">./docker/backend/Dockerfile</span> <span class="na">volumes</span><span class="pi">:</span> <span class="na">entrypoint</span><span class="pi">:</span> <span class="s">/app/docker/backend/wsgi-entrypoint.sh</span> <span class="na">volumes</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">static_volume:/app/backend/server/django_static</span> <span class="na">expose</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">8000</span> <span class="na">volumes</span><span class="pi">:</span> <span class="na">static_volume</span><span class="pi">:</span> <span class="pi">{}</span> </code></pre></div></div> <p>There are few useful commands for dealing with <code class="language-plaintext highlighter-rouge">docker-compose</code>.</p> <h4 id="build-containers">Build containers</h4> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose <span class="nt">-f</span> docker-compose-dev.yml build </code></pre></div></div> <p>Please notice that we are using <code class="language-plaintext highlighter-rouge">-f docker-compose-dev.yml</code> - it is to point custom <code class="language-plaintext highlighter-rouge">yml</code> file. By default <code class="language-plaintext highlighter-rouge">docker-compose</code> is reading <code class="language-plaintext highlighter-rouge">yml</code> configuration file from <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> (we keep default name for production setting).</p> <h4 id="run-containers">Run containers</h4> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose <span class="nt">-f</span> docker-compose-dev.yml up </code></pre></div></div> <h4 id="stop-containers">Stop containers</h4> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose <span class="nt">-f</span> docker-compose-dev.yml down </code></pre></div></div> <p>You can also use <code class="language-plaintext highlighter-rouge">Ctrl+C</code> to stop containers if not running in the background.</p> <h4 id="build-and-run-containers">Build and run containers</h4> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose <span class="nt">-f</span> docker-compose-dev.yml up <span class="nt">--build</span> </code></pre></div></div> <p>Please build containers and run them with last command. You should see logs from <code class="language-plaintext highlighter-rouge">backend</code> and <code class="language-plaintext highlighter-rouge">nginx</code> in the terminal.</p> <p>OK, let’s go into browser and go to <a href="http://0.0.0.0">http://0.0.0.0</a> address. You should see the <code class="language-plaintext highlighter-rouge">Home</code> view. Let’s try to login:</p> <p><a href="login_error.png"><img src="login_error.png" alt="Login error" class="image-border" /></a></p> <p>After login attempt you should see toast with error: “Network Error” and some errors in the console.</p> <p>We need to update our code. We need to set <code class="language-plaintext highlighter-rouge">axios</code> configuration to point to correct address of server in the frontend. In the backend, we need to add <code class="language-plaintext highlighter-rouge">0.0.0.0</code> to <code class="language-plaintext highlighter-rouge">ALLOWED_HOSTS</code> (in <code class="language-plaintext highlighter-rouge">settings.py</code> file).</p> <p>Update <code class="language-plaintext highlighter-rouge">frontend/src/App.js</code> code.</p> <div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// frontend/src/App.js</span> <span class="c1">// remove this line</span> <span class="c1">//axios.defaults.baseURL = "http://localhost:8000";</span> <span class="c1">// new code</span> <span class="k">if</span> <span class="p">(</span><span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">origin</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">http://localhost:3000</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span> <span class="nx">axios</span><span class="p">.</span><span class="nx">defaults</span><span class="p">.</span><span class="nx">baseURL</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">http://127.0.0.1:8000</span><span class="dl">"</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="nx">axios</span><span class="p">.</span><span class="nx">defaults</span><span class="p">.</span><span class="nx">baseURL</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">origin</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>In the frontend, we set a logic to point the correct address of the server:</p> <ul> <li>in the case of development the <code class="language-plaintext highlighter-rouge">axios</code> will call server at <code class="language-plaintext highlighter-rouge">http://127.0.0.1:8000</code>,</li> <li>in the production, the <code class="language-plaintext highlighter-rouge">axios</code> will call server at the same location origin as frontend. Frontend will call in fact <code class="language-plaintext highlighter-rouge">nginx</code> and it will redirect request to the Django.</li> </ul> <p>In the <code class="language-plaintext highlighter-rouge">backend/server/server/settings.py</code> we need to update <code class="language-plaintext highlighter-rouge">ALLOWED_HOSTS</code> variable:</p> <div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># in backend/server/server/settings.py </span> <span class="c1"># ... </span><span class="n">ALLOWED_HOSTS</span> <span class="o">=</span> <span class="p">[</span><span class="s">'0.0.0.0'</span><span class="p">]</span> <span class="c1"># ... </span></code></pre></div></div> <p>After changes we need stop containers (<code class="language-plaintext highlighter-rouge">Ctrl+C</code>) and then rebuild docker containers and run them again.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose <span class="nt">-f</span> docker-compose-dev.yml up <span class="nt">--build</span> </code></pre></div></div> <p>Now, if you try to login you should be successful! What is more, you can use this <code class="language-plaintext highlighter-rouge">docker-compose</code> and run it on VPS in the cloud and make it available to others. BUT, it will be unsecure! It is using only HTTP. We need to add SSL certificate to make it secure. For this we will create next <code class="language-plaintext highlighter-rouge">docker-compose</code> configuration file for production. We also add production <code class="language-plaintext highlighter-rouge">nginx</code> configuration file.</p> <h2 id="production-docker-compose">Production docker-compose</h2> <p>Let’s add directory <code class="language-plaintext highlighter-rouge">production</code> in the <code class="language-plaintext highlighter-rouge">docker/nginx/</code> with <code class="language-plaintext highlighter-rouge">default.conf</code> file. It will be a configuration of <code class="language-plaintext highlighter-rouge">nginx</code> server to be used in the production:</p> <div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">server</span> <span class="p">{</span> <span class="kn">listen</span> <span class="mi">80</span><span class="p">;</span> <span class="kn">server_name</span> <span class="s">boilerplate.saasitive.com</span><span class="p">;</span> <span class="kn">server_tokens</span> <span class="no">off</span><span class="p">;</span> <span class="kn">location</span> <span class="n">/.well-known/acme-challenge/</span> <span class="p">{</span> <span class="kn">root</span> <span class="n">/var/www/certbot</span><span class="p">;</span> <span class="p">}</span> <span class="kn">location</span> <span class="n">/</span> <span class="p">{</span> <span class="kn">return</span> <span class="mi">301</span> <span class="s">https://</span><span class="nv">$host$request_uri</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="k">server</span> <span class="p">{</span> <span class="kn">listen</span> <span class="mi">443</span> <span class="s">ssl</span><span class="p">;</span> <span class="kn">server_name</span> <span class="s">boilerplate.saasitive.com</span><span class="p">;</span> <span class="kn">server_tokens</span> <span class="no">off</span><span class="p">;</span> <span class="kn">ssl_certificate</span> <span class="n">/etc/letsencrypt/live/boilerplate.saasitive.com/fullchain.pem</span><span class="p">;</span> <span class="kn">ssl_certificate_key</span> <span class="n">/etc/letsencrypt/live/boilerplate.saasitive.com/privkey.pem</span><span class="p">;</span> <span class="kn">include</span> <span class="n">/etc/letsencrypt/options-ssl-nginx.conf</span><span class="p">;</span> <span class="kn">ssl_dhparam</span> <span class="n">/etc/letsencrypt/ssl-dhparams.pem</span><span class="p">;</span> <span class="kn">client_max_body_size</span> <span class="mi">20M</span><span class="p">;</span> <span class="kn">location</span> <span class="n">/</span> <span class="p">{</span> <span class="kn">root</span> <span class="n">/usr/share/nginx/html</span><span class="p">;</span> <span class="kn">index</span> <span class="s">index.html</span> <span class="s">index.htm</span><span class="p">;</span> <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="nv">$uri</span><span class="n">/</span> <span class="n">/index.html</span><span class="p">;</span> <span class="p">}</span> <span class="kn">location</span> <span class="n">/api</span> <span class="p">{</span> <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="s">@proxy_api</span><span class="p">;</span> <span class="p">}</span> <span class="kn">location</span> <span class="n">/admin</span> <span class="p">{</span> <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="s">@proxy_api</span><span class="p">;</span> <span class="p">}</span> <span class="kn">location</span> <span class="s">@proxy_api</span> <span class="p">{</span> <span class="kn">proxy_set_header</span> <span class="s">X-Forwarded-Proto</span> <span class="s">https</span><span class="p">;</span> <span class="kn">proxy_set_header</span> <span class="s">X-Url-Scheme</span> <span class="nv">$scheme</span><span class="p">;</span> <span class="kn">proxy_set_header</span> <span class="s">X-Forwarded-For</span> <span class="nv">$proxy_add_x_forwarded_for</span><span class="p">;</span> <span class="kn">proxy_set_header</span> <span class="s">Host</span> <span class="nv">$http_host</span><span class="p">;</span> <span class="kn">proxy_redirect</span> <span class="no">off</span><span class="p">;</span> <span class="kn">proxy_pass</span> <span class="s">http://backend:8000</span><span class="p">;</span> <span class="p">}</span> <span class="kn">location</span> <span class="n">/django_static/</span> <span class="p">{</span> <span class="kn">autoindex</span> <span class="no">on</span><span class="p">;</span> <span class="kn">alias</span> <span class="n">/app/backend/server/django_static/</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>It is similar to the development configuration. It has two <code class="language-plaintext highlighter-rouge">server</code> definitions. The first one:</p> <div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">server</span> <span class="p">{</span> <span class="kn">listen</span> <span class="mi">80</span><span class="p">;</span> <span class="kn">server_name</span> <span class="s">boilerplate.saasitive.com</span><span class="p">;</span> <span class="kn">server_tokens</span> <span class="no">off</span><span class="p">;</span> <span class="kn">location</span> <span class="n">/.well-known/acme-challenge/</span> <span class="p">{</span> <span class="kn">root</span> <span class="n">/var/www/certbot</span><span class="p">;</span> <span class="p">}</span> <span class="kn">location</span> <span class="n">/</span> <span class="p">{</span> <span class="kn">return</span> <span class="mi">301</span> <span class="s">https://</span><span class="nv">$host$request_uri</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>The above definition listen on port <code class="language-plaintext highlighter-rouge">80</code> and redirects all requests to <code class="language-plaintext highlighter-rouge">HTTPS</code> (<code class="language-plaintext highlighter-rouge">return 301 https://$host$request_uri;</code>). In this part there is also a <code class="language-plaintext highlighter-rouge">server_name</code> filled to <code class="language-plaintext highlighter-rouge">boilerplate.saasitive.com</code>. I’m planning to run the application from this tutorial on <code class="language-plaintext highlighter-rouge">boilerplate.saasitive.com</code> domain. <strong>You should use here your own domain.</strong>. The <code class="language-plaintext highlighter-rouge">location /.well-known/acme-challenge/</code> is used by cerbot for issuing the certificate.</p> <p>Let’s look closer at second <code class="language-plaintext highlighter-rouge">server</code> in the configuration:</p> <div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">server</span> <span class="p">{</span> <span class="kn">listen</span> <span class="mi">443</span> <span class="s">ssl</span><span class="p">;</span> <span class="kn">server_name</span> <span class="s">boilerplate.saasitive.com</span><span class="p">;</span> <span class="kn">server_tokens</span> <span class="no">off</span><span class="p">;</span> <span class="kn">ssl_certificate</span> <span class="n">/etc/letsencrypt/live/boilerplate.saasitive.com/fullchain.pem</span><span class="p">;</span> <span class="kn">ssl_certificate_key</span> <span class="n">/etc/letsencrypt/live/boilerplate.saasitive.com/privkey.pem</span><span class="p">;</span> <span class="kn">include</span> <span class="n">/etc/letsencrypt/options-ssl-nginx.conf</span><span class="p">;</span> <span class="kn">ssl_dhparam</span> <span class="n">/etc/letsencrypt/ssl-dhparams.pem</span><span class="p">;</span> <span class="kn">client_max_body_size</span> <span class="mi">20M</span><span class="p">;</span> <span class="c1">#...</span> </code></pre></div></div> <p>It is listening on 443 port which is default for <code class="language-plaintext highlighter-rouge">HTTPS</code>. The <code class="language-plaintext highlighter-rouge">server_name</code> is set to domain. There are added paths with certificate. You should rename them to your domain:</p> <div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">ssl_certificate</span> <span class="n">/etc/letsencrypt/live/---your-domain.com---/fullchain.pem</span><span class="p">;</span> <span class="k">ssl_certificate_key</span> <span class="n">/etc/letsencrypt/live/---your-domain.com---/privkey.pem</span><span class="p">;</span> </code></pre></div></div> <p>The rest of the configuration is very similar to development configuration, except that we need to set additional headers:</p> <div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">location</span> <span class="s">@proxy_api</span> <span class="p">{</span> <span class="kn">proxy_set_header</span> <span class="s">X-Forwarded-Proto</span> <span class="s">https</span><span class="p">;</span> <span class="c1"># additional </span> <span class="kn">proxy_set_header</span> <span class="s">X-Url-Scheme</span> <span class="nv">$scheme</span><span class="p">;</span> <span class="c1"># additional </span> <span class="kn">proxy_set_header</span> <span class="s">X-Forwarded-For</span> <span class="nv">$proxy_add_x_forwarded_for</span><span class="p">;</span> <span class="c1"># additional</span> <span class="kn">proxy_set_header</span> <span class="s">Host</span> <span class="nv">$http_host</span><span class="p">;</span> <span class="kn">proxy_redirect</span> <span class="no">off</span><span class="p">;</span> <span class="kn">proxy_pass</span> <span class="s">http://backend:8000</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>Let’s add production <code class="language-plaintext highlighter-rouge">docker-compose</code>. The <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> file in the main project directory:</p> <div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">2'</span> <span class="na">services</span><span class="pi">:</span> <span class="na">nginx</span><span class="pi">:</span> <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span> <span class="na">build</span><span class="pi">:</span> <span class="na">context</span><span class="pi">:</span> <span class="s">.</span> <span class="na">dockerfile</span><span class="pi">:</span> <span class="s">./docker/nginx/Dockerfile</span> <span class="na">ports</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">80:80</span> <span class="pi">-</span> <span class="s">443:443</span> <span class="na">volumes</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">static_volume:/app/backend/server/django_static</span> <span class="pi">-</span> <span class="s">./docker/nginx/production:/etc/nginx/conf.d</span> <span class="pi">-</span> <span class="s">./docker/nginx/certbot/conf:/etc/letsencrypt</span> <span class="pi">-</span> <span class="s">./docker/nginx/certbot/www:/var/www/certbot</span> <span class="na">depends_on</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">backend</span> <span class="na">certbot</span><span class="pi">:</span> <span class="na">image</span><span class="pi">:</span> <span class="s">certbot/certbot</span> <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span> <span class="na">volumes</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">./docker/nginx/certbot/conf:/etc/letsencrypt</span> <span class="pi">-</span> <span class="s">./docker/nginx/certbot/www:/var/www/certbot</span> <span class="na">entrypoint</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/bin/sh</span><span class="nv"> </span><span class="s">-c</span><span class="nv"> </span><span class="s">'trap</span><span class="nv"> </span><span class="s">exit</span><span class="nv"> </span><span class="s">TERM;</span><span class="nv"> </span><span class="s">while</span><span class="nv"> </span><span class="s">:;</span><span class="nv"> </span><span class="s">do</span><span class="nv"> </span><span class="s">certbot</span><span class="nv"> </span><span class="s">renew;</span><span class="nv"> </span><span class="s">sleep</span><span class="nv"> </span><span class="s">12h</span><span class="nv"> </span><span class="s">&amp;</span><span class="nv"> </span><span class="s">wait</span><span class="nv"> </span><span class="s">$${!};</span><span class="nv"> </span><span class="s">done;'"</span> <span class="na">backend</span><span class="pi">:</span> <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span> <span class="na">build</span><span class="pi">:</span> <span class="na">context</span><span class="pi">:</span> <span class="s">.</span> <span class="na">dockerfile</span><span class="pi">:</span> <span class="s">./docker/backend/Dockerfile</span> <span class="na">entrypoint</span><span class="pi">:</span> <span class="s">/app/docker/backend/wsgi-entrypoint.sh</span> <span class="na">volumes</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">static_volume:/app/backend/server/django_static</span> <span class="na">expose</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">8000</span> <span class="na">volumes</span><span class="pi">:</span> <span class="na">static_volume</span><span class="pi">:</span> <span class="pi">{}</span> </code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">nginx</code> server is listenning on two ports: 80 (<code class="language-plaintext highlighter-rouge">HTTP</code>) and 443 (<code class="language-plaintext highlighter-rouge">HTTPS</code>). There are added additional volumes in <code class="language-plaintext highlighter-rouge">nginx</code> with certificate. There is a new container <code class="language-plaintext highlighter-rouge">certbot</code> that is responsible for certificate renewal. There are no changes in the <code class="language-plaintext highlighter-rouge">backend</code> container.</p> <p>OK, to be ready to run <code class="language-plaintext highlighter-rouge">docker-compose</code> we need to get the certificate from <a href="https://letsencrypt.org/">Let’s Encrypt</a>. I will use for it a bash script <code class="language-plaintext highlighter-rouge">init-letsencrypt.sh</code>. The script is from:</p> <ul> <li>article <a href="https://medium.com/@pentacent/nginx-and-lets-encrypt-with-docker-in-less-than-5-minutes-b4b8a60d3a71">Nginx and Let’s Encrypt with Docker in Less Than 5 Minutes</a>,</li> <li>the Github repository with script: <a href="https://github.com/wmnnd/nginx-certbot">link</a>.</li> </ul> <p>The <code class="language-plaintext highlighter-rouge">init-letsencrypt.sh</code> file:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span> <span class="k">if</span> <span class="o">!</span> <span class="o">[</span> <span class="nt">-x</span> <span class="s2">"</span><span class="si">$(</span><span class="nb">command</span> <span class="nt">-v</span> docker-compose<span class="si">)</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then </span><span class="nb">echo</span> <span class="s1">'Error: docker-compose is not installed.'</span> <span class="o">&gt;</span>&amp;2 <span class="nb">exit </span>1 <span class="k">fi </span><span class="nv">domains</span><span class="o">=(</span>boilerplate.saasitive.com www.boilerplate.saasitive.com<span class="o">)</span> <span class="nv">rsa_key_size</span><span class="o">=</span>4096 <span class="nv">data_path</span><span class="o">=</span><span class="s2">"./docker/nginx/certbot"</span> <span class="nv">email</span><span class="o">=</span><span class="s2">""</span> <span class="c"># Adding a valid address is strongly recommended</span> <span class="nv">staging</span><span class="o">=</span>1 <span class="c"># Set to 1 if you're testing your setup to avoid hitting request limits</span> <span class="k">if</span> <span class="o">[</span> <span class="nt">-d</span> <span class="s2">"</span><span class="nv">$data_path</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then </span><span class="nb">read</span> <span class="nt">-p</span> <span class="s2">"Existing data found for </span><span class="nv">$domains</span><span class="s2">. Continue and replace existing certificate? (y/N) "</span> decision <span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$decision</span><span class="s2">"</span> <span class="o">!=</span> <span class="s2">"Y"</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$decision</span><span class="s2">"</span> <span class="o">!=</span> <span class="s2">"y"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then </span><span class="nb">exit </span><span class="k">fi fi if</span> <span class="o">[</span> <span class="o">!</span> <span class="nt">-e</span> <span class="s2">"</span><span class="nv">$data_path</span><span class="s2">/conf/options-ssl-nginx.conf"</span> <span class="o">]</span> <span class="o">||</span> <span class="o">[</span> <span class="o">!</span> <span class="nt">-e</span> <span class="s2">"</span><span class="nv">$data_path</span><span class="s2">/conf/ssl-dhparams.pem"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then </span><span class="nb">echo</span> <span class="s2">"### Downloading recommended TLS parameters ..."</span> <span class="nb">mkdir</span> <span class="nt">-p</span> <span class="s2">"</span><span class="nv">$data_path</span><span class="s2">/conf"</span> curl <span class="nt">-s</span> https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf <span class="o">&gt;</span> <span class="s2">"</span><span class="nv">$data_path</span><span class="s2">/conf/options-ssl-nginx.conf"</span> curl <span class="nt">-s</span> https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem <span class="o">&gt;</span> <span class="s2">"</span><span class="nv">$data_path</span><span class="s2">/conf/ssl-dhparams.pem"</span> <span class="nb">echo </span><span class="k">fi </span><span class="nb">echo</span> <span class="s2">"### Creating dummy certificate for </span><span class="nv">$domains</span><span class="s2"> ..."</span> <span class="nv">path</span><span class="o">=</span><span class="s2">"/etc/letsencrypt/live/</span><span class="nv">$domains</span><span class="s2">"</span> <span class="nb">mkdir</span> <span class="nt">-p</span> <span class="s2">"</span><span class="nv">$data_path</span><span class="s2">/conf/live/</span><span class="nv">$domains</span><span class="s2">"</span> docker-compose run <span class="nt">--rm</span> <span class="nt">--entrypoint</span> <span class="s2">"</span><span class="se">\</span><span class="s2"> openssl req -x509 -nodes -newkey rsa:1024 -days 1</span><span class="se">\</span><span class="s2"> -keyout '</span><span class="nv">$path</span><span class="s2">/privkey.pem' </span><span class="se">\</span><span class="s2"> -out '</span><span class="nv">$path</span><span class="s2">/fullchain.pem' </span><span class="se">\</span><span class="s2"> -subj '/CN=localhost'"</span> certbot <span class="nb">echo echo</span> <span class="s2">"### Starting nginx ..."</span> docker-compose up <span class="nt">--force-recreate</span> <span class="nt">-d</span> nginx <span class="nb">echo echo</span> <span class="s2">"### Deleting dummy certificate for </span><span class="nv">$domains</span><span class="s2"> ..."</span> docker-compose run <span class="nt">--rm</span> <span class="nt">--entrypoint</span> <span class="s2">"</span><span class="se">\</span><span class="s2"> rm -Rf /etc/letsencrypt/live/</span><span class="nv">$domains</span><span class="s2"> &amp;&amp; </span><span class="se">\</span><span class="s2"> rm -Rf /etc/letsencrypt/archive/</span><span class="nv">$domains</span><span class="s2"> &amp;&amp; </span><span class="se">\</span><span class="s2"> rm -Rf /etc/letsencrypt/renewal/</span><span class="nv">$domains</span><span class="s2">.conf"</span> certbot <span class="nb">echo echo</span> <span class="s2">"### Requesting Let's Encrypt certificate for </span><span class="nv">$domains</span><span class="s2"> ..."</span> <span class="c">#Join $domains to -d args</span> <span class="nv">domain_args</span><span class="o">=</span><span class="s2">""</span> <span class="k">for </span>domain <span class="k">in</span> <span class="s2">"</span><span class="k">${</span><span class="nv">domains</span><span class="p">[@]</span><span class="k">}</span><span class="s2">"</span><span class="p">;</span> <span class="k">do </span><span class="nv">domain_args</span><span class="o">=</span><span class="s2">"</span><span class="nv">$domain_args</span><span class="s2"> -d </span><span class="nv">$domain</span><span class="s2">"</span> <span class="k">done</span> <span class="c"># Select appropriate email arg</span> <span class="k">case</span> <span class="s2">"</span><span class="nv">$email</span><span class="s2">"</span> <span class="k">in</span> <span class="s2">""</span><span class="p">)</span> <span class="nv">email_arg</span><span class="o">=</span><span class="s2">"--register-unsafely-without-email"</span> <span class="p">;;</span> <span class="k">*</span><span class="p">)</span> <span class="nv">email_arg</span><span class="o">=</span><span class="s2">"--email </span><span class="nv">$email</span><span class="s2">"</span> <span class="p">;;</span> <span class="k">esac</span> <span class="c"># Enable staging mode if needed</span> <span class="k">if</span> <span class="o">[</span> <span class="nv">$staging</span> <span class="o">!=</span> <span class="s2">"0"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then </span><span class="nv">staging_arg</span><span class="o">=</span><span class="s2">"--staging"</span><span class="p">;</span> <span class="k">fi </span>docker-compose run <span class="nt">--rm</span> <span class="nt">--entrypoint</span> <span class="s2">"</span><span class="se">\</span><span class="s2"> certbot certonly --webroot -w /var/www/certbot </span><span class="se">\</span><span class="s2"> </span><span class="nv">$staging_arg</span><span class="s2"> </span><span class="se">\</span><span class="s2"> </span><span class="nv">$email_arg</span><span class="s2"> </span><span class="se">\</span><span class="s2"> </span><span class="nv">$domain_args</span><span class="s2"> </span><span class="se">\</span><span class="s2"> --rsa-key-size </span><span class="nv">$rsa_key_size</span><span class="s2"> </span><span class="se">\</span><span class="s2"> --agree-tos </span><span class="se">\</span><span class="s2"> --force-renewal"</span> certbot <span class="nb">echo echo</span> <span class="s2">"### Reloading nginx ..."</span> docker-compose <span class="nb">exec </span>nginx nginx <span class="nt">-s</span> reload </code></pre></div></div> <p>You need to set two variables in the script:</p> <ul> <li><code class="language-plaintext highlighter-rouge">domains=(boilerplate.saasitive.com www.boilerplate.saasitive.com)</code> you should enter <strong>your domain</strong> here,</li> <li><code class="language-plaintext highlighter-rouge">staging=1</code> - is set to <strong>test</strong> the configuration first with <strong>Let’s encrypt staging environment</strong>! It is important to not set <code class="language-plaintext highlighter-rouge">staging=0</code> before you are 100% sure that your configuration is correct. If you are sure, that all is set correctly, set <code class="language-plaintext highlighter-rouge">staging=0</code>. This is because there are limited number of retries to issue the certificate and you don’t want to wait till they are reseted (once a week). To read more about this, check <a href="https://letsencrypt.org/docs/rate-limits/">Let’s encrypt rate limits docs</a>.</li> </ul> <p>We will use this script first with <code class="language-plaintext highlighter-rouge">staging=1</code>. We will test if web application is running correctly. If yes then we will issue production certificate and run the application with it.</p> <p><strong>Note:</strong> You need to have your own domain in this tutorial. The Let’s Encrypt certificates doesn’t work with public IPs (<a href="https://community.letsencrypt.org/t/certificate-for-public-ip-without-domain-name/6082">link to forum topic</a>).</p> <h2 id="deploy-to-vps">Deploy to VPS</h2> <p>I will use AWS to deploy the project. The application will be deployed to <a href="https://boilerplate.saasitive.com">boilerplate.saasitive.com</a> address.</p> <p>We need to do below steps to deploy application:</p> <ul> <li>start EC2 instance,</li> <li>configure DNS,</li> <li>copy code to EC2 instance,</li> <li>issue certificate,</li> <li>start docker-compose.</li> </ul> <h4 id="start-ec2-instance">Start EC2 instance</h4> <p>First thing is to start EC2 instance. I’m using <code class="language-plaintext highlighter-rouge">t2.micro</code> instance (cant use <code class="language-plaintext highlighter-rouge">t2.nano</code> because of too low memory to build containers). Please launch a new EC2 instance. I’m using <code class="language-plaintext highlighter-rouge">Ubuntu Server 18.04 LTS</code> as the image (the last image in the screenshot below):</p> <p><a href="aws_instance_type.png"><img src="aws_instance_type.png" alt="AWS Instance type" class="image-border" /></a></p> <p>After clicking select, please go with <code class="language-plaintext highlighter-rouge">t2.micro</code> instance type (the cost of running is 8.35$ per month). Click <code class="language-plaintext highlighter-rouge">Next: Configure Instance Details</code>. In configuration please click <code class="language-plaintext highlighter-rouge">Next</code> and stop on <code class="language-plaintext highlighter-rouge">Step 6: Configure Security Group</code>. Please add there rules to accept <code class="language-plaintext highlighter-rouge">HTTP</code> and <code class="language-plaintext highlighter-rouge">HTTPS</code> traffic (like in the image below):</p> <p><a href="aws_security.png"><img src="aws_security.png" alt="AWS security rules" class="image-border" /></a></p> <p>Click <code class="language-plaintext highlighter-rouge">Review and Launch</code> and then <code class="language-plaintext highlighter-rouge">Launch</code>. You should get the <code class="language-plaintext highlighter-rouge">*.pem</code> file for secure connection to the instance. You can use existing key pair or generate the new one. My file is <code class="language-plaintext highlighter-rouge">boilerplate.pem</code>. After a few seconds you should see your instance running. Congratulations, you have a VPS running! Now, we will configure domain to point to our VPS.</p> <h4 id="configure-dns">Configure DNS</h4> <p>In the instances view, please click on your instance and you should see <code class="language-plaintext highlighter-rouge">Public IPv4 address</code>. Please copy this address.</p> <p>Let’s go to <code class="language-plaintext highlighter-rouge">Route 53</code> service in AWS to configure the DNS. I have there Hosted zone configured for <a href="https://saasitive">saasitive.com</a> domain. I will add there two <code class="language-plaintext highlighter-rouge">type A</code> records. Please click on <code class="language-plaintext highlighter-rouge">Create record</code>. Then select <code class="language-plaintext highlighter-rouge">Simple routing</code>. Click <code class="language-plaintext highlighter-rouge">Next</code> and <code class="language-plaintext highlighter-rouge">Define simple record</code>. Please give the record name and route traffic to VPS IP, like in the image below:</p> <p><a href="route_53.png"><img src="route_53.png" alt="Route 53 configuration" class="image-border" /></a></p> <p>I want to have routing for:</p> <ul> <li><code class="language-plaintext highlighter-rouge">boilerplate.saasitive.com</code></li> <li><code class="language-plaintext highlighter-rouge">www.boilerplate.saasitive.com</code></li> </ul> <p>That’s why I’ve added two records, one for <code class="language-plaintext highlighter-rouge">boilerplate</code> and one for <code class="language-plaintext highlighter-rouge">www.boilerplate</code>. Both pointing to the same IP.</p> <p>If you don’t want to run application on subdomain, just point main domain to VPS IP.</p> <h4 id="copy-code-to-vps">Copy code to VPS</h4> <p>We need to have our application code in the VPS to run it. There are many ways to do it. I’m using <code class="language-plaintext highlighter-rouge">rsync</code> command. Here is how.</p> <p>Please select the instance (in EC2 instances list) and click <code class="language-plaintext highlighter-rouge">Connect</code> at the top of the website. There should be example how to connect to instance with SSH:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="nt">-i</span> <span class="s2">"boilerplate.pem"</span> [email protected] </code></pre></div></div> <p>Please remember to:</p> <ul> <li>give correct permissions to <code class="language-plaintext highlighter-rouge">*.pem</code> file: <code class="language-plaintext highlighter-rouge">chmod 400 boilerplate.pem</code>,</li> <li>please replace the name of <code class="language-plaintext highlighter-rouge">*.pem</code> file to yours.</li> </ul> <p>After ssh, you should be logged in the VPS. Let’s install <code class="language-plaintext highlighter-rouge">docker</code> and create the <code class="language-plaintext highlighter-rouge">app</code> directory:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>snap <span class="nb">install </span>docker <span class="nb">mkdir </span>app </code></pre></div></div> <p>In the new console, please navigate to the project directory and <code class="language-plaintext highlighter-rouge">rsync</code> it with directory in VPS:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># run locally in project main directory</span> rsync <span class="nt">-avz</span> <span class="nt">-progress</span> <span class="nt">-e</span> <span class="s2">"ssh -i boilerplate.pem"</span> <span class="nt">--exclude</span> backend/venv <span class="nt">--exclude</span> frontend/node_modules <span class="nb">.</span> [email protected]:/home/ubuntu/app/ </code></pre></div></div> <p>The above command sync the current directory (there is a <code class="language-plaintext highlighter-rouge">.</code> dot in the command!) with <code class="language-plaintext highlighter-rouge">home/ubuntu/app/</code> directory in the VPS.</p> <p>That’s all! If you go to terminal with SSH connection to VPS and run <code class="language-plaintext highlighter-rouge">ls</code> command in <code class="language-plaintext highlighter-rouge">app</code> directory, you should see all your project files.</p> <h4 id="issue-certificate">Issue certificate</h4> <p>Let’s go to VPS SSH connection and go into <code class="language-plaintext highlighter-rouge">app</code> directory. In this directory, please first build <code class="language-plaintext highlighter-rouge">docker-compose</code> containers:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># run in VPS!</span> <span class="nb">cd </span>app <span class="nb">sudo </span>docker-compose build </code></pre></div></div> <p>First, we will issue certificate from Let’s Encrypt staging environment.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># run in VPS! in app dir</span> <span class="nb">sudo</span> ./init-letsencrypt.sh </code></pre></div></div> <p>If successful, please stop all running containers and run the <code class="language-plaintext highlighter-rouge">docker-compose</code> in the background:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># run in VPS! in app dir</span> <span class="nb">sudo </span>docker-compose down <span class="nb">sudo </span>docker-compose up <span class="nt">--detach</span> </code></pre></div></div> <p>You should be able to navigate to your website. Please go to web browser and enter your domain (or subdomain). In my case, I will go to <a href="https://boilerplate.saasitive.com">https://boilerplate.saasitive.com</a>. You should see warning message (depending on your web browser) that will tell you that connection is insecure:</p> <p><a href="firefox_warning.png"><img src="firefox_warning.png" alt="Firefox insecure warning" class="image-border" /></a></p> <p>To see the website, you need to go into advanced options and accept the risk. After that, you should see the application:</p> <p><a href="not_secure.png"><img src="not_secure.png" alt="Not secure connection" class="image-border" /></a></p> <p>We need to issue production-ready certificate. Let’s go to VPS and stop containers:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># run in VPS! in app dir</span> <span class="c"># stop containers</span> <span class="nb">sudo </span>docker-compose down </code></pre></div></div> <p>You need to edit <code class="language-plaintext highlighter-rouge">init-letsencrypt.sh</code> file and set <code class="language-plaintext highlighter-rouge">staging=0</code>. Then run the script:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># run in VPS! in app dir</span> <span class="nb">sudo</span> ./init-letsencrypt.sh </code></pre></div></div> <p>Once again, stop containers and run them in the background:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># run in VPS! in app dir</span> <span class="nb">sudo </span>docker-compose down <span class="nb">sudo </span>docker-compose up <span class="nt">--detach</span> </code></pre></div></div> <p>Let’s refresh the web browser:</p> <p><a href="security_boilerplate.png"><img src="security_boilerplate.png" alt="Secure connection with HTTPS to Django+React" class="image-border" /></a></p> <p>Great, the application is running with <code class="language-plaintext highlighter-rouge">HTTPS</code>! What is more, if you enter URL with <code class="language-plaintext highlighter-rouge">http://</code> you will be redirected to <code class="language-plaintext highlighter-rouge">https://</code>! Nice!</p> <hr /> <p>Is it working correctly? Please try to login or signup …</p> <p>Ups, there is some error …</p> <h4 id="edit-allowed_hosts">Edit ALLOWED_HOSTS</h4> <p>One more thing, we need to add <code class="language-plaintext highlighter-rouge">boilerplate.saasitive.com</code> and <code class="language-plaintext highlighter-rouge">www.boilerplate.saasitive.com</code> to the <code class="language-plaintext highlighter-rouge">ALLOWED_HOSTS</code> in Django <code class="language-plaintext highlighter-rouge">settings.py</code> file. And, there is a <code class="language-plaintext highlighter-rouge">DEBUG</code> flag in <code class="language-plaintext highlighter-rouge">settings.py</code>. Please set it to <code class="language-plaintext highlighter-rouge">False</code>.</p> <div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># backend/server/server/settings.py </span> <span class="c1"># ... </span><span class="n">DEBUG</span><span class="o">=</span><span class="bp">False</span> <span class="n">ALLOWED_HOSTS</span> <span class="o">=</span> <span class="p">[</span><span class="s">'0.0.0.0'</span><span class="p">,</span> <span class="s">'boilerplate.saasitive.com'</span><span class="p">,</span> <span class="s">'www.boilerplate.saasitive.com'</span><span class="p">]</span> <span class="c1"># ... </span></code></pre></div></div> <p>Save the file and sync the code on VPS.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># sync code</span> rsync <span class="nt">-avz</span> <span class="nt">-progress</span> <span class="nt">-e</span> <span class="s2">"ssh -i boilerplate.pem"</span> <span class="nt">--exclude</span> backend/venv <span class="nt">--exclude</span> frontend/node_modules <span class="nb">.</span> [email protected]:/home/ubuntu/app/ </code></pre></div></div> <p>On VPS machine, please stop docker, build it and re-run in the background:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># run in VPS! in app dir</span> <span class="c"># stop containers</span> <span class="nb">sudo </span>docker-compose down <span class="c"># build containers</span> <span class="nb">sudo </span>docker-compose build <span class="c"># run containers</span> <span class="nb">sudo </span>docker-compose up <span class="nt">--detach</span> </code></pre></div></div> <p>Please try to login after update:</p> <p><a href="success.png"><img src="success.png" alt="Success app is running" class="image-border" /></a></p> <p>You can try to create new users and login for example from your phone web browser. The application will be working!!! :)</p> <h3 id="commit-changes-to-the-repository-code">Commit changes to the repository code</h3> <p>Please remember to commit changes to the repository:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># in the main project dir</span> git add docker-compose<span class="k">*</span> git add init-letsencrypt.sh git add docker git commit <span class="nt">-am</span> <span class="s2">"add docker-compose"</span> git push </code></pre></div></div> <hr /> <h2 id="summary">Summary</h2> <ul> <li>We’ve created two <code class="language-plaintext highlighter-rouge">docker-compose</code> configurations, one for development and one for production.</li> <li>The production <code class="language-plaintext highlighter-rouge">docker-compose</code> has SSL certificate from Let’s Encrypt.</li> <li>We’ve deployed application to AWS EC2 instance with <code class="language-plaintext highlighter-rouge">docker-compose</code>.</li> <li>There are still many things to be added in the project.</li> <li>We need to better handle configuration variables and <code class="language-plaintext highlighter-rouge">SECRET_KEY</code> in the Django code. We will add this in the future post with <a href="https://github.com/henriquebastos/python-decouple/">python-decouple</a> package.</li> <li>We need to switch database engine to PostgreSQL.</li> <li>After every update in the application, we will need to run a set of commands. This can be boring and error-prone. That’s why we will need to add Continous Integration (CI) to our project. This can be added in many different ways, I will try to figure out the simplest approach (usually, I go with bash scripts). I’m open to your suggestions.</li> <li>It is important to remember that if EC2 machine is restarted, then new IP address is assigned. So there is a need to reconfigure DNS again. This can be prevented with static IP address. But, you need to pay for it additionally. I’m not setting the static IP. I’m planning to write custom blue/green deployment script (in the future post).</li> </ul> <h2 id="whats-next">What’s next?</h2> <p>In the next article we will extend user model.</p>Piotr PłońskiThe most exciting moment of the web application development is a deployment. Your app is going live! It can also be nerve-wracking moment. Unfortunately. There are many options, many variables and configurations. It is easy to miss something … In this article, I will show you how to pack Django and React application into containers and deploy them with docker-compose. The presented approach can be reused on any Cloud Provider (AWS, DigitalOcean, Linode, GCP, Heroku) - you just need a Virtual Private Server (VPS).