diff --git a/changes/changelog.d/456.feature b/changes/changelog.d/456.feature
new file mode 100644
index 0000000000000000000000000000000000000000..f05188e98c51fffa2bbcde613e62e62d3d46f2ed
--- /dev/null
+++ b/changes/changelog.d/456.feature
@@ -0,0 +1,8 @@
+Make funkwhale themable by loading external stylesheets (#456)
+
+Custom themes for Funkwhale
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If you ever wanted to give a custom look and feel to your instance, this is now possible.
+
+Check https://docs.funkwhale.audio/configuration.html#theming if you want to know more!
diff --git a/docs/configuration.rst b/docs/configuration.rst
index 3e7b5aa25df89add324d1aab753b2303504c9efb..6874476974dc250e2110f3e9bf80e057178dfdf8 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -158,3 +158,79 @@ permissions are:
 There is no dedicated interface to manage users permissions, but superusers
 can login on the Django's admin at ``/api/admin/`` and grant permissions
 to users at ``/api/admin/users/user/``.
+
+Theming
+-------
+
+Funkwhale supports custom themes, which are great if you want to personnalize the
+look and feel of your instance. Theming is achieved by declaring
+additionnal stylesheets you want to load in the front-end.
+
+Customize the settings
+^^^^^^^^^^^^^^^^^^^^^^
+
+In order to know what stylesheets to load, the front-end requests the following
+url: ``https://your.instance/settings.json``. On typical deployments, this url
+returns a 404 error, which is simply ignored.
+
+However, if you return the appropriate payload on this url, you can make the magic
+work. We will store the necessary files in the ``/srv/funkwhale/custom`` directory:
+
+.. code-block:: shell
+
+    cd /srv/funkwhale/
+    mkdir custom
+    cat <<EOF > custom/settings.json
+    {
+      "additionalStylesheets": ["/custom/custom.css"]
+    }
+    EOF
+    cat <<EOF > custom/custom.css
+    body {
+      background-color: red;
+    }
+    EOF
+
+By executing the previous commands, you will end up with two files in your ``/srv/funkwhale/custom``
+directory:
+
+- ``settings.json`` will tell the front-end what stylesheets you want to load (``/custom/custom.css`` in this example)
+- ``custom.css`` will hold your custom CSS
+
+The last step to make this work is to ensure both files are served by the reverse proxy.
+
+On nginx, add the following snippet to your vhost config::
+
+    location /settings.json {
+        alias /srv/funkwhale/custom/settings.json;
+    }
+    location /custom {
+        alias /srv/funkwhale/custom;
+    }
+
+On apache, use the following one::
+
+    Alias /settings.json /srv/funkwhale/custom/settings.json
+    Alias /custom /srv/funkwhale/custom
+
+    <Directory "/srv/funkwhale/custom">
+      Options FollowSymLinks
+      AllowOverride None
+      Require all granted
+    </Directory>
+
+Once done, reload your reverse proxy, refresh Funkwhale in your web browser, and you should see
+a red background.
+
+.. note::
+
+    You can reference external urls as well in ``settings.json``, simply use
+    the full urls. Be especially careful with external urls as they may affect your users
+    privacy.
+
+.. warning::
+
+    Loading additional stylesheets and CSS rules can affect the performance and
+    usability of your instance. If you encounter issues with the interfaces and use
+    custom stylesheets, try to disable those to ensure the issue is not caused
+    by your customizations.
diff --git a/front/config/index.js b/front/config/index.js
index d10f35e9146258cc4b2271ca2270bd6ae09b4c98..44a3b640bfa3805b9af88dcf209b600d862d9f72 100644
--- a/front/config/index.js
+++ b/front/config/index.js
@@ -29,6 +29,9 @@ module.exports = {
     assetsSubDirectory: 'static',
     assetsPublicPath: '/',
     proxyTable: {
+      '/settings.json': {
+        target: 'http://127.0.0.1:8000/static/',
+      },
       '**': {
         target: 'http://nginx:6001',
         changeOrigin: true,
diff --git a/front/src/App.vue b/front/src/App.vue
index 58ed698aa98cc1236dbfebd7d5345b5739033b00..8b1ac775a18356753b01c172013190337fb5b765 100644
--- a/front/src/App.vue
+++ b/front/src/App.vue
@@ -1,5 +1,7 @@
 <template>
   <div id="app">
+    <!-- here, we display custom stylesheets, if any -->
+    <link v-for="url in customStylesheets" rel="stylesheet" property="stylesheet" :href="url" :key="url">
     <div class="ui main text container instance-chooser" v-if="!$store.state.instance.instanceUrl">
       <div class="ui padded segment">
         <h1 class="ui header"><translate>Choose your instance</translate></h1>
@@ -175,6 +177,11 @@ export default {
         return null
       }
       return _.get(this.nodeinfo, 'software.version')
+    },
+    customStylesheets () {
+      if (this.$store.state.instance.frontSettings) {
+        return this.$store.state.instance.frontSettings.additionalStylesheets || []
+      }
     }
   },
   watch: {
diff --git a/front/src/main.js b/front/src/main.js
index 7f60c602c9559f71484c9e45d1bed0c9334adad5..fde332acc7485e3c0e5a48a74c73cc44d39e8d36 100644
--- a/front/src/main.js
+++ b/front/src/main.js
@@ -126,6 +126,8 @@ axios.interceptors.response.use(function (response) {
   return Promise.reject(error)
 })
 
+store.dispatch('instance/fetchFrontSettings')
+
 /* eslint-disable no-new */
 new Vue({
   el: '#app',
diff --git a/front/src/store/instance.js b/front/src/store/instance.js
index 95de94171ece68fe69f57ffbe9b4101e42097e86..64e48f7b4b557e87f141a47a231069a84853220d 100644
--- a/front/src/store/instance.js
+++ b/front/src/store/instance.js
@@ -6,6 +6,7 @@ export default {
   namespaced: true,
   state: {
     maxEvents: 200,
+    frontSettings: {},
     instanceUrl: process.env.INSTANCE_URL,
     events: [],
     settings: {
@@ -53,6 +54,9 @@ export default {
     events: (state, value) => {
       state.events = value
     },
+    frontSettings: (state, value) => {
+      state.frontSettings = value
+    },
     instanceUrl: (state, value) => {
       if (value && !value.endsWith('/')) {
         value = value + '/'
@@ -110,6 +114,13 @@ export default {
       }, response => {
         logger.default.error('Error while fetching settings', response.data)
       })
+    },
+    fetchFrontSettings ({commit}) {
+      return axios.get('/settings.json').then(response => {
+        commit('frontSettings', response.data)
+      }, response => {
+        logger.default.error('Error when fetching front-end configuration (or no customization available)')
+      })
     }
   }
 }
diff --git a/front/static/custom.css b/front/static/custom.css
new file mode 100644
index 0000000000000000000000000000000000000000..a5bbb2cff2be83d5920f18110082708b06882881
--- /dev/null
+++ b/front/static/custom.css
@@ -0,0 +1 @@
+/* This is a custom CSS file that can be loaded thanks to settings.json */
diff --git a/front/static/settings.json b/front/static/settings.json
new file mode 100644
index 0000000000000000000000000000000000000000..a8bd9b91df975a32c698f33fa96cd350c4744a61
--- /dev/null
+++ b/front/static/settings.json
@@ -0,0 +1,3 @@
+{
+  "additionalStylesheets": ["/static/custom.css"]
+}