I’m working on a small project, it started as simple showcase using only Django, but soon enough I needed more interaction and started a simple front-end in Ember. So now I have to projects, front-end and backend. Django actually morphed to be only an API endpoint, keeping my data on a database and handling a few other things.
So I need to deploy this, on a really basic server, no fancy clouds and CDN stuff. First thought was to deploy to two different servers, but using the same instance of nginx. But them I would have to handle CORS issues, and what not.
That was kind of bothering me… then I found Luke Melia talk on Lightning Fast Deployment of Your Rails-backed JavaScript app. And it just clicked, problem solved. Applying his ideas to Django where really straightforward. I just needed a view, a simple model for storing the current index, and a static folder to store all this. Nginx will server all static files and Django just need to serve the index.html
, enabling me to use its templating system.
Django
Model for handling the current page in use:
class IndexPage(models.Model): hash = models.CharField(max_length=10) index_name = models.CharField(max_length=40) is_current = models.BooleanField(default=False) def save(self, *args, **kwargs): if self.is_current: IndexPage.objects.filter(is_current=True).update(is_current=False) super(IndexPage, self).save(*args, **kwargs)
The view that is mapped as the default on urls.py
file:
def static_index_view(request): hash_id = request.GET.get('hash_id', '') index = IndexPage.objects.get(is_current=True) if hash_id: try: index = IndexPage.objects.get(hash=hash_id) except IndexPage.DoesNotExist: pass logger.debug("Using index: %s" % index.hash) path = os.path.normpath(os.path.join(settings.BASE_DIR, '../static')) logger.debug(path) return render_to_response(index.index_name, dirs=[path, ])
Django deployment stayed pretty much the same, minus a few extra libraries that weren’t needed anymore and a few paths that changed. I’ve added a few management commands to handle adding, listing and setting the current index page, really basic stuff.
Ember
The easiest part around, just build and upload to the server.
ember build --environment=production
Copy the contents to your server static root after ember build
finishes. I’ve automated that using flightplan, it works like Fabric, but it’s all javascript. One issue of flightplan is that it doesn’t ask for passwords while doing ssh or sudo – not really a bad thing, just extra configuration needed. My flightplan
config is something like this:
var plan = require('flightplan'); plan.target('staging', { host: '10.1.1.50', username: 'stage', agent: process.env.SSH_AUTH_SOCK }); var digest, archiveName; plan.local(['deploy', 'build'], function(local) { local.log("Removing previous build."); local.rm('-rf dist'); local.log("Building app..."); local.exec("ember build dist --environment=production") digest = local.exec("shasum dist/index.html | cut -c 1-8").stdout.replace(/[\n\t\r]/g,"");; local.mv("dist/index.html dist/index."+ digest +".html"); archiveName = "my-project." + digest + ".tar.gz"; local.with("cd dist", function() { local.tar('-czvf ../' + archiveName + ' *') }); }); plan.local(['deploy', 'upload'], function(local) { local.log("Uploading app..."); var input = local.prompt('Ready for deploying to ' + plan.target.destination + '? [yes]'); if (input.indexOf('yes') === -1) { local.abort('user canceled flight'); // this will stop the flightplan right away. } local.log("Current digest: " + digest); local.transfer(archiveName, '/opt/django/apps/my-project/static'); }); plan.remote(['deploy', 'extract'], function(remote) { remote.with('cd apps/my-project/static', function() { remote.tar('-xzf '+ archiveName); }); }); plan.remote(['deploy', 'config'], function(remote) { remote.log("Configure app... digest: " + digest); remote.with('cd apps/my-project', function() { remote.with('source bin/activate', function() { remote.exec('./my-project/manage.py indexadd '+ digest + ' index.' + digest + '.html'); remote.log('Added new index.'); var input = remote.prompt('Make this release current? [yes]'); if (input.indexOf('yes') === 0) { remote.exec('./my_project/manage.py indexsetcur '+ digest); } }); }); }); plan.remote('list-indexes', function(remote) { remote.with('cd apps/my-project', function() { remote.with('source bin/activate', function() { remote.exec('./my_project/manage.py indexlist'); }) }); });
Nginx Configuration
Nginx gave me a few headaches, because I was also using the PushStream module, but in the end I finally found a good enough solution for running both Django and statically serving Ember files. My config is the following, which is pretty much basic:
upstream my_project_backend { server unix:/opt/django/run/my_project.sock fail_timeout=0; } server { # listen 80 default deferred; # for Linux # listen 80 default accept_filter=httpready; # for FreeBSD listen 80; client_max_body_size 4G; server_name my-project.local; # ~2 seconds is often enough for most folks to parse HTML/CSS and # retrieve needed images/icons/frames, connections are cheap in # nginx so increasing this is generally safe... keepalive_timeout 5; # path for static files root /opt/django/apps/my-project/static; access_log /opt/django/logs/nginx/my_project_access.log; error_log /opt/django/logs/nginx/my_project_error.log; location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # enable this if and only if you use HTTPS, this helps Rack # set the proper protocol for doing redirects: # proxy_set_header X-Forwarded-Proto https; proxy_set_header Host $http_host; proxy_redirect off; # proxy_buffering off; # Try to serve static files from nginx, no point in making an # *application* server like Unicorn/Rainbows! serve static files. if (!-f $request_filename) { proxy_pass http://my_project_backend; break; } } }
After all this was in place, refreshing the page was giving me a 404 – Django was trying to find a view for the current url, but it only existed in Ember. To fix that I’ve added to my urls.py
the following:
from django.conf.urls import handler404 from api.views import static_index_view handler404 = static_index_view
And that fixed my issue, it’s not the most elegant way, but it works!