├── .gitignore ├── README.md ├── Vagrantfile ├── debian ├── compat ├── control ├── etc │ ├── init │ │ └── django_app.conf │ ├── nginx │ │ └── sites-enabled │ │ │ └── django_app.conf │ └── uwsgi │ │ └── django_app.ini ├── helloworld.substvars ├── helloworld.triggers ├── install ├── postinst └── rules ├── fabfile.py ├── requirements.txt ├── setup.py ├── src ├── helloworld │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── manage.py └── vagrant ├── bootstrap_packaging_box.sh ├── bootstrap_test_box.sh └── devpi.conf /.gitignore: -------------------------------------------------------------------------------- 1 | # Scratch files 2 | TODO 3 | 4 | # Vagrant 5 | .vagrant 6 | 7 | # Build artifacts 8 | debian/dist/*.deb 9 | 10 | # Python packaging 11 | build/ 12 | dist/ 13 | 14 | # Debian packaging 15 | debian/helloworld/ 16 | debian/helloworld.*.log 17 | debian/changelog 18 | debian/files 19 | debian/usr/share/static/ 20 | 21 | # Python version file 22 | src/helloworld/version.py 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Packaging a Django site as a .deb file 2 | 3 | This repo is an example Django project which can be built and deployed as a 4 | single `.deb` file. The package includes the project virtualenv as well as 5 | uWSGI, nginx and upstart config files. 6 | 7 | Deploying using native packages has [many advantages](https://hynek.me/articles/python-app-deployment-with-native-packages/) 8 | and hopefully this repo can serve as a template for other Django projects. 9 | 10 | ## Quick start 11 | 12 | Clone this repository and install the development tools: 13 | 14 | $ git clone https://github.com/codeinthehole/django-deb-file.git 15 | $ cd django-deb-file 16 | $ fab package 17 | 18 | ## Building the package 19 | 20 | To add a Python package, install it using `pip install $package` then update 21 | `requirements.txt` using `pip-dump`. 22 | 23 | Once installed, the python package will be stored in `/usr/share/python/$package/` 24 | 25 | To run a `manage.py` command, use: 26 | 27 | $ DJANGO_SETTINGS_MODULE=helloworld.settings /usr/share/python/helloworld/bin/django-admin shell 28 | 29 | Eg, to run migrations: 30 | 31 | $ /usr/share/python/helloworld# DJANGO_SETTINGS_MODULE=helloworld.settings bin/django-admin migrate 32 | 33 | ## Configuration 34 | 35 | Environment variables are read from an optional `/etc/django_app.env` file. By 36 | default, the `settings.py` module looks for the following env vars: 37 | 38 | * `SECRET_KEY` 39 | * `DEBUG` (set this to `0` to disable debug mode) 40 | * `ALLOWED_HOSTS` - a comma-separated list of domains 41 | 42 | Use your favourite configuration management software to put this file in place. 43 | At a minimum, you could use an AWS "user data" script like the following: 44 | 45 | ```bash 46 | #!/usr/bin/env bash 47 | 48 | cat < /etc/django_app.env 49 | DEBUG = 0 50 | ALLOWED_HOSTS = "mydomain.com,subdomain.mydomain.com" 51 | SECRET_KEY = "a459ckak1k23la2039ddkd" 52 | SETTINGS 53 | ``` 54 | 55 | ## Package structure 56 | 57 | Python packages are stored in `/usr/share/python/$package`. 58 | 59 | Static files are stored in `/usr/share/static/` and are served directly by 60 | Nginx. 61 | 62 | ## Useful commands 63 | 64 | List contents of a package: 65 | 66 | $ dpkg -c file.deb 67 | 68 | List available versions of a package: 69 | 70 | $ apt-cache madison 71 | 72 | uWSGI is configured to provide statistics and the uwsgitop commmand is included 73 | in the package. To view what uWSGI is doing, use: 74 | 75 | $ /usr/share/python/helloworld/bin/uwsgitop /tmp/uwsgi.stats.sock 76 | 77 | ## Customising 78 | 79 | This sample project is called "helloworld", this is the name of both: 80 | 81 | - The Python package (hence why the package files are installed in 82 | `/usr/share/python/helloworld/`) 83 | 84 | - The Debian package 85 | 86 | If you start with this repo, you'll want to rename the following things: 87 | 88 | - `src/helloworld` - the Python package location, this also requires path 89 | changes in `etc/init/django_app.conf`, `etc/uwsgi/django_app.ini` 90 | - `setup.py` - change the `name` kwarg 91 | - `debian/control` - change the Debian package name 92 | - `debian/helloworld.install` 93 | - `debian/helloworld.postinst` 94 | - `debian/helloworld.triggers` 95 | 96 | ## Installing 97 | 98 | Use the [deb-s3](http://invalidlogic.com/2013/02/26/managing-apt-repos-on-s3/) 99 | library to upload the `.deb` file into S3. You'll first need to create a S3 100 | bucket that is readable by the destination server. Then run: 101 | 102 | $ deb-s3 upload --preserve-packages --bucket .deb 103 | 104 | to upload the package. On the destination server, append: 105 | 106 | deb https://.s3.amazonaws.com stable main 107 | 108 | to `/etc/apt/sources.list` then install using: 109 | 110 | $ apt-get update 111 | $ apt-get install 112 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! 5 | VAGRANTFILE_API_VERSION = "2" 6 | 7 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 8 | config.vm.box = "ubuntu/trusty64" 9 | 10 | # Bring up multiple machines - see http://docs.vagrantup.com/v2/multi-machine/ 11 | 12 | # A machine for building the .deb file 13 | config.vm.define "packaging" do |packaging| 14 | config.vm.provision "shell", path: "vagrant/bootstrap_packaging_box.sh" 15 | end 16 | 17 | # A machine for testing installation the .deb file 18 | config.vm.define "test" do |test| 19 | config.vm.provision "shell", path: "vagrant/bootstrap_test_box.sh" 20 | 21 | # Forward port 80 which Nginx listens to 22 | config.vm.network "forwarded_port", guest: 80, host: 8080 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: helloworld 2 | Section: python 3 | Priority: extra 4 | Maintainer: Build-bot 5 | Build-Depends: debhelper (>= 9), 6 | python, 7 | dh-virtualenv (>= 0.6), 8 | python-dev 9 | Standards-Version: 3.9.5 10 | 11 | Package: helloworld 12 | Architecture: amd64 13 | Pre-Depends: dpkg (>= 1.16.1), python2.7, ${misc:Pre-Depends} 14 | Depends: nginx (>= 1.4.6) 15 | Description: This is a sample Django project 16 | -------------------------------------------------------------------------------- /debian/etc/init/django_app.conf: -------------------------------------------------------------------------------- 1 | description "Django application" 2 | 3 | start on runlevel [2345] 4 | stop on runlevel [06] 5 | 6 | respawn 7 | 8 | exec /usr/share/python/helloworld/bin/uwsgi --ini /etc/uwsgi/django_app.ini 9 | -------------------------------------------------------------------------------- /debian/etc/nginx/sites-enabled/django_app.conf: -------------------------------------------------------------------------------- 1 | upstream django { 2 | server unix:///tmp/uwsgi.app.sock; 3 | } 4 | 5 | server { 6 | listen 80 default_server; 7 | server_name localhost; 8 | charset utf-8; 9 | 10 | # Serve static files 11 | location /static/ { 12 | alias /usr/share/static/; 13 | expires max; 14 | } 15 | 16 | location / { 17 | uwsgi_pass django; 18 | include /etc/nginx/uwsgi_params; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /debian/etc/uwsgi/django_app.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | master = true 3 | processes = 2 4 | 5 | socket = /tmp/uwsgi.app.sock 6 | chmod-socket = 664 7 | uid = www-data 8 | gid = www-data 9 | 10 | virtualenv = /usr/share/python/helloworld/ 11 | module = helloworld.wsgi:application 12 | 13 | stats = /tmp/uwsgi.stats.sock 14 | 15 | max-requests = 5000 16 | vacuum = True 17 | die-on-term 18 | -------------------------------------------------------------------------------- /debian/helloworld.substvars: -------------------------------------------------------------------------------- 1 | shlibs:Depends=libc6 (>= 2.15), libexpat1 (>= 2.0.1), libpython2.7 (>= 2.7), zlib1g (>= 1:1.2.0) 2 | misc:Depends= 3 | -------------------------------------------------------------------------------- /debian/helloworld.triggers: -------------------------------------------------------------------------------- 1 | # Register interest in Python interpreter changes (Python 2 for now); and 2 | # don't make the Python package dependent on the virtualenv package 3 | # processing (noawait) 4 | interest-noawait /usr/bin/python2.6 5 | interest-noawait /usr/bin/python2.7 6 | 7 | # Also provide a symbolic trigger for all dh-virtualenv packages 8 | interest dh-virtualenv-interpreter-update 9 | -------------------------------------------------------------------------------- /debian/install: -------------------------------------------------------------------------------- 1 | debian/etc/uwsgi/django_app.ini /etc/uwsgi/ 2 | debian/etc/init/django_app.conf /etc/init/ 3 | debian/etc/nginx/sites-enabled/django_app.conf /etc/nginx/sites-enabled/ 4 | debian/usr/share/static/* /usr/share/static/ 5 | -------------------------------------------------------------------------------- /debian/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Start/restart Upstart job 4 | SERVICE=django_app 5 | 6 | if $(status $SERVICE | grep -q "start/running") 7 | then 8 | # Service already running - restart 9 | restart $SERVICE 10 | else 11 | # Start service 12 | start $SERVICE 13 | fi 14 | 15 | # Remove default nginx conf 16 | if [ -h /etc/nginx/sites-enabled/default ] 17 | then 18 | unlink /etc/nginx/sites-enabled/default 19 | fi 20 | 21 | # Reload nginx 22 | echo "Restarting nginx" 23 | service nginx configtest > /dev/null && service nginx restart > /dev/null 24 | 25 | #DEBHELPER# 26 | exit 0 27 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | %: 4 | dh $@ --with python-virtualenv 5 | 6 | override_dh_virtualenv: 7 | dh_virtualenv --pypi-url http://127.0.0.1:3141/root/pypi/ 8 | -------------------------------------------------------------------------------- /fabfile.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | import shutil 4 | import datetime 5 | 6 | import pytz 7 | from fabric import api 8 | 9 | PACKAGE = "helloworld" 10 | 11 | TEMP_FOLDERS = ( 12 | "build", 13 | "dist", 14 | "debian/usr/share/static", 15 | "debian/%s" % PACKAGE, 16 | "src/%s.egg-info" % PACKAGE, 17 | ) 18 | 19 | TEMP_FILES = ( 20 | "debian/%s.debhelper.log" % PACKAGE, 21 | "debian/changelog", 22 | "debian/files", 23 | "src/%s/version.py" % PACKAGE, 24 | "src/db.sqlite3", 25 | ) 26 | 27 | DEBIAN_CHANGELOG_TEMPLATE = """PACKAGE (VERSION) unstable; urgency=medium 28 | 29 | * DESCRIPTION 30 | 31 | -- AUTHOR BUILD_DATE 32 | """ 33 | 34 | 35 | def clean(): 36 | """ 37 | Remove temporary files 38 | """ 39 | for folder in TEMP_FOLDERS: 40 | if os.path.exists(folder): 41 | shutil.rmtree(folder) 42 | for file in TEMP_FILES: 43 | if os.path.exists(file): 44 | os.unlink(file) 45 | for file in glob.glob("*.deb"): 46 | os.unlink(file) 47 | 48 | 49 | def package(): 50 | """ 51 | Build a .deb file 52 | """ 53 | clean() 54 | 55 | now = datetime.datetime.now(pytz.timezone("Europe/London")) 56 | version = now.strftime("%Y%m%d%H%M%S") 57 | 58 | _update_python_package_version(version) 59 | _update_debian_changelog(now, version) 60 | 61 | print "Collecting static files" 62 | api.local("src/manage.py collectstatic --noinput") 63 | 64 | print "Building package in Vagrant box" 65 | api.local('vagrant up packaging') 66 | api.local('vagrant ssh packaging -- "cd /vagrant/ && sudo dpkg-buildpackage -us -uc"') 67 | 68 | # dpkg-buildpackage creates the .deb file in the root folder (/) on the 69 | # file system. We need to move that into the shared folder (/vagrant) so we 70 | # can access it from the host machine. 71 | api.local('vagrant ssh packaging -- "sudo mv /*.deb /vagrant/debian/dist/"') 72 | 73 | 74 | def _update_python_package_version(version): 75 | print "Updating python package version" 76 | version_file = "src/%s/version.py" % PACKAGE 77 | with open(version_file, "w") as f: 78 | f.write("__version__ = '%s'" % version) 79 | 80 | 81 | def _update_debian_changelog(now, version): 82 | print "Updating debian changelog" 83 | replacements = { 84 | 'PACKAGE': PACKAGE, 85 | 'VERSION': version, 86 | 'DESCRIPTION': "Built from commit %s" % _commit_sha(), 87 | 'BUILD_DATE': now.strftime("%a, %d %b %Y %H:%M:%S %z"), 88 | 'AUTHOR_EMAIL': 'dev@example.com', 89 | 'AUTHOR': 'Fabric', 90 | } 91 | contents = DEBIAN_CHANGELOG_TEMPLATE 92 | for variable, replacement in replacements.items(): 93 | contents = contents.replace(variable, replacement) 94 | with open("debian/changelog", "w") as f: 95 | f.write(contents) 96 | 97 | 98 | def _commit_sha(): 99 | return api.local("git rev-parse HEAD", capture=True)[:8] 100 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django-dotenv==1.3.0 2 | Django==1.8.3 3 | pip-tools==0.3.6 4 | simplejson==3.7.3 5 | uWSGI==2.0.11 6 | uwsgitop==0.8 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup, find_packages 3 | import os 4 | 5 | PACKAGE_NAME = "helloworld" 6 | VERSION_FILEPATH = "src/%s/version.py" % PACKAGE_NAME 7 | 8 | if os.path.exists(VERSION_FILEPATH): 9 | exec(open(VERSION_FILEPATH).read()) 10 | else: 11 | __version__ = '0.1' 12 | 13 | setup( 14 | name=PACKAGE_NAME, 15 | version=__version__, 16 | package_dir={'': 'src'}, 17 | packages=[PACKAGE_NAME], 18 | include_package_data=True, 19 | ) 20 | -------------------------------------------------------------------------------- /src/helloworld/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeinthehole/django-in-a-deb-file/455064d2d607f9aa683e23da5ef412d5936f44e2/src/helloworld/__init__.py -------------------------------------------------------------------------------- /src/helloworld/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for helloworld project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.8.3. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.8/ref/settings/ 11 | """ 12 | 13 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 14 | import os 15 | 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | # Use django-dotenv to load env variables 19 | ENV_FILE = os.path.join(BASE_DIR, '/etc/django_app.env') 20 | if os.path.exists(ENV_FILE): 21 | import dotenv 22 | dotenv.read_dotenv(ENV_FILE) 23 | 24 | # Quick-start development settings - unsuitable for production 25 | # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ 26 | 27 | # SECURITY WARNING: keep the secret key used in production secret! 28 | DEFAULT_SECRET_KEY = ')&b+zs8h=d04rvq&z_+6k6mvc27ipn8x15ml_+*!^=f$qswo4!' 29 | SECRET_KEY = os.environ.get("SECRET_KEY", DEFAULT_SECRET_KEY) 30 | 31 | # SECURITY WARNING: don't run with debug turned on in production! 32 | DEBUG = bool(int(os.environ.get("DEBUG", "1"))) 33 | 34 | ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "").split(",") 35 | 36 | # Application definition 37 | 38 | INSTALLED_APPS = ( 39 | 'django.contrib.admin', 40 | 'django.contrib.auth', 41 | 'django.contrib.contenttypes', 42 | 'django.contrib.sessions', 43 | 'django.contrib.messages', 44 | 'django.contrib.staticfiles', 45 | ) 46 | 47 | MIDDLEWARE_CLASSES = ( 48 | 'django.contrib.sessions.middleware.SessionMiddleware', 49 | 'django.middleware.common.CommonMiddleware', 50 | 'django.middleware.csrf.CsrfViewMiddleware', 51 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 52 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 53 | 'django.contrib.messages.middleware.MessageMiddleware', 54 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 55 | 'django.middleware.security.SecurityMiddleware', 56 | ) 57 | 58 | ROOT_URLCONF = 'helloworld.urls' 59 | 60 | TEMPLATES = [ 61 | { 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 63 | 'DIRS': [], 64 | 'APP_DIRS': True, 65 | 'OPTIONS': { 66 | 'context_processors': [ 67 | 'django.template.context_processors.debug', 68 | 'django.template.context_processors.request', 69 | 'django.contrib.auth.context_processors.auth', 70 | 'django.contrib.messages.context_processors.messages', 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | WSGI_APPLICATION = 'helloworld.wsgi.application' 77 | 78 | 79 | # Database 80 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 81 | 82 | DATABASES = { 83 | 'default': { 84 | 'ENGINE': 'django.db.backends.sqlite3', 85 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 86 | } 87 | } 88 | 89 | 90 | # Internationalization 91 | # https://docs.djangoproject.com/en/1.8/topics/i18n/ 92 | 93 | LANGUAGE_CODE = 'en-us' 94 | 95 | TIME_ZONE = 'UTC' 96 | 97 | USE_I18N = True 98 | 99 | USE_L10N = True 100 | 101 | USE_TZ = True 102 | 103 | 104 | # Static files (CSS, JavaScript, Images) 105 | # https://docs.djangoproject.com/en/1.8/howto/static-files/ 106 | 107 | STATIC_URL = '/static/' 108 | 109 | 110 | # HELLOWORLD OVERRIDES 111 | # -------------------- 112 | # Everything above this heading is stock Django settings - no customisations 113 | 114 | # Log to a local SQLite file in /tmp (for now) 115 | DATABASES = { 116 | 'default': { 117 | 'ENGINE': 'django.db.backends.sqlite3', 118 | 'NAME': '/tmp/db.sqlite3', 119 | } 120 | } 121 | 122 | STATIC_ROOT = os.path.join(BASE_DIR, '../debian/usr/share/static') 123 | -------------------------------------------------------------------------------- /src/helloworld/urls.py: -------------------------------------------------------------------------------- 1 | """URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.8/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Add an import: from blog import urls as blog_urls 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) 15 | """ 16 | from django.conf.urls import include, url 17 | from django.contrib import admin 18 | 19 | urlpatterns = [ 20 | url(r'^admin/', include(admin.site.urls)), 21 | ] 22 | -------------------------------------------------------------------------------- /src/helloworld/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for helloworld project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "helloworld.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /src/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "helloworld.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /vagrant/bootstrap_packaging_box.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Set-up the Vagrant box for BUILDING debian packages 4 | 5 | apt-get update 6 | apt-get install dh-virtualenv devscripts debhelper python-dev python-pip -y 7 | pip install -U pip devpi-server 8 | 9 | # Use upstart to manage devpi process 10 | cp /vagrant/vagrant/devpi.conf /etc/init/ 11 | start devpi 12 | -------------------------------------------------------------------------------- /vagrant/bootstrap_test_box.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Set-up the Vagrant box for TESTING debian packages 4 | 5 | apt-get update 6 | apt-get install gdebi -y 7 | -------------------------------------------------------------------------------- /vagrant/devpi.conf: -------------------------------------------------------------------------------- 1 | description "DevPi PyPI proxy" 2 | 3 | start on runlevel [2345] 4 | stop on runlevel [06] 5 | 6 | respawn 7 | 8 | exec /usr/local/bin/devpi-server 9 | --------------------------------------------------------------------------------