Dustin Davis
That's me

We just moved into a new house about a month ago. This is actually the second time we have built. I had forgot how much work a new home is. There is so much to be done. One big project we need done is landscaping the backyard. 

Since we have pretty much run out of money with all our other upgrades, I decided that rather than go into debt to get it done, I could perhaps find a landscaper willing to do some trade work. I could build a website for their business in return for their landscaping services.

I’ve actually done trade work once in the past. I needed my house painted and posted an ad on Craigslist. I had an artist come and paint my house and I built a custom website for him in return to feature his artwork. I think after all the iterations and custom work I did, he came out far ahead.

But, I really need a backyard, so I wrote up a few on craigslist. I got a couple of bites. The first one was someone who wanted me to call them. I emailed them instead and never heard back from them. If I can’t communicate with someone via email, I don’t think we will be able to work well together.

The second person who contacted me was all about every plant being editable and water-wise. She said if I liked to use bug spray and non-organic fertilizer then she didn’t feel we would be a good fit. I didn’t feel we would be a good fit.

So I figured it was time to just find someone and get a bid. I subscribed to Angie’s list and sent emails to about 6 different landscapers. I didn’t hear from any of them. It must be a busy time of year. My wife called a few places and actually got one person to return her call. 

But then something crazy happened. Some neighbors came to visit and welcome us into the area. When I told them what I did for a living, one of them’s eyes lit up and said “We need to talk.” I actually hear this a lot. Since I haven’t really done freelance work in many years I usually just let them know I’m not in the freelance business. But, when he said he owned a landscaping company and needed a website and that he was interested in trade work, my eyes lit up. “Yes, we need to talk!”

So we are going to work with each other. Hopefully it all goes well. My wife tends to get nervous when I do business with neighbors because she is afraid that something may go bad and cause hurt feelings and awkward relationships. I tend to look at it as more of a blessing. I love trade work. There is something appealing for about trading skills for skills and work for work.

So what do you think? Have you ever done trade work? How did it go? Would you do it again?

Recently Luke lamented that his favorite ebook reader was being sunset. I suggested Kindle. I was showing off the features I liked about Kindle when I realized that while I could send personal documents (PDF, MOBI) to my kindle devices (Kindle, iPad, iPhone), these personal documents were not showing up in Kindle for Mac or Kindle Cloud Reader. These apps only let me read kindle books I have purchased on Amazon.

This morning I got the following email from Amazon:

Dear Kindle Customer,

As a past user of personal documents on Kindle devices or reading apps, we are pleased to let you know about some improvements we’ve made to how personal documents work.

Personal documents are now in Amazon Cloud Drive: Starting today, all personal documents that you have archived in your Kindle Library will be available to access, delete, organize, and share from your Amazon Cloud Drive. You can see these documents in a new “My Send-to-Kindle Docs” folder alongside all of your saved content such as photos and personal videos.

There is no action required on your part. Your personal documents features will continue to work just as they have in the past. And as always, you can use Manage Your Kindle to see a list of your documents, re-deliver them to Kindle devices and free reading apps, delete them, or turn off auto-saving of documents to the cloud. Documents will be delivered just as they have in the past and you will continue to have 5 GB of free cloud storage for your personal documents. Just “Send Once, Read Everywhere.”

Documents stored in their native format: Also starting today, new documents that you save to the cloud with Send to Kindle will be stored in their native format (e.g. MS Word, TXT) so you can access them anywhere from Amazon Cloud Drive.

Please note: Your usage of Amazon Cloud Drive is subject to the Amazon Cloud Drive Terms of Use.

Sincerely,
The Docs Team

To learn more about sending documents, news, blogs, and other web articles to your Kindle, please visit amazon.com/sendtokindle

To learn more about Amazon Cloud Drive features and apps, please visit amazon.com/clouddrive/learnmore

I learned a number of things from this email.

First, I didn’t even know there was an Amazon Cloud Drive app that essentially works like Dropbox, Copy, Google Drive, etc. So I installed it. What’s one more?

After installing it, I had a cloud drive folder and it synced some MP3′s I had purchased from Amazon in the past. It also created a folder that contained all my personal docs I had syned to my kindle devices.

Still, I didn’t see a way to automatically see these docs in my Kindle for Mac app.

Fortunately, I found an easy hack to make it work.

How to sync personal kindle docs with your local kindle app

In the Kindle for Mac app, you can sent your content folder in your preferences. Just set this to the Amazon Cloud Drive folder that contains all your personal Kindle docs. Done!

Still, I don’t think it will sync furthest location read between devices. Hopefully this is addressed at some point.

Sending personal docs to kindle

The other thing I learned is that there are easier ways to send personal document to kindle. While the email message is nice, it’s not idea. I downloaded the Send To Kindle app that lets you simply drag & drop a file. It will even try to convert PDF files to Kindle files. I’m not sure how well it works though.

Good job Kindle & Docs teams. Keep up the good work!

There are pros and cons for both sides of website development, using an “out of the box” PHP platform such as WordPress or hiring a website developer to create your custom website has elements to examine before making your decision.

WordPress Design Features:

WordPress is free, but offers advanced customized features for a small extra fee. You have full-access to create your custom website with user-friendly building tools for webpages and blogging site pages. WordPress has an open-source blogging tool with a CMS (content management system), based on PHP with MySQL. If the opinion of the masses can sway your decision, there are 60 million websites using WordPress, including 18.9 percent of the Top-ten million best websites online today. As of February 3, 2014, WordPress version 3.8 (released December 12, 2013) has been downloaded over 16 million times.

WordPress is evaluated as an efficient and versatile platform, website templates use a template processor and features thousands of themes using PHP, CSS and HTML coding. Advanced features can be added or edited during or after the build. The versatility of WordPress allows customization and tailoring to the specific needs of your website using 26,000 thousand plug-ins available.

With the development of WordPress versions 3.0, 3.8 and WordPress MU – “Multi-user”, website owners can host their own blogging communities. New website features include a dashboard feature and eight new data tables for each blog. The administrator can control and moderate all blogs from a single dashboard.

WordPress’ last release in 2013, version 3.8, has an improved administrative interface. The main dashboard has been simplified and it has a better responsive design for mobile devices.

WordPress Challenges:

Vulnerabilities that have been addresses recently in WordPress 3.0 involved security issues. These issues were detected in systems that had not been upgraded. In June, 2013, 50 of the most downloaded WordPress plug-ins were subjected to common SQL injection and XSS Web attacks. Also, seven out of the ten Top e-commerce plug-ins were vulnerable to Web Attacks.

There are current remedies to prevent identified vulnerabilities to Web Attacks such as editing the site’s .htaccess file to prevent SQL injection, this also blocks sensitive files from being accessed.

Web Design Comparative:

From the side of affordablity, WordPress wins, the basic cost is Free and even with additional customization, the cost is much less than a standard fee for a professional web-designer set up. As the most used website and blogging site platforms on the Web today, WordPress is a proven platform for reliability and trustworthy service.

From the side of custom web design and individuality of presentation, professional web designers offer custom-looks that can surpassed the somewhat “cookie-cutter” looks of WordPress. No other website will have the custom looks a professional web designer can build according to your needs and specifications. Their custom layouts, size of designing elements, presentations using custom fonts, colors, highlights and shades, plus custom designed graphics will stand out to impress your customers.

Challenges of Using a Professional Website Developer:

The things about using a web designer is you really don’t know who is writing your code. They may be exaggerating their experience level and you wouldn’t know until problems develop. Even at the end of the build, you may not have what you asked for. Sometimes they know all the right words to say, but when it comes to the finished product, it will be lacking in comprehensive functionality.

For some people, taking a chance on a web designer is worth the price to get a more unique web design. In that case try to get referrals from people who have used this developer for their sites. Even with this recommendation, there is still a risk as you still have no way to confirm who wrote the coding for each application on your site.

Ultimately, WordPress services millions of website owners and takes responsibility to provide quick fixes if a problem develops. This serves as insurance that if your website has a problem you will experience the least amount of downtime possible with a large company backing their product.

Show Your WorkMy junior year of high school I took a calculus class from the local college extension. I happened to get a nice Casio calculator (circa 1994) that allowed me to write programs. Since I didn’t really have a computer, this was my first experience programming.

Each time we learned a new algorithm, I would figure out how to program that algorithm into my calculator. The problem was that once I did, I would promptly forget how to work the problem by hand.

I mostly got B’s on my tests. I would get all the answers right, but I would often get docked for not showing my work. How do you show your work when the only way you remember how to solve a problem is to plug numbers in to the program you wrote on your calculator?

I was thinking about this recently. Today I might be accused of the opposite. When I leave a comment in a bug or feature ticket, I might be accused of sharing too much information. The business person that submitted the bug in our ticket system doesn’t really need to see all the queries I ran, the results of those queries and a documented process of how I came to find my solution. They just wanted it fixed.

But here’s the deal: I don’t write all that information for them. I write it for me.

Like the algorithms I programmed into my calculator in high school, I will promptly forget what I worked on two days ago and the process I followed. If I run into a similar problem, finding the ticket where I documented my process of finding a solution in the past is often much faster than coming up with that solution (or a new solution) again. This is also why I blog.

So, if you are going to err, err on the side of sharing too much information. Even if nobody cares, you will thank yourself later. On top of that, the business person requesting the bug fix might just learn a little more about your system in the process.

Lately I was getting this error frequently as I was using Django’s built in cache_page decorator to cache some views.

memcache in check_key
MemcachedKeyLengthError: Key length is > 250

Basically the problem is that Memcached only allows a 250 char key and some of my view names were pretty long and so it was creating keys greater than 250 chars.

I found a quick fix to hash the key with an md5 hash if the key is going to be over 250 characters. You can modify the function that creates the key.

In my settings file I added the following:

import hashlib

...

def hash_key(key, key_prefix, version):
    new_key = ':'.join([key_prefix, str(version), key])
    if len(new_key) > 250:
        m = hashlib.md5()
        m.update(new_key)
        new_key = m.hexdigest()
    return new_key

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
        'LOCATION': '127.0.0.1:11211',
        'KEY_FUNCTION': hash_key,
    }
}

The reason why I only hash if the key is going to be over 250 characters is because 1) hashing is CPU intensive and I only want to do it when I have to; 2) I prefer to have my memcached keys human readable when possible; 3) less likely to have collision problems with duplicate hashes.

I thank Russell Keith-Magee for these tips.

Even after coding in Python for the past five years I’ve never really considered myself an expert in the language because I find the more I know, the more I know I don’t know. I generally keep my code simple on purpose until I have a good reason to be complex – which for most django sites, I haven’t had a good reason to be complex.

Today I had good reason. I’m currently building a number of key performance indicator (KPI) stats for Neutron. There are currently 46 different stats that I need to calculate for 5 different time periods.

For each state I need:

  • Stats for start of current day to current time with a comparison to yesterday start of day to the current time.
  • This week compared to last week delta
  • This month compared to last month delta
  • This quarter compare to last quarter delta
  • This year compared to last year delta

I will be building a view for each stat and associated time period to return these values in JSON format. So as it stand there will be 230 views. I needed to come up with something clever to save myself some lines of code. I opted for class based views.

First I built a base class that will return the JSON data in a consistent format:

class StatWithDelta(BaseDetailView):
    start = None
    end = None
    delta_start = None
    delta_end = None
    title = None
    subtitle = None

    def __init__(self):
        super(StatWithDelta, self).__init__()
        self.end = djtz.localtime(djtz.now())

    def value(self):
        raise NotImplementedError

    def delta(self):
        raise NotImplementedError

    def get(self, request, *args, **kwargs):
        value = self.value()
        delta_value = self.delta()
        try:
            delta_percent = round((((delta_value - value) / value) * 100), 2)
        except ZeroDivisionError:
            delta_percent = 0
        payload = {
            'value': value,
            'delta': delta_percent,
            'title': self.title,
            'subtitle': self.subtitle,
        }
        return self.render_to_response(payload)

    def render_to_response(self, context):
        return self.get_json_response(self.convert_context_to_json(context))

    def get_json_response(self, content, **httpresponse_kwargs):
        return http.HttpResponse(content,
                                 content_type='application/json',
                                 **httpresponse_kwargs)

    def convert_context_to_json(self, context):
        return json.dumps(context)

Next I built classes for each required time range. Here is my class for today compared to yesterday:

class TodayYesterday(StatWithDelta):
    subtitle = 'Today vs. Yesterday'

    def __init__(self):
        super(TodayYesterday, self).__init__()
        self.start = self.end.replace(hour=0, minute=0, second=0, microsecond=0)
        self.delta_start = self.start - datetime.timedelta(days=1)
        self.delta_end = self.end - datetime.timedelta(days=1)

Now for each stat I create a class that gets the main value and its delta value. Here is one example:

class GrossMarginPercent(StatWithDelta):
    title = 'Gross Margin Percent'

    def value(self):
        return functions.gross_margin_percent_within(self.start, self.end)

    def delta(self):
        return functions.gross_margin_percent_within(
            self.delta_start, self.delta_end)

I thought this was clever, but then I found myself writing a lot of similar code. I would create a class based view for each stat class and time period, then an associated url mapping. So for the stat class above I would have these five classes:

class GrossMarginPercentDay(GrossMarginPercent, TodayYesterday):
    pass


class GrossMarginPercentWeek(GrossMarginPercent, ThisWeekLastWeek):
    pass


class GrossMarginPercentMonth(GrossMarginPercent, ThisMonthLastMonth):
    pass


class GrossMarginPercentQuarter(GrossMarginPercent, ThisQuarterLastQuarter):
    pass


class GrossMarginPercentYear(GrossMarginPercent, ThisYearLastYear):
    pass

… and these urls:

    url(r'^edu/gmp-dtd/$', GrossMarginPercentDay.as_view()),
    url(r'^edu/gmp-wtd/$', GrossMarginPercentWeek.as_view()),
    url(r'^edu/gmp-mtd/$', GrossMarginPercentMonth.as_view()),
    url(r'^edu/gmp-qtd/$', GrossMarginPercentQuarter.as_view()),
    url(r'^edu/gmp-ytd/$', GrossMarginPercentYear.as_view()),

You can see the lines of code adding up. I was going to add 230+ lines of code to my urls.py file and 4600 lines of code to my views.py file (20 * 230) following PEP8 guidelines.

So I decided to use one url pattern to send to one view function to dynamically create each of the stat-period classes. Here is my new url pattern:

    url(r'^(?P<category>[\w\-]+)/(?P<period>day|week|month|quarter|year)/'
        r'(?P<base_class_name>\w+)/$', 'magic_view'),

And here is my “magic_view” function that where the *magic* happens:

def magic_view(request, category, period, base_class_name):
    """
    Builds a dynamic class subclassing the base class name passed in and a time 
    period class. It will return its as_view() method.

    URL structure: /category/period/KPI_Class/

    category: KPI category (edu, conversion, etc.) not really used at this point
    period: day, week, month, quarter, year
    KPI Class: One of the class names in this file
    """
    class_name = '{}{}'.format(base_class_name, period.capitalize())
    _module = sys.modules[__name__]
    base_cls = getattr(_module, base_class_name)
    if period == 'day':
        period_name = 'TodayYesterday'
    else:
        period_name = 'This{0}Last{0}'.format(period.capitalize())
    period_cls = getattr(_module, period_name)
    
    # Create a dynamic class based on the base class and time period class
    cls = type(class_name, (base_cls, period_cls), dict())
    return cls.as_view()(request)

So if you include all the comments lines to explain why I did, I’m only using 25 lines of code to save 4830 lines. That’s a lot of typing. Python, my fingers thank you!

A friend pointed me to this simple yet humorous website yesterday which essentially gives a new lazy coder excuse whenever the page is refreshed.

I couldn’t help but whip out a bot to plug in to our IRC channel. My lazy coder bot will give a random excuse whenever someone mentions the word “why”.

I used my Rollbot script as a base to write this up quickly.

requirements.txt

Twisted==13.1.0
beautifulsoup4==4.2.1
requests==1.2.3

because.py

from bs4 import BeautifulSoup
import requests
from twisted.words.protocols import irc
from twisted.internet import protocol, reactor

NICK = '_lazy_coder_'
CHANNEL = '#yourchannel'
PASSWORD = 'channel_password'

class MyBot(irc.IRCClient):
    def _get_nickname(self):
        return self.factory.nickname
    nickname = property(_get_nickname)

    def signedOn(self):
        self.join(self.factory.channel)
        print "Signed on as {}.".format(self.nickname)

    def joined(self, channel):
        print "Joined %s." % channel

    def privmsg(self, user, channel, msg):
        """
        Whenever someone says "why" give a lazy programmer response
        """
        if 'why' in msg.lower():
            # get lazy response
            because = self._get_because()

            # post message
            self.msg(CHANNEL, because)

    def _get_because(self):
        req = requests.get('http://developerexcuses.com/')
        soup = BeautifulSoup(req.text)
        elem = soup.find('a')
        return elem.text.encode('ascii', 'ignore')

class MyBotFactory(protocol.ClientFactory):
    protocol = MyBot

    def __init__(self, channel, nickname=NICK):
        self.channel = channel
        self.nickname = nickname

    def clientConnectionLost(self, connector, reason):
        print "Lost connection (%s), reconnecting." % reason
        connector.connect()

    def clientConnectionFailed(self, connector, reason):
        print "Could not connect: %s" % reason

if __name__ == "__main__":
    channel = CHANNEL
    if PASSWORD:
        channel += ' {}'.format(PASSWORD)
    reactor.connectTCP('irc.freenode.net', 6667, MyBotFactory(channel))
    reactor.run()

*UPDATE: I’ve made some minor modifications and posted the project on Github

I’m not afraid to admit, I’m a visual guy. I like GUI interfaces. Sequel Pro makes it very easy to SSH tunnel into a server and connect to MySQL, but there is nothing I have found built into pgAdmin3 to use SSH tunneling for connections.

Luckily I found it is simple enough to do.

First, open an ssh tunnel:

ssh -fNg -L 5555:localhost:5432 {your_username}@{yourdomain.com}

This open an SSH connection in the background mapping your local port 5555 to your server’s port 5432 (Postgres’ default port). Type “man ssh” to see what each of these flags is specifically doing.

Now, create a new connection in pgAdmin using localhost as your host and port 5555.

New pgAdmin Connection

Have you ever wanted to give your model some month choices relating to integers 1-12. I would guess it’s pretty common – common enough to be included in django.contrib. Well, it is. Here is a quick tip on how to include it in a model:

from django.db import models
from django.utils.dates import MONTHS


class RevenueGoal(models.Model):
    month = models.PositiveSmallIntegerField(choices=MONTHS.items())
    year = models.PositiveIntegerField()
    goal = models.DecimalField('Revenue Goal', max_digits=8, decimal_places=2)

Disclaimer: I am not a sysadmin. I’m just a developer. I welcome and encourage comments to improve this process!

I have set up a couple of Django servers lately and taken copious notes that I have extracted from various sources. Below are the commands I issue to a fresh Ubuntu server install to get Django up and running. This puts everything on one server (PostgreSQL, Celery, RabbitMQ, etc) so it’s nice for a small starter project but don’t expect it to scale.

Log in as root and add a non-root user. Add the user to the sudoers group. Log out and log back in as ‘username’.

adduser username
adduser username sudo
exit

Update the local package index. Upgrade all the packages that can be upgraded. Remove packages that are no longer needed and then reboot for good measure.

sudo apt-get update
sudo apt-get dist-upgrade
sudo apt-get autoremove
sudo reboot

Install libraries for Python, PIP, PIL/Pillow, PostgreSQL, libevent for gevent, memcached server and library, RabbitMQ, git, nginx, & supervisor

sudo apt-get install build-essential python-dev python-pip libjpeg8-dev libfreetype6-dev zlib1g-dev postgresql postgresql-contrib libpq-dev libevent-dev memcached libmemcached-dev rabbitmq-server git nginx supervisor

Install virtualenv and virtualenvwrapper. To enable it, we need to add a line to our .bashrc file and log out and back in.

sudo pip install virtualenv virtualenvwrapper
echo "" >> .bashrc
echo "source /usr/local/bin/virtualenvwrapper.sh" >> .bashrc
exit

Make a virtualenv

mkvirtualenv project_env

Install postgres adminpack

sudo -u postgres psql
CREATE EXTENSION "adminpack";
\q

Change postgres password & create database

sudo passwd postgres
sudo su - postgres
psql -d template1 -c "ALTER USER postgres WITH PASSWORD 'changeme';"
createdb projectdb
createuser username --pwprompt
psql -d template1 -U postgres
GRANT ALL PRIVILEGES ON DATABASE projectdb to username;
\q
exit

Install RabbitMQ

sudo rabbitmqctl add_user username username_pw
sudo rabbitmqctl add_vhost username_vhost
sudo rabbitmqctl set_permissions -p username_vhost username ".*" ".*" ".*"
sudo rabbitmqctl clear_permissions -p username_vhost guest

Generate ssh key to upload to Github, Bitbucket, or wherever you host your code.

ssh-keygen -t rsa -C you@sample.com
cat ~/.ssh/id_rsa.pub

Create some /var/www dirs & set permissions on these directories.

sudo mkdir -p /var/www/static
sudo mkdir /var/www/media
sudo chown -R username:www-data /var/www

Clone your repository to your home directory and install the packages in your requirements file.

git clone git@bitbucket.org:yourusername/project.git
cd project/requirements
pip install -r prod.txt

Remove the default symbolic link for Nginx. Create a new blank config, and make a symlink to it. Edit the new configuration file.

sudo rm /etc/nginx/sites-enabled/default
sudo touch /etc/nginx/sites-available/project
cd /etc/nginx/sites-enabled
sudo ln -s ../sites-available/project
sudo vim /etc/nginx/sites-available/project

Add the following content to nginx config:

# define an upstream server named gunicorn on localhost port 8000
upstream gunicorn {
    server localhost:8000;
}

# make an nginx server
server {
    # listen on port 80
    listen 80;

    # for requests to these domains
    server_name yourdomain.com www.yourdomain.com;

    # look in this directory for files to serve
    root /var/www/;

    # keep logs in these files
    access_log /var/log/nginx/project.access.log;
    error_log /var/log/nginx/project.error.log;

    # You need this to allow users to upload large files
    # See http://wiki.nginx.org/HttpCoreModule#client_max_body_size
    # I'm not sure where it goes, so I put it in twice. It works.
    client_max_body_size 0;

    # THIS IS THE IMPORTANT LINE
    # this tries to serve a static file at the requested url
    # if no static file is found, it passes the url to gunicorn
    try_files $uri @gunicorn;

    # define rules for gunicorn
    location @gunicorn {
        # repeated just in case
        client_max_body_size 0;

        # proxy to the gunicorn upstream defined above
        proxy_pass http://gunicorn;

        # makes sure the URLs don't actually say http://gunicorn 
        proxy_redirect off;

        # If gunicorn takes > 5 minutes to respond, give up
        # Feel free to change the time on this
        proxy_read_timeout 5m;

        # make sure these HTTP headers are set properly
        proxy_set_header Host            $host;
        proxy_set_header X-Real-IP       $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}
server {
    listen  443 ssl;
    
    # start mine
    ssl on;
    ssl_certificate /etc/ssl/localcerts/yourdomain_com.crt;
    ssl_certificate_key /etc/ssl/localcerts/yourdomain.com.key;
    ssl_protocols        SSLv3 TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers          HIGH:!aNULL:!MD5:!kEDH;
	server_name  yourdomain.com www.yourdomain.com;
	
    # look in this directory for files to serve
    root /var/www/;

    # keep logs in these files
    access_log /var/log/nginx/project.access.log;
    error_log /var/log/nginx/project.error.log;

    # You need this to allow users to upload large files
    # See http://wiki.nginx.org/HttpCoreModule#client_max_body_size
    # I'm not sure where it goes, so I put it in twice. It works.
    client_max_body_size 0;

    # THIS IS THE IMPORTANT LINE
    # this tries to serve a static file at the requested url
    # if no static file is found, it passes the url to gunicorn
    try_files $uri @gunicorn;

    # define rules for gunicorn
    location @gunicorn {
        # repeated just in case
        client_max_body_size 0;

        # proxy to the gunicorn upstream defined above
        proxy_pass http://gunicorn;

        # makes sure the URLs don't actually say http://gunicorn 
        proxy_redirect off;

        # If gunicorn takes > 5 minutes to respond, give up
        # Feel free to change the time on this
        proxy_read_timeout 5m;

        # make sure these HTTP headers are set properly
        proxy_set_header Host            $host;
        proxy_set_header X-Real-IP       $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Restart nginx

sudo service nginx restart

Set up database

cd /home/username/project
python manage.py syncdb --settings=project.settings.prod
python manage.py migrate --settings=project.settings.prod

Run collectstatic command

python manage.py collectstatic -l --noinput --settings=project.settings.prod
sudo /etc/init.d/nginx restart

Configure supervisor

Add the following contents to /etc/supervisor/conf.d/celeryd.conf

sudo vim /etc/supervisor/conf.d/celeryd.conf

Contents:

# the name of this service as far as supervisor is concerned
[program:celeryd]

# the command to start celery
command = /home/username/.virtualenvs/project_env/bin/python /home/username/project/manage.py celeryd -B -E --settings=project.settings.prod

# the directory to be in while running this
directory = /home/username/project

# the user to run this service as
user = username

# start this at boot, and restart it if it fails
autostart = true
autorestart = true

# take stdout and stderr of celery and write to these log files
stdout_logfile = /var/log/supervisor/celeryd.log
stderr_logfile = /var/log/supervisor/celeryd_err.log

Now we will create CeleryCam in /etc/supervisor/conf.d/celerycam.conf

sudo vim /etc/supervisor/conf.d/celerycam.conf

Contents:

[program:celerycam]
command = /home/username/.virtualenvs/project_env/bin/python /home/username/project/manage.py celerycam --settings=project.settings.prod
directory = /home/username/project
user = username
autostart = true
autorestart = true
stdout_logfile = /var/log/supervisor/celerycam.log
stderr_logfile = /var/log/supervisor/celerycam_err.log

Create Gunicorn script in /etc/supervisor/conf.d/gunicorn.conf

sudo vim /etc/supervisor/conf.d/gunicorn.conf

Contents:

[program:gunicorn]
command = /home/username/.virtualenvs/project_env/bin/python /home/username/project/manage.py run_gunicorn -w 4 -k gevent --settings=project.settings.prod
directory = /home/username/project
user = username
autostart = true
autorestart = true
stdout_logfile = /var/log/supervisor/gunicorn.log
stderr_logfile = /var/log/supervisor/gunicorn_err.log

Restart supervisor

sudo service supervisor restart

Restart/stop/start all services managed by supervisor

sudo supervisorctl restart all
sudo supervisorctl stop all
sudo supervisorctl start all

Or restart just celeryd

sudo supervisorctl restart celeryd

Or, start just gunicorn

sudo supervisorctl start gunicorn

Reboot and make sure everything starts up

sudo reboot

Bonus: set up ssl

sudo mkdir /etc/ssl/localcerts
cd /etc/ssl/localcerts
sudo openssl req -new -nodes -days 365 -keyout yourdomain.com.key -out yourdomain.com.csr
sudo chmod 400 /etc/ssl/localcerts/yourdomain.com.key
sudo chmod 400 /etc/ssl/localcerts/yourdomain.com.crt

Clicky