Hosting multiple apps at digital ocean droplet

Posted:   |  More posts about php python hosting WIP devops sysadmin

This is to document a setup that I plan on a digital ocean droplet. It should allow us to host applications of different platforms to co-exists side by side.
This initially inspired by dokku setup but dokku still has some rough edges making it not ready yet for production setup. This setup simply eliminate [docker] and run application natively on the host instead inside a container.

Based on diagram below, we'll use nginx as our frontend server. It'll not do much other than forwarding requests to the backend apps running on different port. Each app will run inside specific user account so in theory it should allow us to host apps for multiple users. Some planning on port assignment is needed however in case you want to go in this route. For example user1 will use port range 10000 and user2 using port 11000 space.

If you want to skip the write up and straightly get your hand dirty, just clone the github repo and fix all the path to suit your environment.

We'll start with installing all the required packages first. This assume you already logged to the server as root:-

apt-get install nginx libapache2-mod-php5 php5-gd php5-sqlite python-dev
apt-get install python-virtualenv supervisor
update-rc.d apache2 disable

We disable apache from being started at startup since we're not going to use the default setup. Each apps will run their own minimal instance of apache.
You'll see some errors after the installation, apache2 failed to start since nginx already used the port 80.


Open `/etc/nginx/nginx.conf, remove existing config and add the following config:-

user www-data;
worker_processes 4;
pid /var/run/;

events {
        worker_connections 768;
        # multi_accept on;

http {
        sendfile on;
        tcp_nopush on;
        tcp_nodelay on;
        keepalive_timeout 65;
        types_hash_max_size 2048;
        # server_tokens off;

        server_names_hash_bucket_size 64;
        # server_name_in_redirect off;

        include /etc/nginx/mime.types;
        default_type application/octet-stream;

        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;

        gzip on;
        gzip_disable "msie6";

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;
        include /home/user1/webapps/*/nginx/nginx.conf;

It's the last line that really matter so if you want to keep the existing config, just add the last line. Test the config by running nginx -t. If no errors shown, we're done with nginx.


We'll keep all our apps in a specific user account. Begin by creating the user account:-

adduser --disabled-login --gecos '' user1

We disabled user login since we're not going to log in to this account directly. The option --gecos '' will skip the interactive prompt asking for full name, phone number etc.

Switch to the newly created user to start setting up the initial app layout.

su user1
mkdir -p /home/user1/webapps/drupal
mkdir -p /home/user1/webapps/drupal/apache2
mkdir -p /home/user1/webapps/drupal/nginx
mkdir -p /home/user1/webapps/drupal/app/public

We'll put all our apps inside a folder called webapps inside the user's home directory. Our first app will be a PHP app and I took drupal as an example.
Before we can setup drupal, we have to configure our PHP environment first.


Create a directory to hold the apache2 environment for our PHP app.

cd /home/user1/webapps/drupal
# create minimal apache2 config
cat - > apache2.conf
ServerRoot /home/user1/webapps/drupal/apache2
Listen 10000
LockFile apache2.lock
TypesConfig /etc/mime.types

LoadModule authz_host_module /usr/lib/apache2/modules/
LoadModule dir_module /usr/lib/apache2/modules/
LoadModule mime_module /usr/lib/apache2/modules/
LoadModule rewrite_module /usr/lib/apache2/modules/
LoadModule php5_module /usr/lib/apache2/modules/

LogLevel info
ErrorLog "|cat"
LogFormat "%h %l %u %t \"%r\" %>s %b" common
CustomLog "|cat" common

DocumentRoot "/home/user1/webapps/drupal/app/public"
<Directory "/home/user1/webapps/drupal/app/public">
  AllowOverride all
  Order allow,deny
  Allow from all

AddType application/x-httpd-php .php
DirectoryIndex index.html index.php

Notice that we run this apache2 instance at port 10000. Now let's test running the instance:-

apache2 -d /home/user1/webapps/drupal/apache2 -f apache2.conf -e info -DFOREGROUND

You should see the apache process running. Type CTRL-C to stop it. Even though we manage to run our apache instance now, it's still not accessible from outside yet. Let's configure nginx to proxy request from outside to this instance. Add our specific nginx config for our app in /home/user1/webapps/drupal/nginx/nginx.conf:-

upstream user1-drupal { server; }
server {
  listen      80;
  location    / {
    proxy_pass  http://user1-drupal;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $http_host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Forwarded-Port $server_port;

Test our nginx config to make sure nothing go wrong and then restart it:-

nginx -t
service nginx restart
apache2 -d /home/user1/webapps/drupal/apache2 -f apache2.conf -e info -DFOREGROUND

If nothing goes wrong, we can access our website now at We should get Not Found error from browser. Add index.php to our public folder to verify we can properly execute PHP script.

To avoid having to type the lengthy command in order to start apache2, let's wrap it into a simple script. Save it in /home/user1/webapps/drupal/apache2/


exec apache2 -d /home/user1/webapps/drupal/apache2 -f apache2.conf -e info -DFOREGROUND

While our apache instance now running, it's not yet permanent, mean when we close our console or our ssh connection drop, it will stop. We'll use process manager called Supervisor to turn our apache2 process into a daemon.



Create new supervisor config file in /home/user1/etc/supervisord.conf:-




supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

files = /home/user1/webapps/*/supervisor/supervisor.conf

Next add supervisor config specific to our app in /home/user1/webapps/drupal/supervisor/supervisor.conf:-

command=pidproxy /home/user1/webapps/drupal/apache2/ /home/user1/webapps/drupal/apache2/

Above config allow us to manage the apache2 process using supervisorctl command:-

supervisorctl -c ~/etc/supervisord.conf start drupal
supervisorctl -c ~/etc/supervisord.conf status
drupal                           RUNNING    pid 840, uptime 18:43:55
supervisorctl -c ~/etc/supervisord.conf stop drupal
drupal: stopped

There's one issue with running apache2 process under supervisor. Apache2 create a child processes and supervisor cannot control all these child processes. That mean when we stop the process using supervisor, the child processes will keep running and servicing our website as usual. The workaround is to launch the process using pidproxy as shown in the above config.

To easily manage the supervisor, download this script and put it in /home/user1/bin/ Then you should be able to control supervisord daemon as:-

$HOME/bin/ start
$HOME/bin/ status
$HOME/bin/ stop

This supervisord daemon is still not started when the server reboot. For now I'll just call the script to start daemon from cron every 10 minutes:-

crontab -l
# m h  dom mon dow   command
*/10 * * * * /home/user1/bin/ start


Python app should run inside a virtualenv to isolate it from system python. This allow us to install packages required only for our app and changes to system wide python packages shouldn't affect our app. For this example, we'll use Mezzanine, a Content Management System that quite popular in Python.

Add nginx config for our python app first. The file in /home/user1/webapps/mezzanine/nginx/nginx.conf should look like:-

upstream user1-mezzanine { server; }
server {
  listen      80;
  location    / {
    proxy_pass  http://user1-mezzanine;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $http_host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Forwarded-Port $server_port;
mkdir -p /home/user1/webapps/mezzanine/app
cd /home/user1/webapps/mezzanine
virtualenv venv
./venv/bin/pip install mezzanine
cd app
../venv/bin/mezzanine-project myproject
cd myproject
../../venv/bin/python createdb
../../venv/bin/python runserver 10001

Our python app running at port 10001 and nginx now would correctly proxied request to to the django development server running for our app.


Django development server is not meant to run in production so in order to serve our python app, we'll use gunicorn, one of the many WSGI server available in Python. Begin by installing gunicorn:-

cd /home/user1/webapps/mezzanine/app
../venv/bin/pip install gunicorn
../venv/bin/gunicorn -b myproject.wsgi:application

Our app now being served through gunicorn but still have one problem. Gunicorn will only run python files but not static files such as css, js or images. While we can configure nginx to also serve the static files, I'd prefer not to do that since the nginx process is a system wide process - it shouldn't do more than just proxying the request to backend server. For now we'll took simple approach and serve the static files as part of our python app. We need a little package call dj-static that provide thin middleware to serve all the static files.

../venv/bin/pip install dj-static

We have to modify our file a bit. Make sure it look like below:-

import os

PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
settings_module = "%s.settings" % PROJECT_ROOT.split(os.sep)[-1]
os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_module)

from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()

from dj_static import Cling
application = Cling(application)

Our python app running under gunicorn should be ready now. The final part is to manage it under supervisor. Add new entry to our supervisor.conf file:-




nginx: [emerg] could not build the server_names_hash, you should increase server_names_hash_bucket_size: 32 server_names_hash_bucket_size 64;

Comments powered by Disqus

About me

Web developer in Malaysia. Currently work at MARIMORE Inc building internet services using Python and Django web framework.

ImportError is an error message emitted by Python when it failed to load certain module as requested by programmer. It's a very common error when someone new to the language trying it out. This website on the same theme, will try provide help for newcomers on any technologies to overcome their first hurdle.

Try most of the examples you may find here on Digital Ocean cloud service. They provide excellent VPS at a very cheaper price. Using this referral link you'll get USD10 credits upon sign up. That's enough to run single VPS with 1GB RAM for a month.


I can also be found at the following sites:-



The postings on this site are my own and don't necessarily represent my employer's positions, strategies or opinions.