MacOS – deploy the Django site to OS X Server

macososx-serverpythonweb app

I want to run my Django website on OS X Server 3.2.1 running on Mavericks.

These more detailed considerations arise from my question.

  1. Can I deploy into OS X Server 3.2.1 without interfering with the reliability of OS X Server's own apps?
  2. Can I use the Server App's GUI to install new sites and manage existing ones?
  3. Can I use Django as a web app that I can 'bind' to other websites in the manner of OS X Server's "hello world" python web app?
  4. Can I use different python versions and use virtualenvs to manage package versions?
  5. Can I use UNIX Domain Sockets for interprocess communication between OS X Server's http server and a wsgi server like you can between Nginx and Gunicorn?
  6. Can I use Fabric to automate Django deployment to OS X Server?

Best Answer

wrt the issues above:

  1. Yes you can. Very effectively. Just be aware that your Django sites won't integrate into Server App's GUI unless you restrict yourself to python 2.7; Apache mod_wsgi; no virtualenvs.
  2. Not if you wish to proxy to a wsgi server like Gunicorn, because when you save the Website's panel, OS X Server will sanitise some of your essential Apache directives and you will lose control over how your Django static files, for example, are served. OS X Server has a particular execution and configuration model in mind and it does a good job of managing websites that accord with that model - but at the cost of flexibility. I know that people successully deploy Django using Apache's mod_wsgi, but that comes with severe limitations.
  3. As in 2. The proxies section of the plist for your web app in OS X Server's ../apache2/webapp directory is all-or-nothing. e.g. You can't Include a ProxyPass /static/ ! in the right place for it to have effect.
  4. Not with Apache mod_wsgi.
  5. No. OS X Server 3.2.1 uses Apache 2.2. Proxies via UDS was not included in Apache until later. (Perhaps you could have Apache proxy to Nginx (via a TCP port) and, in turn, proxying to Gunicorn via each Django site's own UDS ... if you thought there was a compelling reason.)
  6. Yes.

The approach I took:

  • Put your application's Apache .conf file in OS X Server's ../apache2/other directory where it will be left alone by the Server app.
  • Forget about trying to use the Server GUI and 'binding' web apps to sites through its Advanced settings window. A nice idea but ...
  • Use virtualenv(wrapper) to set up the different python and package versions for a more future-proof approach ... OS X Server 4 on Yosemite coming up!

Example code:

.conf for Django virtual server

This is adapted directly from the file that OS X Server generates from its GUI with these main differences:

  1. ProxyPreserveHost is off by default.

  2. ProxyPass /static/ ! prevents the Alias directive from being ignored. I found that, when including this directive using the Apple-recommended web app .plist includeFiles section, the Included lines were in the wrong place to have the desired effect.

sed your SITENAME and PORT:

<VirtualHost *:80>
        ServerName SITENAME
        ServerAdmin mail@you.com
        DocumentRoot "/usr/local/python_projects/SITENAME"
        ErrorLog /var/log/apache2/error_log
        <IfModule mod_ssl.c>
                SSLEngine Off
                SSLCipherSuite "ALL:!aNULL:!ADH:!eNULL:!LOW:!EXP:RC4+RSA:+HIGH:+MEDIUM"
                SSLProtocol -ALL +SSLv3 +TLSv1
                SSLProxyEngine On
                SSLProxyProtocol -ALL +SSLv3 +TLSv1
        </IfModule>
        <Directory "/usr/local/python_projects/SITENAME">
                Options All -Indexes -ExecCGI -Includes +MultiViews
                AllowOverride None
                <IfModule mod_dav.c>
                        DAV Off
                </IfModule>
        </Directory>
        ProxyPass         /static/ !
        Alias             /static/ /usr/local/python_projects/SITENAME/static/
        ProxyPass         /         http://localhost:PORT/
        ProxyPassReverse  /         http://localhost:PORT/
        ProxyPreserveHost On
</VirtualHost>

LaunchDaemon plist for Gunicorn wsgi server

sed your SITENAME; REVERSED_SITENAME and PORT:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC -//Apple Computer//DTD PLIST 1.0//EN
http://www.apple.com/DTDs/PropertyList-1.0.dtd >
<plist version="1.0">
<dict>
     <key>Label</key>
     <string>REVERSED_SITENAME</string>
     <key>ProgramArguments</key>
     <array>
          <string>/usr/local/virtualenvs/SITENAME/bin/gunicorn</string>
          <string>--bind=127.0.0.1:PORT</string>
          <string>--workers=2</string>
          <string>superlists.wsgi:application</string>
     </array>
     <key>RunAtLoad</key><true/>
     <key>WorkingDirectory</key><string>/usr/local/python_projects/SITENAME/source</string>
<!--      <key>StandardErrorPath</key><string>/var/log/gunicorn/REVERSED_SITENAME.error.log</string> -->     
     <key>KeepAlive</key><true/>
</dict>
</plist>

Snippet from the OS X Server's .bash_exports

Shows virtualenv paths specified by the virtualenvwrapper environment variables

export WORKON_HOME=/usr/local/virtualenvs
export PROJECT_HOME=/usr/local/python_projects
export VIRTUALENVWRAPPER_PYTHON=/usr/local/bin/python3
export VIRTUALENVWRAPPER_VIRTUALENV=/usr/local/bin/virtualenv
source /usr/local/bin/virtualenvwrapper.sh

Snippet from the Fabric automation file fabfile.py

Shows:

  1. The target paths - the directories where your configuration files will end up;

  2. The commands to activate the Django site in the final steps in deployment

# copy (and overwrite if necssary) configuration files to the LaunchDaemons and apache2/sitesdirectories

sudo('cp -f %s /Library/LaunchDaemons/%s.plist' % (gunicorn_config_file, reversed_site_name))
sudo('cp -f %s /Library/Server/Web/Config/apache2/other/%s.conf' % (apache2_config_file, site_name))

# Get the gunicorn wsgi server running on its unique TCP port 

sudo('launchctl unload -w /Library/LaunchDaemons/%s.plist' % (reversed_site_name,), warn_only=True)
sudo('launchctl load -w /Library/LaunchDaemons/%s.plist' % (reversed_site_name,))  

# Give us some assurance that the gunicorn port is open

sudo('lsof -i TCP:%s' % (port,))

# Get Apache to include the new website; talk to gunicorn; and make it available to he world!

sudo('apache2ctl graceful')