Full-time developer on a small team. Writing great specs and code, delivered on time. Chase is a DZone MVB and is not an employee of DZone and has posted 54 posts at DZone. You can read more from them at their website. View Full User Profile

Deploying Django Staticfiles to Heroku via Hudson/Jenkins

05.02.2012
| 6471 views |
  • submit to reddit

Starting in Django 1.3, you can use the built-in staticfiles feature to bundle up css, javascript, images and other static resources for deployment to a CDN. There are two popular Django apps, django-compressor and django-pipeline that layer in additional functionality such as minification and seemless support for popular hosts like Amazon S3.

These "asset managers" both do the same basic things. They give you a mechanism to bundle multiple css and javascript files into one file, optionally minify the contents, and generate unique files names for the bundled versions. The idea behind unique file names is that you can then tell browsers to cache those files forever; any changes you make will use different URLs for the includes. They will also generate unique files names for images, and then go into your css files and replace the image file paths.

Both libraries are a little immature; I ran into numerous issues trying to get deployment to S3 working. First I tried django-compressor paired with django-storages, which can save directly to S3. Typically django-compressor creates the minified files on the fly, which would work great except that Heroku has an ephemeral file system, meaning that the files would be re-created every time a dyno process restarts. Making matters worse, the built-in S3 boto storage is really slow to sync files up to S3, especially considering most of the files do not change on any given deploy. All in all, I was looking at about 3 minutes of lag every time I wanted to start a dyno.

I tried using their offline compressor, and I actually got it deploying the files. But I could not get django-compressor to use the correct minified URLs; it persistently tried to reference different filenames before and after the deploy, resulting in 404s. Even though it's the most popular framework, I decided to move on and try django-pipeline. Just as well, I'm not sure I agree with their very first design decision anyway, namely that "JS/CSS belong in the templates".

By comparison, django-pipeline was a breeze to set up. Here is my cheat-sheet:

    pip install django-pipeline  
    apt-get install yui-compressor  

Edit settings.py:

    INSTALLED_APPS = (  
        ...  
        'pipeline',  
    )  
      
    PIPELINE = DEBUG  
    PIPELINE_YUI_BINARY = '/usr/bin/yui-compressor'  
    STATICFILES_STORAGE = 'pipeline.storage.PipelineStorage'  
    STATIC_ROOT = '/tmp/myapp-staticfiles'   
    # using a protocol relative URL here so that resources load from http/https accordingly  
    STATIC_URL = '/static/' if DEBUG else '//s3.amazonaws.com/mys3bucket/'    
      
    # the directories stylesheets and javascript should be inside myapp/static,  
    # as per the staticfiles convention. I also put an "images" directory there.  
      
    PIPELINE_CSS = {  
        'base': {  
            'source_filenames': (  
              'stylesheets/reset.css',  
              'stylesheets/base.css',  
            ),  
            'output_filename': 'stylesheets/base.min.css',  
        },  
        'mobile': {  
            'source_filenames': (  
              'stylesheets/reset.css',  
              'stylesheets/mobile.css',  
            ),  
            'output_filename': 'stylesheets/mobile.min.css',  
        },  
    }  
      
    PIPELINE_JS = {  
        'base': {  
            'source_filenames': (  
              'javascript/jquery.min.js',  
              'javascript/jquery-ui.min.js',  
              'javascript/myapp.js',  
            ),  
            'output_filename': 'javascript/base.min.js',  
        },  
        'mobile': {  
            'source_filenames': (  
              'javascript/jquery.min.js',  
            ),  
            'output_filename': 'javascript/mobile.min.js',  
        },  
    }  

For development, you can serve up the static files from /static with the following lines in your urls.py:

    if settings.DEBUG:  
        urlpatterns += patterns('',  
            (r'^static/(?P<path>.*)$', 'django.views.static.serve', {'document_root': settings.STATIC_ROOT}),  
        )  
    </path>  

In your templates, you reference your css/js differently:

    {% load compressed %}  
    <html>  
        <head>  
            {% compressed_css "base" %}  
        </head>  
        <body>  
            <!-- content here -->  
            {% compressed_js "base" %}  
        </body>  
    </html>  

One tricky bit was that with both django-compressor and django-pipeline, I had significant trouble figuring out how to reference images inside my CSS files. I tried various things, with each either only working in my local development environment, or in production. Finally, I tried relative URLs, and it worked. In the past I had always used absolute relative links in the CSS.

    body {  
        background: url(../images/bg.png); /* NOT /static/images/bg.png, or images/bg.png */  
    }  

For deployment, I decided to use Hudson versus trying to get Heroku to do it via hacking Procfile. Here is the deploy script I'm currently using:

    #!/bin/bash  
      
    set -e  
      
    # used to load different settings.py extension file  
    export ENVIRONMENT=production    
      
    # using virtualenv to separate build python class paths  
    source /var/lib/hudson/virtualenv/prod/bin/activate    
    pip install -r requirements.txt  
      
    # this is the static files bit, collect the files and copy them to s3 using the fast s3cmd utility  
    python manage.py collectstatic --noinput  
    s3cmd sync /tmp/myapp-staticfiles/ s3://mys3bucket  # note the trailing slash, critical!  
      
    heroku maintenance:on  
    heroku pgbackups:capture HEROKU_POSTGRESQL_DB --expire  
    git push -f heroku hudsonmerge:refs/heads/master  
    heroku run python manage.py migrate --noinput --merge --ignore-ghost-migrations  
    heroku maintenance:off  

That's it. Currently, my builds take about 90 seconds to deploy, and include zero lag on dyno restarts.

Posted by at 1:46 PM Labels: , ,
Published at DZone with permission of Chase Seibert, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)