diff --git a/backend/app.py b/backend/app.py
index 896f1035fd748df697776c66690ada75f16bd637..5594bb3c58f2ce1b09f3fae4b97852390cf730c9 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -37,6 +37,7 @@ import cluster_config
 from config import *
 import logging
 import migration_reset
+import os
 import sys
 
 # Configure logging.
@@ -69,35 +70,49 @@ cors = CORS(app)
 
 db.init_app(app)
 
-# We'll now perform some initialization routines. Because these have to be done
-# once at startup, not for every gunicorn worker, we take a machine-wide lock
-# for this.
-init_lock = NamedAtomicLock('dashboard_init')
-if init_lock.acquire():
+def init_routines():
+    with app.app_context():
+        # We have reset the alembic migration history at Stackspin version 2.2.
+        # This checks whether we need to prepare the database to follow that
+        # change.
+        migration_reset.reset()
+    flask_migrate.Migrate(app, db)
     try:
         with app.app_context():
-            # We have reset the alembic migration history at Stackspin version 2.2.
-            # This checks whether we need to prepare the database to follow that
-            # change.
-            migration_reset.reset()
-        flask_migrate.Migrate(app, db)
-        try:
-            with app.app_context():
-                flask_migrate.upgrade()
-        except Exception as e:
-            app.logger.info(f"upgrade failed: {type(e)}: {e}")
-            sys.exit(2)
-
-        # We need this app context in order to talk the database, which is managed by
-        # flask-sqlalchemy, which assumes a flask app context.
-        with app.app_context():
-            # Load the list of apps from a configmap and store any missing ones in the
-            # database.
-            cluster_config.populate_apps()
-            # Same for the list of oauthclients.
-            cluster_config.populate_oauthclients()
-    finally:
-        init_lock.release()
+            flask_migrate.upgrade()
+    # TODO: actually flask_migrate.upgrade will catch any errors and
+    # exit the program :/
+    except Exception as e:
+        app.logger.info(f"upgrade failed: {type(e)}: {e}")
+        sys.exit(2)
+
+    # We need this app context in order to talk the database, which is managed by
+    # flask-sqlalchemy, which assumes a flask app context.
+    with app.app_context():
+        # Load the list of apps from a configmap and store any missing ones in the
+        # database.
+        cluster_config.populate_apps()
+        # Same for the list of oauthclients.
+        cluster_config.populate_oauthclients()
+
+# `init_routines` has to run only once per dashboard instance, or at least not
+# concurrently. We used to have locking in place to prevent concurrent
+# executions of this code, but we now rely on proper configuration of gunicorn
+# and/or flask to make sure this is run only once. In particular:
+# * we have "preload" on for gunicorn, so this file is loaded only once, before
+#   workers are forked;
+# * we make sure that in development mode we run this only once, even though
+#   this file is loaded twice by flask for some reason.
+if DEV_MODE:
+    logging.info("WERKZEUG_RUN_MAIN: {}".format(os.environ.get("WERKZEUG_RUN_MAIN", "unset")))
+    if os.environ.get("WERKZEUG_RUN_MAIN") == "true":
+        logging.info("Running initialization code (dev mode).")
+        init_routines()
+    else:
+        logging.info("Not running initialization code (dev mode).")
+else:
+    logging.info("Running initialization code (production mode).")
+    init_routines()
 
 app.register_blueprint(api_v1)
 app.register_blueprint(web)
diff --git a/backend/config.py b/backend/config.py
index 6e5d37ac1221925e8403612674225bd0ccf5314d..b2404b9410e51e2550df76c921533d11d0102526 100644
--- a/backend/config.py
+++ b/backend/config.py
@@ -21,5 +21,9 @@ SQLALCHEMY_TRACK_MODIFICATIONS = False
 # running in a Kubernetes pod. Set it to "false" to load the config from the
 # `KUBECONFIG` environment variable.
 LOAD_INCLUSTER_CONFIG = os.environ.get("LOAD_INCLUSTER_CONFIG").lower() == "true"
+# We use this to detect whether we run in production mode (gunicorn, as
+# specified in the docker image) or dev mode (flask run, as specified in docker
+# compose config).
+DEV_MODE = os.environ.get("DASHBOARD_DEV_MODE", "False").lower() in ('true', '1')
 
 DEMO_INSTANCE = os.environ.get("DASHBOARD_DEMO_INSTANCE", "False").lower() in ('true', '1')
diff --git a/docker-compose.yml b/docker-compose.yml
index 00b2d76d4e3f7f2cf6a5157b97db69ccbd06ef18..be4376c2371883520d79726061d11d7fa816a909 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -54,7 +54,7 @@ services:
       - "$KUBECONFIG:/.kube/config"
     depends_on:
       - kube_port_mysql
-    entrypoint: ["bash", "-c", "flask run --host $$(hostname -i)"]
+    entrypoint: ["bash", "-c", "DASHBOARD_DEV_MODE=true flask run --host $$(hostname -i)"]
   kube_port_kratos_admin:
     image: bitnami/kubectl:1.27.2
     user: "${KUBECTL_UID}:${KUBECTL_GID}"