diff --git a/.gitignore b/.gitignore
index e62b1ce6207902054342af6503f9773ca926f611..b1a3b230bb00baf92598fb4b138f04a0f3e830a0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -95,4 +95,4 @@ _build
 front/src/translations.json
 front/src/translations/*.json
 front/locales/en_US/LC_MESSAGES/app.po
-*.prof
+*.prof
\ No newline at end of file
diff --git a/.gitpod.yml b/.gitpod.yml
index 31b344fab0bcef0948ebde8306b1d3cf37e9d969..2d68532e66b70582f552f809ba1505fd12bd2df7 100644
--- a/.gitpod.yml
+++ b/.gitpod.yml
@@ -1,67 +1,49 @@
+image:
+  file: .gitpod/Dockerfile
+
 tasks:
-  - name: Docker
+  - name: Backend
     env:
-      COMPOSE_FILE: dev.yml
+      ENV_FILE: /workspace/funkwhale/.gitpod/.env
+      COMPOSE_FILE: /workspace/funkwhale/.gitpod/docker-compose.yml
+    before: |
+      cp .gitpod/gitpod.env .gitpod/.env
+      cd api
     init: |
-      # Install frontend depencencies locally
-      cd front
-      yarn install
-      cd ..
-
-      # Prepare prebuild .env
-      echo "# Gitpod Environment Variables" > .env
-
-      # Prepare docker
-      docker network create federation
-      docker-compose pull
-      docker-compose build
-      docker-compose up -d postgres redis
-      sleep 10 # allow postgres and redis to initialize
-
-      # Prepare backend
-      docker-compose run --rm api python manage.py migrate
-      docker-compose run --rm api python manage.py createsuperuser --no-input --username gitpod --email gitpod@example.com
-      docker-compose run --rm api python manage.py fw users set --password "gitpod" gitpod --no-input
+      mkdir -p ../data/media/attachments ../data/music ../data/staticfiles
+      docker-compose up -d
 
-      # Compile frontend locales
-      docker-compose run --rm front yarn run i18n-compile
+      poetry env use python
+      poetry install
 
-      # Start API to let script create an actor
-      docker-compose up -d nginx
-      gp ports await 8000
+      gp ports await 5432
 
-      # Clone music repo
-      git clone https://dev.funkwhale.audio/funkwhale/catalog.git
-      sudo mv catalog/music data
-      sudo chown -R root:root data/music
-      rm -rf catalog
-
-      # Login with cURL to create actor
-      python .gitpod/init_actor.py
-
-      # Import music
-      docker-compose down
-      LIBRARY_ID=`cat .gitpod/create_library.py | docker-compose run --rm -T api python manage.py shell -i python`
-      docker-compose run --rm api python manage.py import_files $LIBRARY_ID "/music/" --recursive --noinput --in-place
-
-      # Stop docker
-      docker-compose stop
+      poetry run python manage.py migrate
+      poetry run python manage.py gitpod init
     command: |
-      # Prepare workspace .env
-      echo "MEDIA_URL=`gp url 8000`/media/" >> .env
-      echo "STATIC_URL=`gp url 8000`/staticfiles/" >> .env
-      echo "FUNKWHALE_HOSTNAME=`gp url 8000 | sed 's#https://##'`" >> .env
-      echo "FUNKWHALE_PROTOCOL=https" >> .env
-      echo "GITPOD_WORKSPACE_URL=$GITPOD_WORKSPACE_URL" >> .env
-      echo "HMR_PORT=8000" >> .env
-      echo "VUE_APP_INSTANCE_URL=$VUE_APP_INSTANCE_URL" >> .env
+      echo "MEDIA_URL=`gp url 8000`/media/" >> ../.gitpod/.env
+      echo "STATIC_URL=`gp url 8000`/staticfiles/" >> ../.gitpod/.env
+      echo "FUNKWHALE_HOSTNAME=`gp url 8000 | sed 's#https://##'`" >> ../.gitpod/.env
+      echo "FUNKWHALE_PROTOCOL=https" >> ../.gitpod/.env
 
-      # Start app
-      docker-compose up front api nginx
+      docker-compose up -d
+      gp ports await 5432
+      poetry run python manage.py collectstatic --no-input
+      poetry run python manage.py gitpod dev
+
+  - name: Frontend
+    env:
+      HMR_PORT: 8000
+    before: cd front
+    init: |
+      yarn install
+      yarn run i18n-compile
+    command: yarn dev --host 0.0.0.0 --base /front/
 
   - name: Welcome to Funkwhale development!
     env:
-      COMPOSE_FILE: dev.yml
+      COMPOSE_FILE: /workspace/funkwhale/.gitpod/docker-compose.yml
+      ENV_FILE: /workspace/funkwhale/.gitpod/.env
     command: |
       clear
       echo ""
@@ -78,12 +60,33 @@ ports:
     visibility: public
     onOpen: notify
 
+  - port: 5000
+    visibility: private
+    onOpen: ignore
+
+  - port: 5432
+    visibility: private
+    onOpen: ignore
+
+  - port: 5678
+    visibility: private
+    onOpen: ignore
+
+  - port: 6379
+    visibility: private
+    onOpen: ignore
+
+  - port: 8080
+    visibility: private
+    onOpen: ignore
+
 vscode:
   extensions:
     - lukashass.volar
-    - lextudio.restructuredtext
-    - trond-snekvik.simple-rst
     - ms-python.python
     - ms-toolsai.jupyter
     - ms-toolsai.jupyter-keymap
     - ms-toolsai.jupyter-renderers
+    - hbenl.vscode-test-explorer
+    - hbenl.test-adapter-converter
+    - littlefoxteam.vscode-python-test-adapter
diff --git a/.gitpod/Dockerfile b/.gitpod/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..00573fd1be391aa4f4c2ceb92380c84a69b8530b
--- /dev/null
+++ b/.gitpod/Dockerfile
@@ -0,0 +1,9 @@
+FROM gitpod/workspace-full
+USER gitpod
+
+RUN sudo apt update -y \
+    && sudo apt install libsasl2-dev libldap2-dev libssl-dev ffmpeg -y
+
+RUN pip install poetry \
+    && poetry config virtualenvs.create true \
+    && poetry config virtualenvs.in-project true 
\ No newline at end of file
diff --git a/.gitpod/create_library.py b/.gitpod/create_library.py
deleted file mode 100644
index 7991417f72674e625c499022da6bfa7cfff2e419..0000000000000000000000000000000000000000
--- a/.gitpod/create_library.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from funkwhale_api.music.models import Library
-from django.contrib.auth import get_user_model
-
-actor = get_user_model().objects.get(username='gitpod').actor
-
-try:
-    library = Library.objects.get(actor=actor)
-except:
-    #  Create library
-    library = Library()
-    library.actor = actor
-    library.description = 'Libre music to build a starter catalog for your instance'
-    library.name = 'funkwhale/catalog'
-    library.privacy_level = 'everyone'
-    library.save()
-
-print(str(library.uuid))
\ No newline at end of file
diff --git a/.gitpod/docker-compose.yml b/.gitpod/docker-compose.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2fe85a9a5c18e570bb30ce0d95a0947dda47f1ae
--- /dev/null
+++ b/.gitpod/docker-compose.yml
@@ -0,0 +1,43 @@
+version: '3'
+
+services:
+  postgres:
+    image: postgres:14-alpine
+    environment:
+      - "POSTGRES_HOST_AUTH_METHOD=trust"
+    volumes:
+      - "../data/postgres:/var/lib/postgresql/data"
+    ports:
+      - 5432:5432
+
+  redis:
+    image: redis:7-alpine
+    volumes:
+      - "../data/redis:/data"
+    ports:
+      - 6379:6379
+
+  nginx:
+    command: /entrypoint.sh
+    env_file:
+      - ./.env
+    image: nginx
+    ports:
+      - 8000:80
+    extra_hosts:
+      - host.docker.internal:host-gateway
+    environment:
+      - "NGINX_MAX_BODY_SIZE=100M"
+      - "FUNKWHALE_API_IP=host.docker.internal"
+      - "FUNKWHALE_API_PORT=5000"
+      - "FUNKWHALE_FRONT_IP=host.docker.internal"
+      - "FUNKWHALE_FRONT_PORT=8080"
+      - "FUNKWHALE_HOSTNAME=${FUNKWHALE_HOSTNAME-host.docker.internal}"
+    volumes:
+      - ../data/media:/protected/media:ro
+      - ../data/music:/music:ro
+      - ../data/staticfiles:/staticfiles:ro
+      - ../deploy/funkwhale_proxy.conf:/etc/nginx/funkwhale_proxy.conf:ro
+      - ../docker/nginx/conf.dev:/etc/nginx/nginx.conf.template:ro
+      - ../docker/nginx/entrypoint.sh:/entrypoint.sh:ro
+      - ../front:/frontend:ro
\ No newline at end of file
diff --git a/.gitpod/gitpod.env b/.gitpod/gitpod.env
new file mode 100644
index 0000000000000000000000000000000000000000..cda36d82a4ae933143ed0a2295327c187d04e3fa
--- /dev/null
+++ b/.gitpod/gitpod.env
@@ -0,0 +1,25 @@
+# Dev Environment Variables
+DJANGO_ALLOWED_HOSTS=.funkwhale.test,localhost,nginx,0.0.0.0,127.0.0.1,.gitpod.io
+DJANGO_SETTINGS_MODULE=config.settings.local
+C_FORCE_ROOT=true
+BROWSABLE_API_ENABLED=True
+FORWARDED_PROTO=http
+LDAP_ENABLED=False
+FUNKWHALE_SPA_HTML_ROOT=http://localhost:8000/front/
+FUNKWHALE_URL=http://localhost:8000/
+MUSIC_DIRECTORY_PATH=/workspace/funkwhale/data/music
+STATIC_ROOT=/workspace/funkwhale/data/staticfiles/
+MEDIA_ROOT=/workspace/funkwhale/data/media/
+
+PYTHONTRACEMALLOC=0
+PYTHONDONTWRITEBYTECODE=true
+
+POSTGRES_VERSION=14
+DEBUG=true
+
+
+# Django Environment Variables
+DATABASE_URL=postgresql://postgres@localhost:5432/postgres
+DJANGO_SECRET_KEY=gitpod
+
+# Gitpod Environment Variables
diff --git a/.gitpod/init_actor.py b/.gitpod/init_actor.py
deleted file mode 100644
index 7dde3945e05b5d540840ca31a7be65696603888d..0000000000000000000000000000000000000000
--- a/.gitpod/init_actor.py
+++ /dev/null
@@ -1,21 +0,0 @@
-import requests
-
-# Login to initialize user actor
-req = requests.Session()
-
-res = req.get('http://localhost:8000/login')
-print(res.status_code, res.cookies)
-token = res.cookies['csrftoken']
-
-res = req.post('http://localhost:8000/api/v1/users/login', data={
-    'username': 'gitpod',
-    'password': 'gitpod',
-    'csrfmiddlewaretoken': token,
-})
-print(res.status_code, res.content)
-
-res = req.get('http://localhost:8000/')
-print(res.status_code)
-
-if res.status_code == 401:
-    exit(1)
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000000000000000000000000000000000000..fa20725c9e99e51b00256545943892a32aec85a5
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,34 @@
+{
+    // Use IntelliSense to learn about possible attributes.
+    // Hover to view descriptions of existing attributes.
+    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "name": "Attach python debugger",
+            "type": "python",
+            "request": "attach",
+            "connect": {
+                "host": "localhost",
+                "port": 5678
+            },
+            "django": true
+        },
+        {
+            "name": "Debug python",
+            "type": "python",
+            "request": "launch",
+            "module": "uvicorn",
+            "cwd": "${workspaceFolder}/api",
+            "envFile": "${workspaceFolder}/.gitpod/.env",
+            "args": [
+                "--reload", "config.asgi:application",
+                "--host", "0.0.0.0",
+                "--port", "5000",
+                "--reload-dir", "config/",
+                "--reload-dir", "funkwhale_api/"
+            ],
+            "django": true
+        }
+    ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index a7d0fc7b77f630f503516f9a4206c554b3223b29..7319f51803fdd3bec011aa582785808da589aec4 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,3 +1,11 @@
 {
-    "esbonio.sphinx.confDir": ""
+    "python.defaultInterpreterPath": "/workspace/funkwhale/api/.venv/bin/python",
+    "python.testing.cwd": "/workspace/funkwhale/api",
+    "python.envFile": "/workspace/funkwhale/.gitpod/.env",
+    "python.testing.pytestArgs": [
+        "--cov=funkwhale_api",
+        "tests/"
+    ],
+    "python.testing.unittestEnabled": false,
+    "python.testing.pytestEnabled": true
 }
\ No newline at end of file
diff --git a/api/funkwhale_api/common/management/commands/gitpod.py b/api/funkwhale_api/common/management/commands/gitpod.py
new file mode 100644
index 0000000000000000000000000000000000000000..46561220ca7bd97de76c34c02dfafe870e9dc4ff
--- /dev/null
+++ b/api/funkwhale_api/common/management/commands/gitpod.py
@@ -0,0 +1,72 @@
+from django.core.management.commands.migrate import Command as BaseCommand
+from django.core.management import call_command
+from funkwhale_api.music.models import Library
+from funkwhale_api.users.models import User
+import uvicorn
+import debugpy
+import os
+
+
+class Command(BaseCommand):
+    help = "Manage gitpod environment"
+
+    def add_arguments(self, parser):
+        parser.add_argument("command", nargs="?", type=str)
+
+    def handle(self, *args, **options):
+        command = options["command"]
+
+        if not command:
+            return self.show_help()
+
+        if command == "init":
+            return self.init()
+
+        if command == "dev":
+            return self.dev()
+
+    def show_help(self):
+        self.stdout.write("")
+        self.stdout.write("Available commands:")
+        self.stdout.write("init - Initialize gitpod workspace")
+        self.stdout.write("dev - Run Funkwhale in development mode with debug server")
+        self.stdout.write("")
+
+    def init(self):
+        try:
+            user = User.objects.get(username="gitpod")
+        except Exception:
+            call_command("createsuperuser", username="gitpod", email="gitpod@example.com", no_input=False)
+            user = User.objects.get(username="gitpod")
+
+        user.set_password('gitpod')
+        if not user.actor:
+            user.create_actor()
+
+        user.save()
+
+        # Download music catalog
+        os.system("git clone https://dev.funkwhale.audio/funkwhale/catalog.git /tmp/catalog")
+        os.system("mv -f /tmp/catalog/music /workspace/funkwhale/data")
+        os.system("rm -rf /tmp/catalog/music")
+
+        # # Import music catalog into library
+        call_command("script", "migrate_to_user_libraries", no_input=False)
+        call_command(
+            "import_files",
+            Library.objects.get(actor=user.actor).uuid,
+            "/workspace/funkwhale/data/music/",
+            recursive=True,
+            in_place=True,
+            no_input=False,
+        )
+
+    def dev(self):
+        debugpy.listen(5678)
+        uvicorn.run(
+            "config.asgi:application",
+            host="0.0.0.0",
+            port=5000,
+            reload=True,
+            reload_dirs=["/workspace/funkwhale/api/config/", "/workspace/funkwhale/api/funkwhale_api/"],
+        )
diff --git a/api/funkwhale_api/music/management/commands/import_files.py b/api/funkwhale_api/music/management/commands/import_files.py
index 1c1d0917cba3abb638ff08f5f8f51d45eb34b625..cc56e07a9f485715fca234af05406320cdaf41f6 100644
--- a/api/funkwhale_api/music/management/commands/import_files.py
+++ b/api/funkwhale_api/music/management/commands/import_files.py
@@ -279,7 +279,7 @@ class Command(BaseCommand):
                 if p and not import_path.startswith(p):
                     raise CommandError(
                         "Importing in-place only works if importing "
-                        "from {} (MUSIC_DIRECTORY_PATH), as this directory"
+                        "from {} (MUSIC_DIRECTORY_PATH), as this directory "
                         "needs to be accessible by the webserver."
                         "Culprit: {}".format(p, import_path)
                     )
diff --git a/api/poetry.lock b/api/poetry.lock
index 1bbdaccc01d51a7297f76fe631bea33cb16449fb..355f0fea849d2e26246d9e58c9a4898f7b654cf1 100644
--- a/api/poetry.lock
+++ b/api/poetry.lock
@@ -529,6 +529,14 @@ twisted = {version = ">=18.7", extras = ["tls"]}
 [package.extras]
 tests = ["hypothesis (==4.23)", "pytest (>=3.10,<4.0)", "pytest-asyncio (>=0.8,<1.0)"]
 
+[[package]]
+name = "debugpy"
+version = "1.6.2"
+description = "An implementation of the Debug Adapter Protocol for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
 [[package]]
 name = "decorator"
 version = "5.1.1"
@@ -2120,7 +2128,7 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.7"
-content-hash = "66a96848a355caf841c42228ca2f411fef64f2507cf91d6d056038b2d2a2c22b"
+content-hash = "f09ed385656e74fcd8d2c68ef9c43768de5459eed452dc9a4544df19894b7bfe"
 
 [metadata.files]
 aiohttp = [
@@ -2470,6 +2478,26 @@ daphne = [
     {file = "daphne-3.0.2-py3-none-any.whl", hash = "sha256:a9af943c79717bc52fe64a3c236ae5d3adccc8b5be19c881b442d2c3db233393"},
     {file = "daphne-3.0.2.tar.gz", hash = "sha256:76ffae916ba3aa66b46996c14fa713e46004788167a4873d647544e750e0e99f"},
 ]
+debugpy = [
+    {file = "debugpy-1.6.2-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:77a47d596ce8c69673d5f0c9876a80cb5a6cbc964f3b31b2d44683c7c01b6634"},
+    {file = "debugpy-1.6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:726e5cc0ed5bc63e821dc371d88ddae5cba85e2ad207bf5fefc808b29421cb4c"},
+    {file = "debugpy-1.6.2-cp310-cp310-win32.whl", hash = "sha256:9809bd1cdc0026fab711e280e0cb5d8f89ae5f4f74701aba5bda9a20a6afb567"},
+    {file = "debugpy-1.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:40741d4bbf59baca1e97a5123514afcc036423caae5f24db23a865c0b4167c34"},
+    {file = "debugpy-1.6.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:67749e972213c395647a8798cc8377646e581e1fe97d0b1b7607e6b112ae4511"},
+    {file = "debugpy-1.6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e3c43d650a1e5fa7110af380fb59061bcba1e7348c00237e7473c55ae499b96"},
+    {file = "debugpy-1.6.2-cp37-cp37m-win32.whl", hash = "sha256:9e572c2ac3dd93f3f1a038a9226e7cc0d7326b8d345c9b9ce6fbf9cb9822e314"},
+    {file = "debugpy-1.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:ac5d9e625d291a041ff3eaf65bdb816eb79a5b204cf9f1ffaf9617c0eadf96fa"},
+    {file = "debugpy-1.6.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:9f72435bc9a2026a35a41221beff853dd4b6b17567ba9b9d349ee9512eb71ce6"},
+    {file = "debugpy-1.6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:aaf579de5ecd02634d601d7cf5b6baae5f5bab89a55ef78e0904d766ef477729"},
+    {file = "debugpy-1.6.2-cp38-cp38-win32.whl", hash = "sha256:0984086a670f46c75b5046b39a55f34e4120bee78928ac4c3c7f1c7b8be1d8be"},
+    {file = "debugpy-1.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:19337bb8ff87da2535ac00ea3877ceaf40ff3c681421d1a96ab4d67dad031a16"},
+    {file = "debugpy-1.6.2-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:163f282287ce68b00a51e9dcd7ad461ef288d740dcb3a2f22c01c62f31b62696"},
+    {file = "debugpy-1.6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4909bb2f8e5c8fe33d6ec5b7764100b494289252ebe94ec7838b30467435f1cb"},
+    {file = "debugpy-1.6.2-cp39-cp39-win32.whl", hash = "sha256:3b4657d3cd20aa454b62a70040524d3e785efc9a8488d16cd0e6caeb7b2a3f07"},
+    {file = "debugpy-1.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:79d9ac34542b830a7954ab111ad8a4c790f1f836b895d03223aea4216b739208"},
+    {file = "debugpy-1.6.2-py2.py3-none-any.whl", hash = "sha256:0bfdcf261f97a603d7ef7ab6972cdf7136201fde93d19bf3f917d0d2e43a5694"},
+    {file = "debugpy-1.6.2.zip", hash = "sha256:e6047272e97a11aa6898138c1c88c8cf61838deeb2a4f0a74e63bb567f8dafc6"},
+]
 decorator = [
     {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
     {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
diff --git a/api/pyproject.toml b/api/pyproject.toml
index cb18109933da0fbba731c78c510a895beeb82610..2757aab2769ece905edfe43f640d5374fffbdb35 100644
--- a/api/pyproject.toml
+++ b/api/pyproject.toml
@@ -57,7 +57,7 @@ django-auth-ldap = "==4.1.0"
 uvicorn = {version = "==0.17.6", extras = ["standard"]}
 django-cache-memoize = "0.1.10"
 requests-http-message-signatures = "==0.3.1"
-drf-spectacular = "0.22.1"
+drf-spectacular = "==0.22.1"
 
 [tool.poetry.dev-dependencies]
 flake8 = "==3.9.2"
@@ -80,6 +80,7 @@ aioresponses = "==0.7.3"
 prompt-toolkit = "==3.0.30"
 black = "==22.6.0"
 ipdb = "==0.13.9"
+debugpy = "==1.6.2"
 
 [build-system]
 requires = ["poetry-core>=1.0.0"]
diff --git a/changes/changelog.d/1875.enchancement b/changes/changelog.d/1875.enchancement
new file mode 100644
index 0000000000000000000000000000000000000000..0026c1ff418af26dbe9c364ef5b460f2011dbc8e
--- /dev/null
+++ b/changes/changelog.d/1875.enchancement
@@ -0,0 +1 @@
+Add python debug and test support for gitpod
\ No newline at end of file