Auto Deploying a Django App using Capistrano with GitLab CI/CD

Yes, you read that right. We are using Capistrano – a very popular application deployment tool that is written in Ruby to deploy a Python Django app. In fact, you can deploy any application written in any language using Capistrano. We have been using Capistrano to deploy Rails apps for a very long time and has been the obvious tool of choice for deployments.

We recently moved our entire code base to GitLab and we really wanted to leverage the CI-CD capabilities that GitLab provided out of the box. Some of our apps are built and deployed using Docker, which can be easily deployed using GitLab CI-CD but deploying Django wasn’t pretty straight forward, and that’s how we ended up using Capistrano.

1. Install bundler

Install bundler if you don’t have it yet.

gem install bundler

2. Add Gemfile

We will add a Gemfile to specify the gems we will need. Mostly we would need only the Capistrano gem, but I needed the ed25519 gem as well, as I am using a ed25519 SSH key.

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "capistrano", '~> 3.11.2'
gem 'ed25519', '>= 1.2', '< 2.0'
gem 'bcrypt_pbkdf', '>= 1.0', '< 2.0'

3. Do bundle install

bundle install

This will install all the required gems including Capistrano.

4. Initiate Capistrano on the project

bundle exec cap install

This command would create a few files inside your project directory, with some boilerplate code for deploying your code. You basically have to tweak them as per your requirements.

Few files that this would create are:

  • Capfile – main Capistrano file
  • config/deploy.rb – main deploy script
  • config/deploy/staging.rb – staging specific Capistrano directives
  • config/deploy/production.rb – production specific

5. Tweak Capistrano

Now, all you need to do is, modify these files to tell Capistrano where to find your project code (Git repo), server details, where you want to upload the files on the server and what files need symlinked etc.

Here is what ours look like for deploying a Django app that uses nginx, gunicorn and celery stack.

Capfile

# Load DSL and set up stages
require "capistrano/setup"

# Include default deployment tasks
require "capistrano/deploy"

require "capistrano/scm/git"
install_plugin Capistrano::SCM::Git

config/deploy.rb

lock "~> 3.11.2"

set :application, "ApplicationName"
set :repo_url, "[email protected]:group/sub-group/app.git"
set :keep_releases, 10

append :linked_files, "app_name/local_settings.py" # could be .env or any file you probably use for config variables
append :linked_dirs, "media"

set :deploy_to, "/var/www/app_name"
set :ssh_options, forward_agent: true

namespace :deploy do

  desc "Run post-deploy actions (migrate and collect static)"
  task :post_deploy do
    invoke 'deploy:install_deps'
    invoke 'deploy:migrate'
    invoke 'deploy:collect_static'
    invoke 'deploy:restart'
  end

  desc "Install dependencies"
  task :install_deps do
    on roles(:app), in: :sequence, wait: 5 do
      within release_path do
        execute("source #{fetch :venv_path}/bin/activate")
        execute :pip, :install, '-r', 'requirements.txt'
      end
    end
  end

  desc "Migrate database"
  task :migrate do
    on roles(:app), in: :sequence, wait: 5 do
      within release_path do
        execute :python, 'manage.py', 'migrate', '--no-input'
      end
    end
  end

  desc "Collect static"
  task :collect_static do
    on roles(:app), in: :sequence, wait: 5 do
      within release_path do
        execute :python, 'manage.py', 'collectstatic', '--no-input'
      end
    end
  end

  desc "Restart Gunicorn"
  task :restart do
    on roles(:app), in: :sequence, wait: 5 do
      execute :sudo, :service, 'gunicorn', :restart
    end
  end
end

after 'deploy:finished', 'deploy:post_deploy'

config/deploy/staging.rb

server "myapp.com", user: "username", roles: %w{app db web}
set :deploy_user, 'username'

set :branch, "staging"
set :stage, :staging

set :venv_path, "/home/username/venvs/app-name"
SSHKit.config.command_map[:python] = "#{fetch :venv_path}/bin/python"
SSHKit.config.command_map[:pip] = "#{fetch :venv_path}/bin/pip"

config/deploy/production.rb

server "myapp.com", user: "username", roles: %w{app db web}
set :deploy_user, 'username'

set :branch, "master"
set :stage, :production

set :venv_path, "/home/username/venvs/app-name"
SSHKit.config.command_map[:python] = "#{fetch :venv_path}/bin/python"
SSHKit.config.command_map[:pip] = "#{fetch :venv_path}/bin/pip"

6. Ready to deploy

You need to make sure that the directory mentioned in the deploy_to directive has been created and has the required file permissions.

Now, lets check if everything is in order. The following command would check if Capistrano can reach your git repo, SSH to server and verify folder permissions.

bundle exec cap staging deploy:check

At this point, you could simply, run a deployment to your staging or production servers using the following commands, respectively.

bundle exec cap staging deploy

bundle exec cap production deploy

More commands are available:

bundle exec cap -T

7. Auto deploy using GitLab CI/CD

Now that we can run deployments from your local machine, create a .gitlab-ci.yml file on the root of your project directory to tell GitLab to auto deploy your app.

image: ruby:2.6

stages:
  - deploy

deploy_staging:
  stage: deploy
  environment:
    name: staging
    url: https://$STAGING_HOST
  before_script:
    - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client git -y )'
    - eval $(ssh-agent -s)
    - echo "$DEPLOY_PRIVATE_KEY" | base64 -d | ssh-add -
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    - ssh-keyscan $STAGING_HOST >> ~/.ssh/known_hosts
    - chmod 644 ~/.ssh/known_hosts
    - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
    - git config --global user.email "[email protected]"
    - git config --global user.name "Name"
    - gem install bundler
    - bundle install
  script:
    - bundle exec cap staging deploy
  only:
    - staging

deploy_production:
  stage: deploy
  when: manual
  environment:
    name: production
    url: https://$PRODUCTION_HOST
  before_script:
    - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client git -y )'
    - eval $(ssh-agent -s)
    - echo "$DEPLOY_PRIVATE_KEY" | base64 -d | ssh-add -
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    - ssh-keyscan $PRODUCTION_HOST >> ~/.ssh/known_hosts
    - chmod 644 ~/.ssh/known_hosts
    - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
    - git config --global user.email "[email protected]"
    - git config --global user.name "Name"
    - gem install bundler
    - bundle install
  script:
    - bundle exec cap production deploy
  only:
    - master

Head to Settings -> CI/CD under your project in GitLab and add the following under Variables.

  • DEPLOY_PRIVATE_KEY You need to generate a new SSH key pair and hash the private key using base64, and then copy the private key as the value here. Add the public key to your servers’ authorized keys file.
  • PRODUCTION_HOST – domain or IP of your app. e.g. www.example.com
  • STAGING_HOST

Now, that we have created the Gitlab CI file, try pushing to your repo, and Gitlab should pick it up and start deploying to your server.

Related Posts

Begin typing your search term above and press enter to search. Press ESC to cancel.

Back To Top