diff --git a/.gitignore b/.gitignore
index 86709ead9de0b2767b5487451032789b566cd97d..0bfe066ca2b05822fc5894d0fe721a8d99141b45 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,16 +1,17 @@
-*.swp
-*.pyc
 *.egg-info/
-.project
-.pydevproject
-.vscode/
+*.pyc
+*.swp
 .coverage
-.venv/
-.settings/
-.mypy_cache/
 .dockerignore
 .idea/
+.mypy_cache/
+.project
+.pydevproject
+.settings/
+.venv/
+.vscode/
+venv/
 
 gregsite/db.sqlite3
-gregsite/static/
 gregsite/settings/local.py
+gregsite/static/
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..50c66dd5d58fb852bf43dbfffba43a2b8b81756c
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,30 @@
+PIP ?= pip -q
+POETRY ?= poetry
+PYTHON ?= python3.9
+VENV ?= venv
+
+pip = python -m $(PIP)
+poetry = python -m $(POETRY)
+venv = . $(VENV)/bin/activate &&
+
+.PHONY: clean
+clean:
+	$(RM) -r $(VENV)
+
+$(VENV): $(VENV)/touchfile
+$(VENV)/touchfile:
+	test -d $(VENV) || $(PYTHON) -m venv $(VENV)
+	$(venv) $(pip) install -U pip
+	$(venv) $(pip) install poetry
+	$(venv) $(poetry) install
+	touch $@
+
+.PHONY: test
+test: $(VENV)
+	$(venv) python manage.py test
+
+.PHONY: deps
+deps: poetry.lock
+
+poetry.lock: pyproject.toml | $(VENV)
+	$(venv) $(poetry) update $(PACKAGES)