Tips and Tricks for using Tutor as Devstack Replacement

On a number of occasions the question of tutor as a replacement for devstack has come up. Since quite some time now I’m mostly using Tutor as my devstack, so I figured I’d share how I’m using it in the hopes that it will help other people.

There are many ways in which I’ve come to really enjoy using Tutor as a devstack replacement. There are some things it enables that were much harder with the old devstack.

I’ll start with some key facts about Tutor, some that I discovered late. Some or none of these may be news to you as they were at some point for me:

  • Tutor is a regular Python package.
  • Each major version deals with a new release of Open edX, so v16 is for Palm, v15 was for Olive and so on.
    • You shouldn’t try to install Palm with v15 or vice versa.
    • Upgrading between releases should be as simple as, install new version of Tutor and run tutor launch again.
    • New features of tutor itself can land between major releases, but often new Tutor features also come with a new major release
    • The version for the master branch is called tutor nightly, and it installs and is configured separately.
  • It is entirely pluggable using actions and hooks
    • Actions let you do things as a response to tutor events
    • Hooks like you change existing configuration, add commands, add more docker images, services etc.
  • It uses Jinja2 internally and config variables are plugged into templates to produce Dockerfiles, settings files etc.
  • You don’t need to create a whole python package for a plugin, you can just create one file and put it in the right place.

Tutor can handle 3 types of deployments, dev for development (a devstack replacement), local, which runs a production install via docker-compose on your local system, and k8s which installs to Kubernetes.

There are command that apply globally, such as config, mounts, plugins etc. and then there are command that can be run for a specific environment. For instance, you can use tutor config to set up a config, tutor plugins to install, enable/disable plugins and tutor mounts to handle mount points. These will all update your config file. Then using this singular config file, you can use tutor dev to launch a devstack, tutor local to launch a local docker compose based production deployment and tutor k8s to deploy to Kubernetes. This means if you want to test a client’s setup locally, you can just use their config file to launch a devstack (make sure to not use the same DB etc).

When you run tutor config save tutor will load your config file, and from it build all required configuration, such as Caddyfiles, edx-platform yaml and Django files, Dockerfiles for each service, docker-compose files for local and dev deployments, Kubernetes config etc. Then running tutor dev/local/k8s will use this config to deploy and launch services. After they are built, you can go and edit them. This is especially useful during development, since you can go edit any file directly and the changes should reflect.

If you want tutor to use a particular config file, just put it in an empty directory, and run tutor -r path/to/dir and it will build the config in that directory. You can also specify this path using the TUTOR_ROOT environment variable.

I tend to create new tutor environments quite often. So I have a folder for all instances, for each instance I create a subfolder for its config. I also use pyenv + virtualenv to create a virtulenv for each version. I have this set in an .envrc file that is used by a tool called direnv to automatically load environment variables when you enter a directory and clear them when you leave it. So I can just cd into the config directly, and the everything is set up to use that tutor environment! You can use this to run multiple devstacks at once.

Here is a sample .envrc file for my palm environment:

layout pyenv palm

Here the first line sets the tutor root to the current directory, and the next line tells direnv to use the palm python virtualenv from pyenv. You can also do this without pyenv but I won’t go into details yet, but that link has more. This way you can create a new tutor environment for each ticket and then delete it when no longer needed.

Common devstack operations

Use your own version of edx-platform, especially with nightly

Use tutor mounts for this, and point it to your edx-platform directory and others as needed. This will then use that directory, changes are automatically picked up just like with the old devstack (if using tutor dev ).

Install a python package / xblock to test

If you just want to quickly test it, not need to go through the huge process of adding it to the config and rebuilding images. Just directly install the package in the docker container. You can run tutor dev exec lms bash to get a bash prompt in the lms, or just directly do pip install here. I’ve created a quick plugin that helps me quickly test PRs and MRS here. It will use the PR to get the name of the branch that should be installed and calls pip install on that in the lms and cms.

Test a theme

The tutor docs are pretty good here.

Develop MFEs

This is a bit painful with both devstacks. You can mount the MFE and it should automatically hot-reload sometimes, but the second you start dealing with custom themes overriding component etc it doesn’t work. When you install a package from a local directory it creates a symlink, so changes should automatically be picked up. For example, if you are testing a branding package intalled locally then you can edit the theme and it should update in your app. However, if it’s running in a container, the symlink is not valid in there (unless you install using a relative path and also mount that path inside the container).

So I prefer running MFEs outside of docker. Each MFE contains a .env.development file that is designed to work with the regular devstack, not tutor. However, you can modify it to work with tutor as well. Just replace and

This isn’t enough still, because you need to access the MFE using and that work work just yet. So create the following file in the MFE dir:

const { createConfig } = require('@edx/frontend-build');

const config = createConfig('webpack-dev');

config.cache = { type: 'filesystem' }; // This isn't needed but really speeds up rebuilds!

config.devServer.allowedHosts = [

module.exports = config;

It’s possible that such a file already exists, in which case, just add the allowedHosts bit from above.

I hope this helps!


@kshitij This is very helpful and extremely well written - thanks! Maybe you should post on the Open edX forum too :slight_smile:

This is very key! Tutor’s philosophy is that almost any customization that’s more complex than changing a setting should be done via a plugin, and that sounds like a lot of work but often it’s just putting a few lines of code into a single file.

1 Like

+1 to cross-posting this to the Open edX forum. Really well written summary!

1 Like

Oh and here’s a tip that I’ve learned:

When you are using Tutor (nightly) as a devstack and you update edx-platform to a newer version of master, often your devstack will stop working because it needs some new python package or migration or static assets build or something. The simplest way to resolve this is to run tutor dev stop then tutor dev launch.

This is a bit counter-intuitive because it asks you these questions:

Your platform name/title [Braden's Open edX Dev] 
Your public contact email address [] 
The default language code for the platform [en]

So it seems like it’s doing some total reset or re-config. But just hit ENTER three times, wait a few minutes*, and your devstack will be back and working nicely again.

*It takes about 7 minutes, which is not ideal but not terrible. There is ongoing work to reduce this time. I find that the consistent, predictable state that results is worth the extra time compared to just trying to update packages or run migrations or whatever manually.

You can skip those questions by adding a -I or --non-interactive, I generally do that after I’ve set up the config once.

A few more tips:

  • I cloned the nightly branch of tutor in a separate folder, used pyenv+virtualenv to install it in a separate venv and then renamed it in the user bin directory to tutor-nightly this way I can always run commands a tutor-nightly ....
  • If you need to develop a tutor plugin, it helps to create a separate folder for the config, init a git repo there, and commit the entire environment. Then you can install the plugin, change the config etc and commit or diff at each step to see how it’s affecting the environment.

I’ll clean this up a bit and cross-post on Monday :-)

1 Like

@kshitij Thank you so much for this write up. I have tried Tutor at least 2 or 3 times and given up each time (mostly owing to my lazyness and 1 time due to not having ARM images). But on crucial thing that I never figured out in each attempt was the config stuff and how & when dev and local are to be used.

I might finally migrate to Tutor. :pray:

1 Like

@kaustav @farhaan @demid Can you post your questions in regards to XBlock and MFE local development via tutor here?

One of my big concerns used to be running multiple different version of tutor (i.e. different Open edX releases) locally for development, since I frequently had to switch between master, nutmeg, maple, etc.

eduNEXT released tvm - tutor version manager, which allows easily switching between versions. I haven’t tried it yet, as I haven’t found an opportunity/reason to fully switch to tutor yet, but I though I’d leave it as a reference here.

Forum thread which has small demo and some useful links.
Short presentation from the conference.

I frequently switch between the latest release and nightly, and that is quite simple, however you can also extend this to multiple environments by setting the TUTOR_APP environment variable. This will change the docker project name, so you can then have multiple environments running at once. I haven’t tried this though since it hasn’t come up for me yet.

For MFEs I’d recommend running outside docker with the config I specified in my original post. I’ll also share the contents of .env.private:


For testing XBlock PRs I’d recommend my plugin I linked before. It converts a PR url to a pip command which it runs in the lms and cms, so you can use it to quickly install a plugin or XBlock from a PR or gitlab MR.

I finally made the switch yesterday using the tutor-nightly. It was a bumpy ride, but everything worked in the end. Some issues encountered:

  • It took at least 5 tries before everything worked as expected. I think it is mostly to do with the next point.
  • Docker Desktop on Mac M2 isn’t really that robust.
    • Containers kept getting stuck at the post build script, esp on npm ci with Zero CPU or Network usage. This is a single run container which is supposed to do things like install deps, run migrations…etc.,
    • Stopping the stuck container didn’t work from CLI or from the Docker Desktop interface, and had to restart the Docker Engine to get it cleared out.
  • The tutor mfe plugin docs were helpful in when I wanted to just have the Learning MFE in dev mode. For some reason, following @kshitij’s MFE steps to run it independently didn’t work. I just got a blank page and no errors in the browser console. So, I stuck to using the tutor mfe dev setup. Maybe I needed to remove the Learning MFE from the mount? Didn’t have the time to debug this.
  • The dev container for the Learning MFE was behaving strangely, it runs fine when I restart the container, but is stuck when it starts initially.

Just leaving these here in case someone uses a Mac and gets stuck.

Yes my instructions for running MFEs out of docker have many caveats. Most importantly, the MFEs are not behind a proxy, so while tutor will usually map paths as “/learning” path, that will not happen when running the MFE directly. So it will be served without the leading /learning. Just remove that and the MFE should load.