To complete our system setup we will require 3 additional packages, as we intend to use Django 2+, we'll install the required packages for python 3, let's install them:
> apt-get update > apt-get install python3-pip apache2 libapache2-mod-wsgi-py3
We need to identify the apache runtime user id and guid for later mounting our shared folder:
> id www-data uid=33(www-data) gid=33(www-data) groups=33(www-data)
VirtualBox has chosen not to allow symlinks to work in shared folders, you can learn more on the subject by reading this virtualbox ticket.
This is obviously a major problem as this will prevent you from deploying python virtual environments inside the mounted shared folder (at some point a “read only system” error is generated, so only your projects files can be shared).
You can unfold the following section in case you want to try and use a workaround that works for VirtualBox up to version 4 but doesn't seem to work for the latest one. Also note that this workaround only works on Linux / MacOS hosts, not on Windows hosts.
Setting up shared folders for a development environment
There are a few other ways to share files between your host system and a VM, like NFS or CIFS/SAMBA, you can learn more about those possibilities by reading this VirtualBox article.
Here we'll focus on one last way of sharing files from our host's filesystem to our VM, using SSHFS.
The following steps are inspired by this DigitalOcean article, the main difference being that we'll access our host directory from our guest VM which is the opposite of what's described in the initial article.
First check whether SSHFS is already installed on your VM system:
From your VM system
> apt-cache policy sshfs sshfs: Installed: 2.8-1 Candidate: 2.8-1 Version table: 2.8-1 500 500 http://deb.debian.org/debian stretch/main amd64 Packages 100 /var/lib/dpkg/status
In case you get Installed: (none)
, then you need to install sshfs as follow:
From your VM system
> sudo apt-get install sshfs
You can install SSHFS on Mac OSX. You will need to download FUSE and SSHFS from the osxfuse site
To install SSHFS in Windows you will need to grab the latest win-sshfs package from the google code repository. A direct download link can be found below. After you have downloaded the package, double click to launch the installer. You may be prompted to download additional files, if so the installer will download the .NET Framework 4.0 and install it for you.
https://win-sshfs.googlecode.com/files/win-sshfs-0.0.1.5-setup.exe
As we are willing to set up a Django development environment, we'll first create a user that will be used as our Django reference user:
from the VM console
> adduser django Enter new UNIX password: Retype new UNIX password: passwd: password updated successfully Changing the user information for django Enter the new value, or press ENTER for the default Full Name []: Room Number []: Work Phone []: Home Phone []: Other []: Is the information correct? [Y/n] Y > id django uid=1001(django) gid=1001(django) groups=1001(django)
This will create a new /home/django/
directory inside our VM's filesystem. In this directory, we'll create a mount point, /home/django/projects
, for our development files residing under our host's directory. Let's say that we have our development files located under the following host's directory: /home/devuser/Documents/dev/django
.
in your VM's console
> su django > mkdir /home/django/projects > exit > sshfs devuser@172.20.20.1:/home/devuser/Documents/dev/django /home/django/projects -o allow_other,uid=1001,gid=1001 The authenticity of host '172.20.20.1 (172.20.20.1)' can't be established. ECDSA key fingerprint is SHA256:ck1WSFJeryeGqN86bK3Zci37W8ZrDapOzi/O9aTJo60. Are you sure you want to continue connecting (yes/no)? yes devuser@172.20.20.1's password:
Note that you'll have to uncomment user_allow_other
in /etc/fuse.conf
to be able to use the allow_other
option.
RSA keys
To have our host directory automatically mounted at each startup of our VM, we'll need a few more steps. As we've just experienced, mounting the directory through SSHFS the way we've just done it, we're asked for a password. Automating the process at startup will require authentication using an RSA key, so we'll first prepare RSA keys authentication between the VM and our host system:
as django user in VM
> cd ~/ > ssh-keygen -t rsa Generating public/private rsa key pair. Enter file in which to save the key (/home/ddi/.ssh/id_rsa): Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /home/ddi/.ssh/id_rsa. Your public key has been saved in /home/ddi/.ssh/id_rsa.pub. The key fingerprint is: SHA256:RmGAqk6LRpYzpQrrurRgK7Z45tt4G3vMKbA2AlhncII ddi@deb9 The key's randomart image is: +---[RSA 2048]----+ | . ...o | |E o o . . | | = . | | o.o . | |.o+o S | |=O. . | |@++o.o . | |@=Oooo= | |X%=+++ | +----[SHA256]-----+
Accept default value for key location and leave password blank.
We now copy our user's id_rsa.pub
key to our host system:
as django user in VM
> ssh-copy-id devuser@172.20.20.1 /usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/ddi/.ssh/id_rsa.pub" /usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed /usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys thibaut@172.20.20.1's password:
Let's make sure mounting through SSHFS works without a password now by specifying usage of the RSA key:
from VM console
> sshfs devuser@172.20.20.1:/home/devuser/Documents/dev/django /home/django/projects -o allow_other,uid=1001,gid=1001,IdentityFile=/home/django/.ssh/id_rsa
Make sure you can access your host directory under the mountpoint and you're all set to finalize the automation.
Ownership
Up until now we simply mounted the directory and all it's content with our django user and group ids (1001
). But for Apache to be able to interact with the database we'll need the ability to change the group owner of our project's directory as well as the database file. This is not possible when fixing uid
and gid
at mount time. As described in the SSHFS man page, we'll use the SSHFS options idmap=file
, uidfile
and gidfile
to map our host ids to ou VM ids.
We'll thus create two files for id mappings and then edit /etc/fstab
to set the options to use those files. We are considering the following situation:
devuser
: uid=1000(devuser) gid=1000(devuser)
www-data
: uid=33(www-data) gid=33(www-data)
django
: uid=1001(django) gid=1001(django)
www-data
: uid=33(www-data) gid=33(www-data)
django user in VM console
> mkdir /home/django/sshfs > nano /home/django/sshfs/sshfs_uids ADD django:1000 www-data:33 > nano /home/django/sshfs/sshfs_gids ADD django:1000 www-data:33 > sudo nano /etc/fstab ADD # Automount development directory from Host using SSHFS devuser@172.20.20.1:/home/devuser/Documents/dev/django /home/django/projects fuse.sshfs _netdev,allow_other,idmap=file,uidfile=/home/django/sshfs/sshfs_uids,gidfile=/home/django/sshfs/sshfs_gids,nomap=ignore,IdentityFile=/home/django/.ssh/id_rsa,reconnect 0 0
For more details and options, see this ArchLinux page, the SSHFS manual page and the FUSE man page.
Our SSHFS shared directory is ready to be used for development !
Note that you will need to modify ownership properties from the host system.
Using the chown
command from your VM console will give a Permission denied
error.
Ownership modifications commands will have to be executed from the host console for files accessed through SSHFS, using numeric ids and taking the id mapping into consideration.
From linuxconfig.org
By default Debian 9(.4.0) comes with two versions of Python installed Python 2.7(.13) and Python 3.5(.3), to allow easy switching between the two, we'll use update-alternatives
as follow.
First we need to determine what Python binaries are available:
> ls /usr/bin/python* /usr/bin/python /usr/bin/python2.7-config /usr/bin/python3.5 /usr/bin/python-config /usr/bin/python2 /usr/bin/python2-config /usr/bin/python3.5m /usr/bin/python2.7 /usr/bin/python3 /usr/bin/python3m
Next, update the Python alternatives list for each version we whish to use (/usr/bin/python2.7 and /usr/bin/python3.5):
> update-alternatives --install /usr/bin/python python /usr/bin/python3.5 1 update-alternatives: using /usr/bin/python3.5 to provide /usr/bin/python (python) in auto mode > update-alternatives --install /usr/bin/python python /usr/bin/python2.7 2 update-alternatives: using /usr/bin/python2.7 to provide /usr/bin/python (python) in auto mode
Note that the integer at the end of the command line defines default priority. In this case, we keep python2.7 as the default priority since it has a higher number than python3.5.
From now on, switching to a different version of python is as simple as:
> update-alternatives --config python There are 2 choices for the alternative python (providing /usr/bin/python). Selection Path Priority Status ------------------------------------------------------------ * 0 /usr/bin/python2.7 2 auto mode 1 /usr/bin/python2.7 2 manual mode 2 /usr/bin/python3.5 1 manual mode Press <enter> to keep the current choice[*], or type selection number: 2 update-alternatives: using /usr/bin/python3.5 to provide /usr/bin/python (python) in manual mode python --version Python 3.5.3
We keep python 2.7 as default to make sure nothing brakes as some packages, commands and utilities are relying on it. But we'll be able to use Python 3 for our Django development.
In case you need to use Python 3.6, here is how to compile it on Debian 9. As this is quite a power hungry process, one might consider allowing sufficient resources to the VM before executing those commands, with an i7-2720QM CPU, a 4 core / 1 GB RAM VM takes about 20 minutes to complete the compilation…
As root user
> apt-get update && apt-get upgrade > apt-get install -y make build-essential libssl-dev zlib1g-dev > apt-get install -y libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm > apt-get install -y libncurses5-dev libncursesw5-dev xz-utils tk-dev > wget https://www.python.org/ftp/python/3.6.4/Python-3.6.4.tgz > tar xvf Python-3.6.4.tgz > cd Python-3.6.4 > ./configure --enable-optimizations > make -j8 > make altinstall
To test that Python 3.6 is well installed and working, enter a Python shell:
> python3.6 Python 3.6.4 (default, Aug 27 2018, 23:04:48) [GCC 6.3.0 20170516] on linux Type "help", "copyright", "credits" or "license" for more information. >>>
The Python3.6(.4) binary will be located in /usr/local/bin/python3.6
You might want to add it to the available alternatives using the above mentioned method.
We now need to create a Python virtual environment so that our Django project will be separated from the system's tools and any other Python projects we may be working on, we need to install the virtualenv
command to create these environments, this can be done using the pip
command. Lets' install it for Python 3:
As dev user
> update-alternatives --config python update-alternatives --config python There are 2 choices for the alternative python (providing /usr/bin/python). Selection Path Priority Status ------------------------------------------------------------ * 0 /usr/bin/python2.7 2 auto mode 1 /usr/bin/python2.7 2 manual mode 2 /usr/bin/python3.5 1 manual mode Press <enter> to keep the current choice[*], or type selection number: 2 update-alternatives: using /usr/bin/python3.5 to provide /usr/bin/python (python) in manual mode > python --version Python 3.5.3 > pip3 install virtualenv Collecting virtualenv Downloading https://files.pythonhosted.org/packages/b6/30/96a02b2287098b23b875bc8c2f58071c35d2efe84f747b64d523721dc2b5/virtualenv-16.0.0-py2.py3-none-any.whl (1.9MB) 100% |████████████████████████████████| 1.9MB 665kB/s Installing collected packages: virtualenv Successfully installed virtualenv-16.0.0
Make sure to act as your django user for the next steps !
> su django > cd </home/django/projects> > mkdir <my_project> > cd <my_project>
As we are now in the main project directory, let's create the virtual environment and activate it, making sure we are using Python 3. If the wrong version of python is active, switch to root and execute update-alternatives –config python
to activate python 3.:
> python --version Python 3.5.3 > virtualenv <djangoenv> > source <djangoenv>/bin/activate (<djangoenv>) >
Your prompt should change to indicate that you are now operating within a Python virtual environment, indicating the virtualenv name between parenthesis.
virtualenvwrapper
keeps all your virtualenvs in one place, and provides convenient tools for activating and deactivating them.
As dev user
> pip3 install virtualenvwrapper > nano ~/.bashrc ADD # LOAD VIRTUALENVWRAPPER AUTOMATICALLY source virtualenvwrapper.sh > source ~/.bashrc
Let's create a virtualenv
specifying the Python version to use:
As dev user
> mkvirtualenv --python=python3.5 <djangoenv>
To activate the superlist virtualenv
:
As dev user
> workon <djangoenv> (<djangoenv>) >
To deactivate the current virtualenv
:
As dev user
(<djangoenv>) > deactivate >
In case you ever need to remove a virtualenv
:
As dev user
> rmvirtualenv <djangoenv>
Now that we reside in our project's directory, and that we have the associated virtualenv running, we're ready to install the Django framework package:
(djangoenv) django@dev > pip install django Collecting django Downloading https://files.pythonhosted.org/packages/56/0e/afdacb47503b805f3ed213fe732bff05254c8befaa034bbada580be8a0ac/Django-2.0.6-py3-none-any.whl (7.1MB) 100% |████████████████████████████████| 7.1MB 2.6MB/s Collecting pytz (from django) Downloading https://files.pythonhosted.org/packages/dc/83/15f7833b70d3e067ca91467ca245bae0f6fe56ddc7451aa0dc5606b120f2/pytz-2018.4-py2.py3-none-any.whl (510kB) 100% |████████████████████████████████| 512kB 2.9MB/s Installing collected packages: pytz, django Successfully installed django-2.0.6 pytz-2018.4
It is possible to select the version of Django you'd like to install using the <
or =
option:
as dev user
> cd ~/your/project/path/ > pip3 install "django<1.12"
Staying in our already created project directory, we'll now initiate our Django project, which will create a second level directory containing the actual code. The key to achieving the correct directory structure is to list the parent directory after the project name:
> django-admin.py startproject myproject ~/projects/myproject
First thing, we need to adjust our project's settings:
> nano ~/projects/myproject/settings.py . . . ALLOWED_HOSTS = ["172.20.20.10", "127.0.0.1", "127.0.1.1"] . . . . . . STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
We will now use the management script to migrate the initial database schema to our SQLite database, create a superuser and collect all static files:
> cd ~/projects/myproject > ./manage.py makemigrations No changes detected > ./manage.py migrate Operations to perform: Apply all migrations: admin, auth, contenttypes, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying sessions.0001_initial... OK > ./manage.py createsuperuser Username (leave blank to use 'ddi'): admin Email address: tech@tacticz.com Password: Password (again): This password is too short. It must contain at least 8 characters. This password is too common. This password is entirely numeric. Password: Password (again): Superuser created successfully. > ./manage.py collectstatic .... .... 118 static files copied to '/home/ddi/django/ddi_api/static'.
Our basic Django project should now be ready. Let's starting up the default Django server:
> ./manage.py runserver 0.0.0.0:8000
The project should now be accessible by pointing your host's web browser to the address http://172.20.20.10:8000
In order not to always have to launch the Django server, we will use Apache to serve our project's pages. Clients requests will be translated into the WSGI format that the Django application expects using the mod_wsgi
module.
Our Apache VHosts will be configured using the ServerName
directive in our VHost config file. To achieve this, we'll first decide on a unique VBoxNet0 IP address for the VM and associate this to the desired domain name we'll use to access our dev environment.
Let's say we'd like to be able to test our project typing myproject.dev
in our host's web browser. We first need to define our unique VM's IP address in the allowed VBoxNet0 VirtualBox network. Let's say we'd like to have our VM responding under 172.20.20.50
:
on VM console
> nano /etc/network/interfaces ... # The vboxnet network interface allow-hotplug enp0s8 iface enp0s8 inet static address 172.20.20.50/24 > reboot
on HOST console
> sudo nano /etc/hosts ADD # Django-D9Py3A2w virtual hosts 172.20.20.50 myproject.dev
As we want to deploy different projects from the same VM, we'll create a new Apache Virtual Host for our project, let's create the configuration file from the default Apache one. We will modify the configuration so that it answers on requests posted on the port 80 with the domain name myproject.dev
. Then we'll make sure static files are served from our project's defined static
directory. We'll grant access to the wsgi.py
file within the second level project directory where the Django code is stored.
Finally we'll construct the directives to handle the WSGI pass, we'll use daemon mode to run the WSGI process, which is the recommended configuration, the WSGIDaemonProcess
directive will be used to set this up, for consistency myproject
will be used as an arbitrary name for the process. We point the Python home to our virtual environment as this is where Apache can find all of the components that may be required, and the Python path to point to the base of our Django project. We need to specify the process group, this should point to the same name we selected for the WSGIDaemonProcess
directive (myproject
). We finish by setting the script alias so that Apache will pass requests for the root domain to the wsgi.py
file.
> cp -a /etc/apache2/sites-available/000-default.conf /etc/apache2/sites-available/010-myproject.conf > nano /etc/apache2/sites-available/010-myproject.conf CHANGE #ServerName www.example.com TO ServerName myproject.dev ADD # Grant access to static files Alias /static /home/django/projects/myproject/static <Directory /home/django/projects/myproject/static> Require all granted </Directory> # Grant access to uwsgi.py file <Directory /home/django/projects/myproject/myproject> <Files wsgi.py> Require all granted </Files> </Directory> # Handle the WSGI pass WSGIDaemonProcess myproject python-home=/home/django/projects/myproject/myprojectenv python-path=/home/django/projects/myproject WSGIProcessGroup myproject WSGIScriptAlias / /home/django/projects/myproject/myproject/wsgi.py
We still need to grant write accesses so that Apache can access the database file as well as our project directory:
> chmod 664 /home/django/projects/myproject/db.sqlite3 > chmod 775 /home/django/projects/myproject
We then need to grant ownership of those files to the www-data
group which Apache runs under:
> sudo chown :www-data /home/django/projects/myproject/db.sqlite3 > sudo chown :www-data /home/django/projects/myproject
Those chown
operations will return a Permission denied
warning on the VM side when using SSHFS
. As explained earlier, ownership modifications for those files will have to be executed from the host console, using numeric ids and taking the id mapping into consideration.
To make sure we made no mistake writing our configuration file, we'll first activate the new config file and check the syntax, then restart the server:
> sudo a2ensite 010-myproject To activate the new configuration, you need to run: systemctl reload apache2 > sudo apache2ctl configtest ... Syntax OK > systemctl reload apache2
We should now be able to access our dev environment typing by myproject.dev
in our host's web browser.
If you plan to use Selenium for functional tests or to scrape websites content, you'll need to install Firefox and Geckodriver.
Though Firefox can be launched in headless mode using the -headless
option, it still requires the libgtk-3-0
and xvfb
packages to be installed in order to run, this has been reported in Bugzilla (https://bugzilla.mozilla.org/show_bug.cgi?id=1372998) but seems unlikely to ever be addressed by the Mozilla community
Here are the steps to get Firefox running on a headless (no X11) system:
As root
> wget -O FirefoxSetup.tar.bz2 "https://download.mozilla.org/?product=firefox-latest&os=linux64&lang=en-US" > tar xvf FirefoxSetup.tar.bz2 > mv firefox/ /opt/ > apt-get install libgtk-3-0 xvfb > /opt/firefox/firefox -headless *** You are running in headless mode.
You can check for the latest Geckodriver version on this github page. Note that we will install the geckodriver
binary in our local user path, so we'll update our user's .bashrc
file to access it under ~/.local/bin
, because this is also where Python will install things when you use pip install –user
.
As dev user
> mkdir -p ~/.local/bin > cd ~/.local/bin > wget https://github.com/mozilla/geckodriver/releases/download/v0.21.0/geckodriver-v0.21.0-linux64.tar.gz > tar xvf geckodriver-v0.21.0-linux64.tar.gz > nano ~/.bashrc ADD # LOCAL BINARIES PATH=~/.local/bin/:$PATH > geckodriver --version geckodriver 0.21.0 ... ...