How to configure Sass and Bower with django-compressor - part 2 (deployment to Heroku and S3)

Flávio Juvenal
April 15, 2015
<p>This is the part 2 of the guide on how to configure a <strong>Django 1.8</strong> project with <strong>Sass</strong> (django-libsass), <strong>Bower</strong> (django-bower) and <strong>django-compressor</strong>. You can <a href="http://www.vinta.com.br/blog/2015/how-to-configure-sass-and-bower-with-django-compressor.md.html">read the part 1 by clicking here</a>. In this part 2, we'll describe how to deploy this setup to <a href="https://heroku.com">Heroku</a> and <a href="http://aws.amazon.com/s3/">AWS S3</a>.</p><h2 id="example-code">Example code</h2><p>Keep in mind that we are working over the code we've described in the <a href="http://www.vinta.com.br/blog/2015/how-to-configure-sass-and-bower-with-django-compressor.md.html">part 1</a> of this guide. If you want to see right now the complete example of this setup, <a href="https://github.com/vintasoftware/django-sass-bower-compressor-example/tree/with-s3">click here to check the GitHub repository at the branch <code>with-s3</code></a>.</p><h2 id="why-s3">Why S3?</h2><p>Django wasn't made to serve static files, it was made to run application code and respond to complex requests. S3 can host and serve static files more efficiently, since it's tailored for hosting static files. That said, <a href="http://stackoverflow.com/a/25423865/145349">S3 is not a CDN</a>. While you should host your static (CSS, images and scripts for running your app) and media (user files uploaded through your app) files in S3, you might also need a CDN to function as a cache if your app has a high traffic. In AWS, the CDN is complementary to S3 and it is called <a href="http://aws.amazon.com/cloudfront/">CloudFront</a>. But the first step certainly is to save your Heroku server resources by serving static files with S3. So lets do it.</p><h2 id="get-s3-bucket-access-keys">Get S3 Bucket Access Keys</h2><p>If you've already configured S3 for a Django project, you can skip this section. If not, please follow the steps below:</p><ul><li><a href="http://docs.aws.amazon.com/AmazonS3/latest/UG/CreatingaBucket.html">Create a new bucket</a>, and write down its name.</li><li>Create a new IAM user to get access keys: go to <a href="https://console.aws.amazon.com/iam/">AWS IAM</a>, click on <em>Users</em> section, then on <em>Create New Users</em>, then input a name like <code>s3-your-bucket-name</code> and keep <em>Generate an access key for each user</em> checked.</li><li>Now the user is created, click on <em>Show User Security Credentials</em> to get its access keys. Write them down, we will need them for the deployment step. After that, click on <em>Download Credentials</em> for backup purposes.</li><li>After that, we need to give this new user access to the bucket we've just created. Click on this user in the list to see its details, then scroll to <em>Permissions</em>, click on <em>Inline Policies</em>, then on <em>To create one, click here</em>. Choose <em>Custom Policy</em> and <em>Select</em>. Add a <em>Policy Name</em> like <code>s3-your-bucket-name-policy</code> and on <em>Policy Document</em> fill as below (don't forget to replace with your bucket-name where necessary!):</li></ul><pre><code class="language-json">{ "Statement": [ { "Effect": "Allow", "Action": "s3:*", "Resource": [ "arn:aws:s3:::replace-with-your-bucket-name", "arn:aws:s3:::replace-with-your-bucket-name/*" ] } ] } </code></pre><ul><li>Finally, configure the CORS for your bucket (if you don't know what CORS is, <a href="http://www.vinta.com.br/blog/2015/django-cors.html">read here</a>). Access the <a href="https://console.aws.amazon.com/s3/home">S3 bucket list</a>, right click on the bucket, then <em>Properties</em>, after <em>Permissions</em> and on <em>Add CORS Configuration</em> fill as below:</li></ul><pre><code class="language-xml">&lt;?xml version="1.0" encoding="UTF-8"?&gt; &lt;CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"&gt; &lt;CORSRule&gt; &lt;AllowedOrigin&gt;*&lt;/AllowedOrigin&gt; &lt;AllowedMethod&gt;GET&lt;/AllowedMethod&gt; &lt;MaxAgeSeconds&gt;3000&lt;/MaxAgeSeconds&gt; &lt;AllowedHeader&gt;Authorization&lt;/AllowedHeader&gt; &lt;/CORSRule&gt; &lt;/CORSConfiguration&gt; </code></pre><ul><li>Now we can move on to edit your Django project <strong>settings.py</strong>, but first we need to install some dependencies.</li></ul><h2 id="install-the-dependencies-with-pip">Install the dependencies with pip</h2><p>These dependencies are additional to the ones we've seen in <a href="http://www.vinta.com.br/blog/2015/how-to-configure-sass-and-bower-with-django-compressor.md.html">part 1</a> and they're necessary for sending static assets to S3 and deploying app code to Heroku.</p><pre><code>pip install django-storages-redux python-decouple django-toolbelt </code></pre><h2 id="edit-django-settings">Edit Django settings</h2><p>We will use <a href="https://github.com/jschneier/django-storages">django-storages-redux</a> for hosting your static files in S3. <strong>django-storages-redux</strong> is a fork of the original <strong>django-storages</strong>, which is abandoned. Follow the steps below for configuring S3 in Django <strong>settings.py</strong>:</p><ul><li>Add <code>storages</code> to <code>INSTALLED_APPS</code>:</li></ul><pre><code class="language-python">INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'storages', # add this 'djangobower', 'compressor', 'sample_app', ) </code></pre><ul><li>Add settings for AWS, staticfiles and django-compress. Here we are using the <a href="https://github.com/henriquebastos/python-decouple">python-decouple</a> lib, which provides the <code>config</code> function. <code>config</code> gets the setting from environment variables.</li></ul><pre><code class="language-python">from decouple import config, Csv if not DEBUG: ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=Csv()) SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') AWS_ACCESS_KEY_ID = config('AWS_ACCESS_KEY_ID') AWS_SECRET_ACCESS_KEY = config('AWS_SECRET_ACCESS_KEY') AWS_STORAGE_BUCKET_NAME = config('AWS_STORAGE_BUCKET_NAME') AWS_S3_CUSTOM_DOMAIN = '{0}.s3.amazonaws.com'.format(AWS_STORAGE_BUCKET_NAME) STATICFILES_STORAGE = 'sample_app.custom_storages.StaticCachedS3BotoStorage' DEFAULT_FILE_STORAGE = 'sample_app.custom_storages.MediaS3BotoStorage' COMPRESS_STORAGE = STATICFILES_STORAGE STATIC_URL = 'https://{0}/static/'.format(AWS_S3_CUSTOM_DOMAIN) MEDIA_URL = 'https://{0}/media/'.format(AWS_S3_CUSTOM_DOMAIN) COMPRESS_URL = STATIC_URL </code></pre><p><strong>Note that <code>sample_app.custom_storages.StaticCachedS3BotoStorage</code> and <code>sample_app.custom_storages.MediaS3BotoStorage</code> are custom classes available at <a href="https://github.com/vintasoftware/django-sass-bower-compressor-example/blob/with-s3/sample_app/custom_storages.py">custom_storages.py</a>. Copy them to your project.</strong> The <code>StaticCachedS3BotoStorage</code> class has some adjusments necessary to make django-storages work with S3, based on django-storages documentation. It also sets the static directory to <code>/static/</code> inside the bucket, storing static files separate from media files, which prevents overwrites. Meanwhile, the <code>MediaS3BotoStorage</code> class sets the directory to <code>/media/</code> and keeps the media files private with querystring auth, only accesible for some duration after the request. This is usually the desired setting, since media files are generally user uploaded private content.</p><p>After settings are done, we only need to add some files required by Heroku.</p><h2 id="add-heroku-required-files">Add Heroku required files</h2><p>Since this project uses <strong>Bower</strong> (which is node based) and <strong>Django</strong> (Python), we need multiple <a href="https://devcenter.heroku.com/articles/buildpacks">buildpacks</a>. Buildpacks are the scripts which setup the stack necessary to run apps. Please follow the steps below to properly configure Heroku for running your app:</p><ul><li>Create a new Heroku dyno or set the Heroku remote</li><li>Set BUILDPACK_URL to <strong>heroku-buildpack-multi</strong>, since this project uses multiple buildbacks. You can fork the <a href="https://github.com/vintasoftware/heroku-buildpack-multi">Vinta heroku-buildpack-multi repository</a> if you prefer:</li></ul><pre><code class="language-sh">heroku config:set BUILDPACK_URL=https://github.com/vintasoftware/heroku-buildpack-multi.git </code></pre><ul><li>Create a file named <strong>.buildpacks</strong> at the root of the project and fill it with the contents below. This file has the buildpacks necessary to run your Django and Bower setup. You can fork these repositories if you prefer:</li></ul><pre><code>https://github.com/heroku/heroku-buildpack-nodejs.git https://github.com/vintasoftware/heroku-buildpack-python-with-django-bower.git </code></pre><ul><li>Create a <strong>package.json</strong> file in the project root with the content below. This hints node that we need Bower:</li></ul><pre><code>{"private": true,"dependencies": {"bower": "*"}} </code></pre><ul><li>Create a <strong>Procfile</strong> file in the project root with the following content. Replace the <strong>example.wsgi</strong> with the name of your wsgi file. Your Django project should have one:</li></ul><pre><code>web: gunicorn example.wsgi --log-file - </code></pre><ul><li>Now we can set Heroku environment variables with the command below. Fill the AWS variables with values according to the bucket and IAM user you've created:</li></ul><pre><code class="language-sh">heroku config:set DEBUG=False \ AWS_ACCESS_KEY_ID=fill-here \ AWS_SECRET_ACCESS_KEY=fill-here \ AWS_STORAGE_BUCKET_NAME=fill-here \ ALLOWED_HOSTS=fill-here-with-a-*-or-a-value-or-a-comma-separated-list \ SECRET_KEY=fill-here-with-a-django-secret-key </code></pre><p>(<code>ALLOWED_HOSTS</code> and <code>SECRET_KEY</code> aren't related to S3, but it is a good practice to set them as environment variables. <code>ALLOWED_HOSTS</code> can be a <code>*</code> at first, but when all is working, <a href="https://docs.djangoproject.com/en/1.8/ref/settings/#allowed-hosts">fill it with your domain</a>. And you can generate a new <code>SECRET_KEY</code> with <a href="https://gist.github.com/fjsj/92e8687de77917a7c084">a simple Python script</a>)</p><ul><li>Deploy!</li></ul><pre><code class="language-sh">git push heroku with-s3:master # in our case, in your case may be only: git push heroku master </code></pre><p>Please refer the <a href="https://github.com/vintasoftware/django-sass-bower-compressor-example/tree/with-s3">working example project</a> if you have any trouble. Note that the correct branch for this part 2 is called <code>with-s3</code>. If you need additional help, please leave a comment below.</p><h2 id="what-is-next">What is next?</h2><p>Now you have an almost perfect Django frontend setup! The only thing we are missing is using a CDN, which is recommended for web apps with global reach. We will address that in a future post. Follow us on Twitter to keep updated: <a href="https://twitter.com/vintasoftware">@vintasoftware</a>. Thanks!</p>