I am going to prepare a live deploy of a web app built with Flask. The app will be hosted as a subdomain for an already existing website that runs a WordPress installation on a LAMP stack exposed to the web via an Nginx reverse proxy.

The Flask app (svm-demo) will be served by a Gunicorn service also via Nginx. The web app will be available at this URL: svm-demo.singularaspect .com

WSGI Entry Point

Since the app is served by Gunicorn, it will require an entry point. For that I create a file under a name wsgi.py with this content:

Here, it is important that the module contains a variable called app or application. My web app uses the app factory pattern, so I have to import the function that produces the app instance.

The path to this entry point will be used later when I define a service that will run Gunicorn for my website.

Deploying the App

The future deployment of the app will be done via continuous integration (CI) service set up with Jenkins. But the first step is to prepare the deployment script and the physical location on the target server.

Preparing the Destination

The file system location of the future app will be within /var/sites directory. The project will be deployed into:

The folder structure looks like this:

In this location, the deploy script will create subdirectories that correspond to the types of deployment. The default type is master that I will use for the production (live) deployment. I could provide other types (stage or testing) for environments other than production. Important is that the deployment type label must have a corresponding branch in the repo (master, staging or testing) because the deploy script will pull that branch each time it runs.

Within the master (or any other deployment type subdirectory) the script will create a releases folder. There, the script will clone the repository. Each tome a clone will be placed under a timestamp-named directory. After each deployment, the most recent such directory will be symlinked to /var/sites/master/app. The Ngnix will serve the website from this symlink’s location. The older directories will be kept for a fallback in the case of a catastrophic deploy. There will be at most 2 releases directory at a time: one active and one fallback. Older directories will be deleted after each deployment.

Deploy Script

I use a fabric script to run the deployment. First, I need to do the required imports and set up some constants:

The main function’s content is this:

This function takes the deployment type and (optionally) the path to the private key used to access the git repository. The function creates the target path, clones there the web-app repository, and checks out the branch respective to the deployment type. This is done by calling the _git_clone function:

In the next step, it calls the _setup_app function that creates a virtual environment and installs requirements:

The unit tests are run in the next step by the _run_tests function:

If nothing is broken at this stage, it is safe to update the symlink to the live code:

Finally, the script deletes the older release directories:

The deployment script requires Fabric 3. I place the script into the project’s subdirectory deploy_tools. To run the deploy, I switch to this directory and run the following command:

Setting Up Nginx

I create a new file under /etc/nginx/sites-available/svm-demo.singularaspect.com with the following content:

The website code will be served via a socket that will be created by a service that runs Gunicorn for the website. The static files are delivered directly by Nginx.

After the file is created, I add a symlink to it under /etc/nginx/sites-enabled:

I test the Nginx configuration by running:

If everything is OK, I reload the Nginx server:

Setting Up Gunicorn

For this website, I want to run Gunicorn as a service bound to a socket. Under /etc/systemd/system , I create a file gunicorn-svm-demo.singularaspect.com.service with the following content:

I replace DOMAIN with svm-demo.singularaspect.com and USERNAME with the user I am going to run the service under. It is the same user that runs the deploy script.

To enable this configuration I run:

I check the status of the service by running:

If something goes wrong I can check either the Nginx log or the service log by running:

Continuous Integration

I want to have continuous integration set up for this project so that it would automatically pull any changes from the production branch (master) and perform all the required checks and deploy it to the production server. Alternatively, I would like to be able to manually start the deployment process from the continuous integration server.

Setting up Jenkins

I set up a Jenkins project. The general settings are these:

I specify Git as the source code control and point to the repository and branch to pull from. The repository is accessed with credentials that I’ve set up previously for this purpose.

I set up the repository polling for every hour. If changes in the master branch are detected, the project will be built and deployed.

I set up the build environment to use an SSH-Agent configured with the credentials I’ve set up to access the production server.

I set up two build steps. The first step is the virtualenv Builder that runs the pip installation and tests on the CI server and the fabric deploy on the production server.

After the fabric deploy is done, Jenkins must restart the gunicorn service.

Dealing with User Privileges

For the last step to execute, I need to allow the user that logs in with the SSH plugin to run systemd’s systemctl component to restart the gunicorn service by executing this command:

When I run this command manually I must do it with sudo and type in my password. To automate this I need to do the following steps.

First, I create a user group whose members will have access to commands starting, stopping, and restarting the service:

Second, I add the user that will run the service and that Jenkins will use to SSH connect to this group:

Third, I edit /etc/sudoers file using the sudo visudo command to add a command alias for the service control commands and give passwordless access to this alias to the user. I add these lines first:

To the end of the file I add these:

With this, Jenkins can deploy the fresh code and restart the service to put it live.