diff --git a/Dockerfile b/Dockerfile
index fd008a2fe99b632515cc3ab6de5e1977d9bd98e7..db15945c35f17c0ec388115022088b3dcac618bd 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -11,7 +11,7 @@ WORKDIR /app
 
 COPY poetry.lock pyproject.toml /app/
 
-RUN apt-get update && apt-get install -y curl gcc graphviz ldap-utils libldap2-dev libmagic1 libsasl2-dev
+RUN apt-get update && apt-get install -y curl gcc graphviz ldap-utils libldap2-dev libmagic1 libsasl2-dev make
 RUN python -m venv ${POETRY_HOME}
 RUN pip install poetry==1.4.0
 RUN poetry install
@@ -29,10 +29,14 @@ RUN chown -R app:app /app
 
 USER app
 
-CMD ["poetry", "run", "python", "manage.py", "runserver", "0.0.0.0:8000"]
+# run with Django's development server
+CMD ["run.dev"]
 
 FROM base AS prod
 
 COPY . .
 
-CMD ["sh", "-c", "poetry run gunicorn --bind 0.0.0.0:8000 --workers $(nproc) steering.wsgi"]
+# run with gunicorn
+CMD ["run.prod"]
+
+ENTRYPOINT ["make"]
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..67e72f663f141c824be89eeb5065434f8d280c7b
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,37 @@
+POETRY = poetry
+POETRY_RUN = $(POETRY) run
+POETRY_RUN_MANAGE = $(POETRY_RUN) ./manage.py
+
+.DEFAULT_GOAL := run.prod
+
+initialize: migrate loadfixtures
+	$(POETRY_RUN_MANAGE) collectstatic --clear --no-input
+	$(POETRY_RUN_MANAGE) createsuperuser --no-input
+	$(POETRY_RUN_MANAGE) creatersakey
+	$(POETRY_RUN_MANAGE) create_oidc_client dashboard public --client-id ${DASHBOARD_OIDC_CLIENT_ID} --client-secret ${DASHBOARD_OIDC_CLIENT_SECRET} -r "id_token token" -u ${DASHBOARD_CALLBACK_BASE_URL}/oidc_callback.html -u ${DASHBOARD_CALLBACK_BASE_URL}/oidc_callback_silentRenew.html -p ${DASHBOARD_CALLBACK_BASE_URL} -p ${DASHBOARD_CALLBACK_BASE_URL}/
+	$(POETRY_RUN_MANAGE) create_oidc_client tank confidential --client-id ${TANK_OIDC_CLIENT_ID} --client-secret ${TANK_OIDC_CLIENT_SECRET} -r "code" -u ${TANK_CALLBACK_BASE_URL}/tank/auth/oidc/callback
+
+migrate:
+	$(POETRY_RUN_MANAGE) migrate --no-input
+
+loadfixtures:
+	# TODO: reduce the fixtures loaded to the very minimum
+	$(POETRY_RUN_MANAGE) loaddata fixtures/*/*.json
+
+loaddata:
+	$(POETRY_RUN_MANAGE) loaddata ${DATA}
+
+run.dev: migrate
+	$(POETRY_RUN_MANAGE) runserver 0.0.0.0:8000
+
+run.prod: migrate
+	$(POETRY_RUN) gunicorn --bind 0.0.0.0:8000 --workers `(nproc)` steering.wsgi
+
+run.debug: migrate
+	DEBUG=1 $(POETRY_RUN_MANAGE) runserver_plus 0.0.0.0:8000
+
+release: VERSION := `$(POETRY_RUN) python -c 'import tomli; print(tomli.load(open("pyproject.toml", "rb"))["tool"]["poetry"]["version"])'`
+release:
+	git tag ${VERSION}
+	git push origin ${VERSION}
+	@echo "Release '${VERSION}' tagged and pushed successfully."