A few days ago I switched my main development machine from a 2017 15" MacBook Pro with a 4-core 2.9GHz 7820HQ i7, 16GB of RAM and 512GB of SSD to a Lenovo Thinkpad X1 Extreme with a 6-core 8750H i7, 32GB of RAM and 1.5TB of Really Fast SSD.
My reasons for the switch were:
- The fact that my upgrade path with the MacBook would have been much more expensive.
- The disappointing MacBook keyboard; the rest of the machine caused many sparks of joy over the past 1.5 years, but the keyboard managed to make me sad Every Single Time.
- The fact that I like to give my development environment a really good shake-up every few years.
Although the Thinkpad has solid Linux support, I have decided to venture out of my comfort zone to see if I could turn current generation Windows on a top-of-the-line workstation laptop into a productive development environment.
This post deals specifically with the development workflow for one of our main products, a web-app built with Django and React (TypeScript) that is deployed exclusively on Linux servers.
Requirements for the workflow
- Use of first-class IDE, such as PyCharm.
- First-class debugging using the above-mentioned IDE.
- Canonical version of source code remains on the Windows host, can be synced or shared with the Docker container.
- Auto-refresh has to work. When I make a change in the editor, those changes should be reflected automatically upon first reload of the browser.
Overview of the solution
- Docker for Windows and Docker Compose are used to configure and orchestrate the Linux container(s) for running the app and its components.
- The souce code directory on the host is exposed to the running container via bind mount.
- The front-end system is continuously built on the Windows side and exposed to the Docker container via the same bind mount.
- Because Docker for Windows currently does not support inotify on a bind mount, I use an ingenious little tool called docker-windows-volume-watcher which efficiently works around this problem.
- PyCharm has first-class support for both Docker and Docker Compose. I use this for developing and debugging.
I use Docker for Windows to setup and run the Linux containers required for the development of this project.
Ideally, I would have preferred to use the new Linux Containers on Windows (LCOW) functionality, but that’s currently unusable due to a bug resulting in the shutdown of containers taking up to 5 minutes.
Until LCOW is fixed, we make use of the default Moby VM. One of the implications of this is that our app runs as root inside of the container.
Below is the simplified and redacted
Dockerfile for the main app image.
The following aspects are notable:
locale-dance shown here is the best way to ensure that the Ubuntu image is configured as UTF-8.
PYTHONBUFFEREDis required if you would like to see the standard output of the Django app in the docker-compose console.
pipenvvirtual environment is created and all dependencies are installed as part of the image building process.
Docker Compose configuration
Docker Compose enables us to coordinate more than just the image specified up above. Here we show a simplified version with just the app image.
Notable here is that we bind mount the source code directory (which also contains the docker configuration files) into the running container.
pipenv command will run Django in the default
WORKDIR specified in the
Starting up the system without PyCharm
Start up the docker container, building any images that might be required:
If you’ve made changes to the
Dockerfile, invoke with
docker-compose up --build.
Any containers specified in the docker compose config are now up and running.
I tend to treat this whole invocation as a single command: I don’t detach with
-d, because I prefer to watch the log output just there, and to press
Ctrl-C when I want to stop the whole business.
Starting up continuous front-end building
I do all of the frontend transpilation on the Windows side using the usual suspects: Webpack for coordinating everything, Yarn for package management, and TypeScript (and a bit of Babel) for all of the transpilation.
In short, via a
package.json script I keep
webpack --watch running. As per
usual, when any of the watched source files changes, webpack rebuilds the
This is running on the Windows side, so the usual efficient filesystem monitoring tools are used for this.
All of the built assets are exposed to the running docker container using the existing source code bind mount.
Ensuring that Django edits automatically restart the dev server
With the current Docker for Windows (pre-LCOW) setup, bind mounted host files are network shared to the Linux VM on which the docker containers are actually running.
As mentioned previously, one of the drawbacks of this system is that events on the Windows side are not propagated through to the container as filesystem events that can be detected with the efficient inotify system calls on the Linux side.
This Python tool (there are Go versions also) uses efficient filesystem events on the Windows host to detect changed files, and then performs no-op attribute changes on those files within the Linux container to have any inotify clients, such as the Django development server, detect these.
pip installing the tool, I use the following invocation on the Windows
- In the Docker for Windows General settings, ensure that “Expose daemon on tcp://localhost:2375 without TLS” is checked. PyCharm requires this at the moment.
- In PyCharm, open the source directory on your Windows machine.
- Under Settings | Project | Project Interpreter add an interpreter of type
Docker Compose. After configuring and selecting the docker server, you will need to select the correct
docker-compose.ymlfile and the
our_productservice. After this, PyCharm will do its usual detection and listing of all installed packages.
- Finally create a Run / Debug configuration at the top right of the UI. Here it’s important that you use the “Django Server” template and not the “Docker-compose” like I first did. The latter will work for running, but not debugging.
Once you’ve taken care of all of the above, you should have a fairly usable development workflow for a Django / React web-app.
Working auto-refresh is one of the most important components of iterative
development. Here, the combination of building the frontend on the Windows
side, and the
docker-windows-volume-watcher for relaying backend editing
events, was a life saver.
I am looking forward to Docker for Windows LCOW one day working even more smoothly than this setup, and obviating the need for additional moving, although quite clever, parts such as the volume watcher.