diff --git a/pica-mumble-web/.dockerignore b/pica-mumble-web/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..8d3188d51af5570cae953804a4db830dc67f7799
--- /dev/null
+++ b/pica-mumble-web/.dockerignore
@@ -0,0 +1,3 @@
+README.md
+docker-compose.yml
+secrets/
diff --git a/pica-mumble-web/Dockerfile b/pica-mumble-web/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..4640db03748866b90d1d70569a0fd7f06f47f84d
--- /dev/null
+++ b/pica-mumble-web/Dockerfile
@@ -0,0 +1,33 @@
+FROM alpine:3.12
+
+RUN echo http://nl.alpinelinux.org/alpine/edge/testing >> /etc/apk/repositories && \
+    apk add --update --no-cache nodejs npm tini websockify git && \
+    git clone --branch features/fabien/framasoft-theme https://framagit.org/fabien4vo/mumble-web.git /home/node && \
+    adduser -D -g 1001 -u 1001 -h /home/node node && \
+    mkdir -p /home/node && \
+    mkdir -p /home/node/.npm-global && \
+    mkdir -p /home/node/app  && \
+    chown -R node: /home/node 
+
+USER node
+
+ENV PATH=/home/node/.npm-global/bin:$PATH
+ENV NPM_CONFIG_PREFIX=/home/node/.npm-global
+
+RUN cd /home/node && \
+    npm install && \
+    npm run build 
+
+USER root
+
+RUN apk del gcc git
+
+USER node
+
+COPY index.html /home/node/app/index.html
+
+EXPOSE 8080
+ENV MUMBLE_SERVER=murmur:64738
+
+ENTRYPOINT ["/sbin/tini", "--"]
+CMD websockify --ssl-target --web=/home/node/dist 8080 "$MUMBLE_SERVER"
diff --git a/pica-mumble-web/clair-whitelist.yml b/pica-mumble-web/clair-whitelist.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/pica-mumble-web/docker-compose.yml b/pica-mumble-web/docker-compose.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d2aa8e74bb1bc48c8df4ce8e79e5813deddb7314
--- /dev/null
+++ b/pica-mumble-web/docker-compose.yml
@@ -0,0 +1,22 @@
+version: "2.4"
+networks:
+  docker_default:
+    external: true
+    name: "docker_default"
+
+services:
+  mumble-web:
+    image: registry.picasoft.net/pica-mumble-web:1.3.0
+    container_name: mumble-web
+    environment:
+      MUMBLE_SERVER: "murmur:64738"
+    networks:
+      - docker_default
+    volumes:
+      - /DATA/docker/volumes/murmur/mumble-web/config.js:/home/node/dist/config.local.js
+    labels:
+      - "traefik.frontend.rule=Host:voice.picasoft.net"
+      - "traefik.port=8080"
+      - "traefik.frontend.passHostHeader=true"
+      - "traefik.enable=true"
+    restart: unless-stopped
diff --git a/pica-mumble-web/index.html b/pica-mumble-web/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..70409982e1268cf4aa89d4f7b25f1b852a22399f
--- /dev/null
+++ b/pica-mumble-web/index.html
@@ -0,0 +1,652 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <!-- Favicon as generated by realfavicongenerator.net (slightly modified for webpack) -->
+    <link rel="apple-touch-icon" sizes="180x180" href="favicon/apple-touch-icon.png">
+    <link rel="icon" type="image/png" href="favicon/favicon-32x32.png" sizes="32x32">
+    <link rel="icon" type="image/png" href="favicon/favicon-16x16.png" sizes="16x16">
+    <link rel="manifest" href="favicon/manifest.json">
+    <link rel="mask-icon" href="favicon/safari-pinned-tab.svg" color="#5bbad5">
+    <link rel="shortcut icon" href="favicon/favicon.ico">
+    <meta name="apple-mobile-web-app-title" content="Mumble">
+    <meta name="application-name" content="Mumble">
+    <meta name="msapplication-config" content="${require('./favicon/browserconfig.xml')}">
+    <meta name="theme-color" content="#ffffff">
+
+    <script src="config.js"></script>
+    <script src="config.local.js"></script>
+    <script src="theme.js"></script>
+    <script src="matrix.js"></script>
+  </head>
+  <body>
+    <div class="loading-container" data-bind="css: { loaded: true }">
+      <div class="loading-circle" data-bind="css: { loaded: true }"><div></div><div><div></div></div></div>
+    </div>
+    <div id="container" style="display: none" data-bind="visible: true,
+                                                         css: { minimal: minimalView }">
+
+      <div class="dialog-overlay" data-bind="visible: connectDialog.visible() && config.defaults.easyMode">
+        <div class="startup-dialog dialog">
+          <div class="dialog-header" data-bind="text: connected() ? 'Connecté' : 'Préparation'"></div>
+		  <div style="text-align: center">Bienvenue sur le Mumble de Picasoft !</div>
+		  <div style="text-align: center">Ceci est l'interface web, elle permet d'utiliser le service, mais offre beaucoup moins de fonctionnalités que le client lourd. Vous pouvez consulter la <a href="https://voice.picasoft.net/doc">Documentation</a> pour plus d'informations.</div>
+          <form data-bind="submit: connectDialog.connect">
+             <!-- ko if: !connected() -->
+             <fieldset>
+              <legend>Rejoindre le salon <span data-bind="text: connectDialog.channelName() == '' ? 'principal' : connectDialog.channelName()"></span></legend>
+                  <label data-bind="visible: !connectDialog.loading()">
+                    Quel est votre nom ?
+                    <input type="text" data-bind="textInput: connectDialog.username" required autofocus>
+                  </label>
+                  <div data-bind="visible: connectDialog.loading()">
+                    Connexion en tant que <span data-bind="text: connectDialog.username"></span>,
+                    merci de patienter ...
+                    <div class="small"><div class="loading-container">
+                      <div class="loading-circle"><div></div><div><div></div></div></div>
+                    </div></div>
+                  </div>
+              <input class="dialog-submit" type="submit" data-bind="{
+                value: !connected() && !connectDialog.loading() ? 'Participer' : 'Connexion...',
+                disable: !connectDialog.username() || connectDialog.loading()
+              }">
+            </fieldset>
+            <!-- /ko -->
+            <!-- ko if: connected() -->
+            <fieldset>
+              Bonjour <span data-bind="text: thisUser().name()"></span>,
+              vous voilà connecté sur le salon <span data-bind="text: thisUser().channel().name()"></span>.
+              <!-- ko if: thisUser().channel() -->
+              <p>Il y a <span data-bind="text: thisUser().channel().users().length"></span> participant&middot;e&middot;s à ce salon</p>
+              <!-- /ko -->
+              <ul data-bind="foreach: thisUser().channel().users()" style="text-align: left">
+                <li>
+                  <span data-bind="{text: name, style: {fontWeight: name() == $parent.thisUser().name() ? 'bold': 'normal'}}"></span>
+                  <div data-bind="class: talking" class="talking"><div><div data-bind="text: talking"></div><div></div></div></div>
+                </li>
+              </ul>
+              <button class="dialog-submit" data-bind="click: connectDialog.hide">Interface complète</button>
+            </fieldset>
+            <!-- /ko -->
+          </form>
+        </div>
+      </div>
+
+      <!-- ko with: connectDialog --> 
+      <div class="connect-dialog dialog" data-bind="visible: visible() && !joinOnly() && !config.defaults.easyMode">
+          <div class="dialog-header">
+            Connect to Server
+          </div>
+          <form data-bind="submit: connect">
+            <table>
+                <tr data-bind="if: $root.config.connectDialog.address">
+                  <td>Address</td>
+                  <td><input id="address" type="text" data-bind="value: address" required></td>
+                </tr>
+                <tr data-bind="if: $root.config.connectDialog.port">
+                  <td>Port</td>
+                  <td><input id="port" type="text" data-bind="value: port" required></td>
+                </tr>
+                <tr data-bind="if: $root.config.connectDialog.username">
+                  <td>Username</td>
+                  <td><input id="username" type="text" data-bind="value: username" required></td>
+                </tr>
+                <tr data-bind="if: $root.config.connectDialog.password">
+                  <td>Password</td>
+                  <td><input id="password" type="password" data-bind="value: password"></td>
+                </tr>
+                <tr data-bind="if: $root.config.connectDialog.token">
+                  <td>Tokens</td>
+                  <td>
+                      <input type="text" data-bind='value: tokenToAdd, valueUpdate: "afterkeydown"'>
+                  </td>
+                </tr>
+                <tr data-bind="if: $root.config.connectDialog.token">
+                      <td></td>
+                      <td>
+                        <button class="dialog-submit" type="button" data-bind="enable: selectedTokens().length > 0, click: removeSelectedTokens()">Remove</button>
+                        <button class="dialog-submit" type="button" data-bind="enable: tokenToAdd().length > 0, click: addToken()">Add</button>
+                     </td>
+                </tr>
+               <tr data-bind="if: $root.config.connectDialog.token, visible: tokens().length > 0">
+                <td></td>
+                  <td><select id="token" multiple="multiple" height="5" data-bind="options:tokens, selectedOptions:selectedTokens"></select></td>
+                </tr>
+                <tr data-bind="if: $root.config.connectDialog.channelName">
+                  <td>Channel</td>
+                  <td><input  id="channelName" type="text" data-bind="value: channelName"></td>
+                </tr>
+            </table>
+            <div class="dialog-footer">
+              <input class="dialog-close" type="button" data-bind="click: hide" value="Cancel">
+              <input class="dialog-submit" type="submit" value="Connect">
+            </div>
+          </form>
+        </div>
+      <!-- /ko -->
+      <!-- ko with: connectDialog -->
+      <div class="join-dialog dialog" data-bind="visible: visible() && joinOnly()">
+          <div class="dialog-header">
+            Mumble Voice Conference
+          </div>
+          <form data-bind="submit: connect">
+            <input class="dialog-submit" type="submit" value="Join Conference">
+          </form>
+        </div>
+      <!-- /ko -->
+      <!-- ko with: connectErrorDialog -->
+       <div class="connect-dialog error-dialog dialog" data-bind="visible: visible()">
+          <div class="dialog-header">
+            Failed to connect
+          </div>
+          <form data-bind="submit: connect">
+            <table>
+                <tr>
+                  <td colspan=2>
+                    <!-- ko if: type() == 0 || type() == 8 -->
+                    The connection has been refused.
+                    <!-- /ko -->
+                    <!-- ko if: type() == 1 -->
+                    The server uses an incompatible version.
+                    <!-- /ko -->
+                    <!-- ko if: type() == 2 -->
+                    Your user name was rejected. Maybe try a different one?
+                    <!-- /ko -->
+                    <!-- ko if: type() == 3 -->
+                    The given password is incorrect.
+                    The user name you have chosen requires a special one.
+                    <!-- /ko -->
+                    <!-- ko if: type() == 4 -->
+                    The given password is incorrect.
+                    <!-- /ko -->
+                    <!-- ko if: type() == 5 -->
+                    The user name you have chosen is already in use.
+                    <!-- /ko -->
+                    <!-- ko if: type() == 6 -->
+                    The server is full.
+                    <!-- /ko -->
+                    <!-- ko if: type() == 7 -->
+                    The server requires you to provide a client certificate
+                    which is not supported by this web application.
+                    <!-- /ko -->
+                    <br>
+                    The server reports:
+                    <br>
+                    "<span class="connect-error-reason" data-bind="text: reason"></span>"
+                  </td>
+                </tr>
+                <tr data-bind="if: type() == 2 || type() == 3 || type() == 5">
+                  <td>Username</td>
+                  <td><input id="username" type="text" data-bind="value: username" required></td>
+                </tr>
+                <tr data-bind="if: type() == 3 || type() == 4">
+                  <td>Password</td>
+                  <td><input id="password" type="password" data-bind="value: password" required></td>
+                </tr>
+            </table>
+            <div class="dialog-footer">
+              <input class="dialog-close" type="button" value="Cancel"
+                     data-bind="click: hide, visible: !joinOnly()">
+              <input class="dialog-submit" type="submit" value="Retry">
+            </div>
+          </form>
+        </div>
+      <!-- /ko -->
+      <!-- ko with: connectionInfo -->
+      <div class="connection-info-dialog dialog" data-bind="visible: visible">
+        <div class="dialog-header">
+          Connection Information
+        </div>
+        <div class="dialog-content">
+          <h3>Version</h3>
+          <!-- ko with: serverVersion -->
+          Protocol
+          <span data-bind="text: major + '.' + minor + '.' + patch"></span>.
+          <br>
+          <br>
+          <span data-bind="text: release"></span>
+          <br>
+          <span data-bind="text: os"></span>
+          <span data-bind="text: osVersion"></span>
+          <br>
+          <!-- /ko -->
+          <!-- ko if: !serverVersion() -->
+          Unknown
+          <!-- /ko -->
+
+          <h3>Control channel</h3>
+          <span data-bind="text: latencyMs().toFixed(2)"></span> ms average latency
+          (<span data-bind="text: latencyDeviation().toFixed(2)"></span> deviation)
+          <br>
+          <br>
+          Remote host <span data-bind="text: remoteHost"></span>
+          (port <span data-bind="text: remotePort"></span>)
+          <br>
+
+          <h3>Audio bandwidth</h3>
+          Maximum <span data-bind="text: (maxBitrate()/1000).toFixed(1)"></span> kbits/s
+          (<span data-bind="text: (maxBandwidth()/1000).toFixed(1)"></span> kbits/s with overhead)
+          <br>
+          Current <span data-bind="text: (currentBitrate()/1000).toFixed(1)"></span> kbits/s
+          (<span data-bind="text: (currentBandwidth()/1000).toFixed(1)"></span> kbits/s with overhead)
+          <br>
+          Codec: <span data-bind="text: codec"></span>
+        </div>
+        <div class="dialog-footer">
+          <input class="dialog-close" type="button" data-bind="click: hide" value="OK">
+        </div>
+      </div>
+      <!-- /ko -->
+      <!-- ko with: settingsDialog -->
+      <div class="settings-dialog dialog" data-bind="visible: $data">
+          <div class="dialog-header">
+            Settings
+          </div>
+          <form data-bind="submit: $root.applySettings">
+            <table>
+                <tr>
+                  <td>Transmission</td>
+                  <td>
+                    <select data-bind='value: voiceMode'>
+                      <option value="cont">Continuous</option>
+                      <option value="vad">Voice Activity</option>
+                      <option value="ptt">Push To Talk</option>
+                  </td>
+                </tr>
+                <tr data-bind="visible: voiceMode() == 'vad'">
+                  <td colspan="2">
+                    <div class="mic-volume-container">
+                      <div class="mic-volume" data-bind="style: {
+                          width: testVadLevel()*100 + '%',
+                          background: testVadActive() ? 'green' : 'red'
+                        }"></div>
+                    </div>
+                    <input type="range" min="0" max="1" step="0.01"
+                           data-bind="value: vadLevel">
+                  </td>
+                </tr>
+                <tr data-bind="visible: voiceMode() == 'ptt'">
+                  <td>PTT Key</td>
+                  <td>
+                    <input type="button" data-bind="value: pttKeyDisplay, click: recordPttKey">
+                  </td>
+                </tr>
+                <tr>
+                  <td>Audio Quality</td>
+                  <td><span data-bind="text: (audioBitrate()/1000).toFixed(1)"></span> kbit/s</td>
+                </tr>
+                <tr>
+                  <td colspan="2">
+                    <input type="range" min="8000" max="96000" step="8"
+                           data-bind="value: audioBitrate, valueUpdate: 'input'">
+                  </td>
+                </tr>
+                <tr>
+                  <td>Audio per packet</td>
+                  <td><span data-bind="text: msPerPacket"></span> ms</td>
+                </tr>
+                <tr>
+                  <td colspan="2">
+                    <input type="range" min="10" max="60" step="10"
+                           data-bind="value: msPerPacket, valueUpdate: 'input'">
+                  </td>
+                </tr>
+                <tr>
+                  <td colspan="2" class="bandwidth-info">
+                    <span data-bind="text: (totalBandwidth()/1000).toFixed(1)"></span>
+                    kbit/s
+                    (Audio
+                    <span data-bind="text: (audioBitrate()/1000).toFixed(1)"></span>,
+                    Position
+                    <span data-bind="text: (positionBandwidth()/1000).toFixed(1)"></span>,
+                    Overhead
+                    <span data-bind="text: (overheadBandwidth()/1000).toFixed(1)"></span>)
+                  </td>
+                </tr>
+                <tr>
+                  <td>Show Avatars</td>
+                  <td>
+                    <select data-bind='value: showAvatars'>
+                      <option value="always">Always</option>
+                      <option value="own_channel">Same Channel</option>
+                      <option value="linked_channel">Linked Channels</option>
+                      <option value="minimal_only">Minimal View</option>
+                      <option value="never">Never</option>
+                  </td>
+                </tr>
+                <tr>
+                    <td colspan="2">
+                        <input type="checkbox" data-bind="checked: userCountInChannelName">
+                        Show user count after channel name
+                    </td>
+                </tr>
+            </table>
+            <div class="dialog-footer">
+              <input class="dialog-close" type="button" data-bind="click: $root.closeSettings" value="Cancel">
+              <input class="dialog-submit" type="submit" value="Apply">
+            </div>
+          </form>
+        </div>
+      <!-- /ko -->
+      <img class="avatar-view" data-bind="visible: avatarView, attr: { src: avatarView },
+                                          click: function () { avatarView(null) }"></img>
+      <!-- ko with: userContextMenu -->
+      <ul class="context-menu" data-bind="if: target,
+                                          style: { left: posX() + 'px',
+                                                   top:  posY() + 'px' }">
+
+        <!-- ko with: target -->
+        <li data-bind="css: { disabled: !canChangeMute() }">
+          Mute
+        </li>
+        <li data-bind="css: { disabled: !canChangeDeafen() }">
+          Deafen
+        </li>
+        <li data-bind="css: { disabled: !canChangePrioritySpeaker() }">
+          Priority Speaker
+        </li>
+
+        <li data-bind="css: { disabled: !canLocalMute() }">
+          Local Mute
+        </li>
+        <li data-bind="css: { disabled: !canIgnoreMessages() }">
+          Ignore Messages
+        </li>
+
+        <li data-bind="css: { disabled: !canChangeComment() }, visible: comment">
+          View Comment
+        </li>
+        <!-- ko if: $data === $root.thisUser() -->
+        <li data-bind="css: { disabled: !canChangeComment() }, visible: true">
+          Change Comment
+        </li>
+        <!-- /ko -->
+        <!-- ko if: $data !== $root.thisUser() -->
+        <li data-bind="css: { disabled: !canChangeComment() }, visible: comment">
+          Reset Comment
+        </li>
+        <!-- /ko -->
+
+        <li data-bind="css: { disabled: !canChangeAvatar() }, visible: texture,
+                       click: viewAvatar">
+          View Avatar
+        </li>
+        <!-- ko if: $data === $root.thisUser() -->
+        <li data-bind="css: { disabled: !canChangeAvatar() }, visible: true,
+                       click: changeAvatar">
+          Change Avatar
+        </li>
+        <!-- /ko -->
+        <li data-bind="css: { disabled: !canChangeAvatar() }, visible: texture,
+                       click: removeAvatar">
+          Reset Avatar
+        </li>
+
+        <li data-bind="css: { disabled: true }, visible: true">
+          Send Message
+        </li>
+        <li data-bind="css: { disabled: true }, visible: true">
+          Information
+        </li>
+
+        <li data-bind="visible: $data === $root.thisUser(),
+                       css: { checked: selfMute },
+                       click: toggleMute">
+          Self Mute
+        </li>
+        <li data-bind="visible: $data === $root.thisUser(),
+                       css: { checked: selfDeaf },
+                       click: toggleDeaf">
+          Self Deafen
+        </li>
+
+        <!-- ko if: $data !== $root.thisUser() -->
+        <li data-bind="css: { disabled: true }, visible: true">
+          Add Friend
+        </li>
+        <li data-bind="css: { disabled: true }, visible: false">
+          Remove Friend
+        </li>
+        <!-- /ko -->
+
+        <!-- /ko -->
+      </ul>
+      <!-- /ko -->
+      <!-- ko with: channelContextMenu -->
+      <ul class="context-menu" data-bind="if: target,
+                                          style: { left: posX() + 'px',
+                                                   top:  posY() + 'px' }">
+
+        <!-- ko with: target -->
+        <li data-bind="visible: users.indexOf($root.thisUser()) === -1,
+                       css: { disabled: !canJoin() },
+                       click: $root.requestMove.bind($root, $root.thisUser())">
+          Join Channel
+        </li>
+        <li data-bind="css: { disabled: !canAdd() }">
+          Add
+        </li>
+        <li data-bind="css: { disabled: !canEdit() }">
+          Edit
+        </li>
+        <li data-bind="css: { disabled: !canRemove() }">
+          Remove
+        </li>
+
+        <li data-bind="css: { disabled: !canLink() }">
+          Link
+        </li>
+        <li data-bind="css: { disabled: !canUnlink() }">
+          Unlink
+        </li>
+        <li data-bind="css: { disabled: !canUnlink() }">
+          Unlink All
+        </li>
+
+        <li data-bind="css: { disabled: true }">
+          Copy Mumble URL
+        </li>
+        <li data-bind="css: { disabled: true }">
+          Copy Mumble-Web URL
+        </li>
+        <li data-bind="css: { disabled: !canSendMessage() }">
+          Send Message
+        </li>
+
+        <!-- /ko -->
+      </ul>
+      <!-- /ko -->
+      <script type="text/html" id="user-tag">
+        <span class="user-tag" data-bind="text: name"></span>
+      </script>
+      <script type="text/html" id="channel-tag">
+        <span class="channel-tag" data-bind="text: name"></span>
+      </script>
+      <div class="toolbar" data-bind="css: { 'toolbar-horizontal': toolbarHorizontal(),
+                                             'toolbar-vertical': !toolbarHorizontal() }">
+        <img class="handle-horizontal" src="/svg/handle_horizontal.svg"
+             data-bind="click: toggleToolbarOrientation">
+        <img class="handle-vertical" src="/svg/handle_vertical.svg"
+             data-bind="click: toggleToolbarOrientation">
+        <img class="tb-connect" data-bind="visible: !connectDialog.joinOnly(),
+                                           click: connectDialog.show"
+                      rel="connect" src="/svg/applications-internet.svg">
+        <img class="tb-information" rel="information" src="/svg/information_icon.svg"
+             data-bind="click: connectionInfo.show,
+                        css: { disabled: !thisUser() }">
+        <div class="divider"></div>
+        <img class="tb-mute" data-bind="visible: !selfMute(),
+                              click: function () { requestMute(thisUser()) }"
+                      rel="mute" src="/svg/audio-input-microphone.svg">
+        <img class="tb-unmute tb-active" data-bind="visible: selfMute,
+                              click: function () { requestUnmute(thisUser()) }"
+                      rel="unmute" src="/svg/audio-input-microphone-muted.svg">
+        <img class="tb-deaf" data-bind="visible: !selfDeaf(),
+                              click: function () { requestDeaf(thisUser()) }"
+                      rel="deaf" src="/svg/audio-output.svg">
+        <img class="tb-undeaf tb-active" data-bind="visible: selfDeaf,
+                              click: function () { requestUndeaf(thisUser()) }"
+                      rel="undeaf" src="/svg/audio-output-deafened.svg">
+        <img class="tb-record" data-bind="click: function(){}"
+                      rel="record" src="/svg/media-record.svg">
+        <div class="divider"></div>
+        <img class="tb-comment" data-bind="click: commentDialog.show"
+                      rel="comment" src="/svg/toolbar-comment.svg">
+        <div class="divider"></div>
+        <img class="tb-settings" data-bind="click: openSettings"
+                      rel="settings" src="/svg/config_basic.svg">
+        <div class="divider"></div>
+        <img class="tb-sourcecode" data-bind="click: openSourceCode"
+                      rel="Source Code" src="/svg/source-code.svg">
+      </div>
+      <div class="chat">
+        <script type="text/html" id="log-generic">
+          <span data-bind="text: value"></span>
+        </script>
+        <script type="text/html" id="log-welcome-message">
+          Welcome message: <span data-bind="html: message"></span>
+        </script>
+        <script type="text/html" id="log-chat-message">
+          <span data-bind="visible: channel">
+            (Channel)
+          </span>
+          <span data-bind="template: { name: 'user-tag', data: user }"></span>:
+          <span class="message-content" data-bind="html: message"></span>
+        </script>
+        <script type="text/html" id="log-chat-message-self">
+          To
+          <span data-bind="template: { if: $data.channel, name: 'channel-tag', data: $data.channel }">
+          </span><span data-bind="template: { if: $data.user, name: 'user-tag', data: $data.user }">
+          </span>:
+          <span class="message-content" data-bind="html: message"></span>
+        </script>
+        <script type="text/html" id="log-disconnect">
+        </script>
+        <div class="log" data-bind="foreach: {
+          data: log,
+          afterRender: function (e) {
+            [].forEach.call(e[1].getElementsByTagName('a'), function(e){e.target = '_blank'})
+          }
+        }">
+          <div class="log-entry">
+            <span class="log-timestamp" data-bind="text: $root.getTimeString()"></span>
+            <!-- ko template: { data: $data, name: function(l) { return 'log-' + l.type; } } -->
+            <!-- /ko -->
+          </div>
+        </div>
+        <form data-bind="submit: submitMessageBox">
+          <input id="message-box" type="text" data-bind="
+              attr: { placeholder: messageBoxHint }, textInput: messageBox">
+        </form>
+      </div>
+      <script type="text/html" id="channel">
+        <div class="channel" data-bind="
+            click: $root.select, 
+            event: {
+              contextmenu: openContextMenu,
+              dblclick: $root.requestMove.bind($root, $root.thisUser())
+            },
+            css: {
+              selected: $root.selected() === $data,
+              currentChannel: users.indexOf($root.thisUser()) !== -1
+            }">
+          <div class="channel-status">
+            <img class="channel-description" data-bind="visible: description"
+                alt="description" src="/svg/comment.svg">
+          </div>
+          <div data-bind="if: description">
+            <div class="channel-description tooltip" data-bind="html: description"></div>
+          </div>
+          <img class="channel-icon" src="/svg/channel.svg"
+            data-bind="visible: !linked() && $root.thisUser().channel() !== $data">
+          <img class="channel-icon-active" src="/svg/channel_active.svg"
+            data-bind="visible: $root.thisUser().channel() === $data">
+          <img class="channel-icon-linked" src="/svg/channel_linked.svg"
+            data-bind="visible: linked() && $root.thisUser().channel() !== $data">
+          <div class="channel-name">
+            <span data-bind="text: name"></span>
+            <!-- ko if: $root.settings.userCountInChannelName() && userCount() !== 0 -->
+            &nbsp;(<span data-bind="text: userCount()"></span>)
+            <!-- /ko -->
+          </div>
+        </div>
+        <!-- ko if: expanded -->
+        <!-- ko foreach: users -->
+        <div class="user-wrapper">
+          <div class="user-tree"></div>
+          <div class="user" data-bind="
+                click: $root.select,
+                event: {
+                  contextmenu: openContextMenu
+                },
+                css: {
+                  thisClient: $root.thisUser() === $data,
+                  selected: $root.selected() === $data
+                }">
+            <div class="user-status" data-bind="attr: { title: state }">
+              <img class="user-comment" data-bind="visible: comment"
+                  alt="comment" src="/svg/comment.svg">
+              <img class="user-server-mute" data-bind="visible: mute"
+                  alt="server mute" src="/svg/muted_server.svg">
+              <img class="user-suppress-mute" data-bind="visible: suppress"
+                  alt="suppressed" src="/svg/muted_suppressed.svg">
+              <img class="user-self-mute" data-bind="visible: selfMute" 
+                  alt="self mute" src="/svg/muted_self.svg">
+              <img class="user-server-deaf" data-bind="visible: deaf"
+                  alt="server deaf" src="/svg/deafened_server.svg">
+              <img class="user-self-deaf" data-bind="visible: selfDeaf"
+                  alt="self deaf" src="/svg/deafened_self.svg">
+              <img class="user-authenticated" data-bind="visible: uid"
+                  alt="authenticated" src="/svg/authenticated.svg">
+            </div>
+            <div data-bind="if: comment">
+              <div class="user-comment tooltip" data-bind="html: comment"></div>
+            </div>
+            <!-- ko if: show_avatar() -->
+            <img class="user-avatar" alt="avatar"
+                 data-bind="attr: { src: texture },
+                            css: { 'user-avatar-talk-off': talking() == 'off',
+                                   'user-avatar-talk-on': talking() == 'on',
+                                   'user-avatar-talk-whisper': talking() == 'whisper',
+                                   'user-avatar-talk-shout': talking() == 'shout' }">
+            <!-- /ko -->
+            <!-- ko ifnot: show_avatar() -->
+            <img class="user-talk user-talk-off" data-bind="visible: talking() == 'off'"
+                alt="talk off" src="/svg/talking_off.svg">
+            <img class="user-talk user-talk-on" data-bind="visible: talking() == 'on'"
+                alt="talk on" src="/svg/talking_on.svg">
+            <img class="user-talk user-talk-whisper" data-bind="visible: talking() == 'whisper'"
+                alt="whisper" src="/svg/talking_whisper.svg">
+            <img class="user-talk user-talk-shout" data-bind="visible: talking() == 'shout'"
+                alt="shout" src="/svg/talking_alt.svg">
+            <!-- /ko -->
+            <div class="user-name" data-bind="text: name"></div>
+          </div>
+        </div>
+        <!-- /ko -->
+        <!-- ko foreach: channels -->
+        <div class="channel-wrapper">
+          <!-- ko ifnot: users().length || channels().length -->
+          <div class="channel-tree"></div>
+          <!-- /ko -->
+          <div class="branch" data-bind="if: users().length || channels().length">
+            <img class="branch-open" src="/svg/branch_open.svg"
+                data-bind="click: expanded.bind($data, false), visible: expanded()">
+            <img class="branch-closed" src="/svg/branch_closed.svg"
+                data-bind="click: expanded.bind($data, true), visible: !expanded()">
+          </div>
+          <div class="channel-sub" data-bind="template: {name: 'channel', data: $data}"></div>
+        </div>
+        <!-- /ko -->
+        <!-- /ko -->
+      </script>
+      <div class="channel-root-container" data-bind="if: root, visible: !minimalView()">
+        <div class="channel-root" data-bind="template: {name: 'channel', data: root}"></div>
+      </div>
+      <div class="channel-root-container" data-bind="if: thisUser, visible: minimalView()">
+        <div class="channel-root" data-bind="template: {name: 'channel', data: thisUser().channel}"></div>
+      </div>
+    </div>
+  </body>
+  <script src="index.js"></script>
+</html>
diff --git a/pica-murmur/.dockerignore b/pica-murmur/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..8d3188d51af5570cae953804a4db830dc67f7799
--- /dev/null
+++ b/pica-murmur/.dockerignore
@@ -0,0 +1,3 @@
+README.md
+docker-compose.yml
+secrets/
diff --git a/pica-murmur/Dockerfile b/pica-murmur/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..0cfd9bd253034b1d9c1f146e33d336c03f95dc6f
--- /dev/null
+++ b/pica-murmur/Dockerfile
@@ -0,0 +1,28 @@
+FROM debian:buster-slim
+
+# Install Mumble server
+RUN apt-get update -y \
+    && apt-get dist-upgrade -y \
+    && apt-get install -y \
+      libssl-dev \
+      libbz2-dev \
+      mumble-server=1.3.0~git20190125.440b173+dfsg-2 \
+      pcproc \
+      python3 \
+      python3-pip \
+      zeroc-ice-slice \
+    && rm -rf /var/lib/apt/lists/*
+
+# Install prometheus exporter and Python dependencies
+COPY requirements.txt /
+RUN pip3 install --no-cache-dir -r requirements.txt
+COPY exporter.py /
+
+EXPOSE 64738/tcp 64738/udp 8000/tcp
+
+# Volume for persistent storage (config file and database)
+VOLUME ["/data"]
+
+# Add entrypoint
+COPY entrypoint.sh /
+ENTRYPOINT ["/entrypoint.sh"]
diff --git a/pica-murmur/README.md b/pica-murmur/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..94e274429f16a12b15ccbdbde70fe1b1d100d00e
--- /dev/null
+++ b/pica-murmur/README.md
@@ -0,0 +1,15 @@
+# Murmur
+
+Docker image for a simple Mumble server (Murmur)
+
+## Configuration
+Some environment variables allow to configure Murmur at startup :
+- `MAX_BANDWIDTH` : integer, maximum bandwidth clients are allowed speech at (default is `128000` bps)
+- `MAX_USERS` : integer, maximum number of users on the server (default is `100`)
+- `METRICS_SERVER_LABEL` : Name of this Murmur server to add as metrics label
+
+## Mounted volumes
+Murmur store its server database and configuration files under `/data/` folder. You should mount this directory on your host.
+
+## Network
+Murmur is listening both TCP and UDP on port 64738. You should bind this container port to your host.
diff --git a/pica-murmur/clair-whitelist.yml b/pica-murmur/clair-whitelist.yml
new file mode 100644
index 0000000000000000000000000000000000000000..82e705c5a9dda07792d93b2b24af4eb1e201b693
--- /dev/null
+++ b/pica-murmur/clair-whitelist.yml
@@ -0,0 +1,12 @@
+generalwhitelist:
+  CVE-2019-19816: On utilise pas btrfs
+  CVE-2019-19814: On utilise pas f2fs
+  CVE-2019-19074: ¯\_(ツ)_/¯
+  CVE-2020-10543: ¯\_(ツ)_/¯
+  CVE-2020-10878: ¯\_(ツ)_/¯
+  CVE-2019-19813: On utilise pas btrfs
+  CVE-2019-19815: On utilise pas f2fs
+  CVE-2020-8492: ¯\_(ツ)_/¯
+  CVE-2013-7445: ¯\_(ツ)_/¯
+  CVE-2020-13974: DISPUTED
+  CVE-2020-14155: ¯\_(ツ)_/¯
diff --git a/pica-murmur/docker-compose.yml b/pica-murmur/docker-compose.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b05c0c11dccb70e3fc805157d8de3cca311dbd5e
--- /dev/null
+++ b/pica-murmur/docker-compose.yml
@@ -0,0 +1,33 @@
+version: "2.4"
+networks:
+  docker_default:
+    external: true
+    name: "docker_default"
+
+volumes:
+  murmur-data:
+    name: murmur-data
+
+services:
+  murmur:
+    image: registry.picasoft.net/pica-murmur:1.3.0
+    container_name: murmur
+    environment:
+      MAX_USERS: 2000
+      METRICS_SERVER_LABEL: voice.picasoft.net
+    ports:
+      - "64738:64738"
+      - "64738:64738/udp"
+    volumes:
+      - murmur-data:/data
+      - /DATA/docker/certs/voice.picasoft.net/:/certs
+    networks:
+      - docker_default
+    labels:
+      - "traefik.enable=true"
+      - "traefik.port=8000"
+      - "traefik.frontend.rule=Host:voice.picasoft.net;Path:/metrics"
+      - "tls-certs-monitor.enable=true"
+      - "tls-certs-monitor.action=kill:SIGUSR1"
+      - "tls-certs-monitor.owner=103"
+    restart: unless-stopped
diff --git a/pica-murmur/entrypoint.sh b/pica-murmur/entrypoint.sh
new file mode 100755
index 0000000000000000000000000000000000000000..3b28d616911f3a78a3207181db55bcb0034164e1
--- /dev/null
+++ b/pica-murmur/entrypoint.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+
+CONFIG_FILE="/data/mumble-server.ini"
+MAX_BANDWIDTH=${MAX_BANDWIDTH:-128000}
+MAX_USERS=${MAX_USERS:-100}
+
+# Copy configuration file if not exists
+if [ ! -f $CONFIG_FILE ]
+then
+  cp /etc/mumble-server.ini $CONFIG_FILE
+fi
+
+# Change configuration
+sed -i -E "s/^(\/\/ )?database( )?=.*/database=\/data\/murmur.sqlite/g" $CONFIG_FILE
+sed -i -E "s/^(\/\/ )?logfile( )?=.*/logfile=/g" $CONFIG_FILE
+sed -i -E "s/^(\/\/ )?bandwidth( )?=.*/bandwidth=$MAX_BANDWIDTH/g" $CONFIG_FILE
+sed -i -E "s/^(\/\/ )?users( )?=.*/users=$MAX_USERS/g" $CONFIG_FILE
+
+# Set correct rights on murmur files
+chown -R mumble-server:mumble-server /data
+
+# Run exporter if variable set
+if [ -n "$METRICS_SERVER_LABEL" ]
+then
+  python3 /exporter.py &
+fi
+
+# Trap SIGUSR1 signal to reload certificates without restarting
+_reload() {
+  echo "Caught SIGUSR1 signal!"
+  /usr/bin/pkill -USR1 murmurd
+}
+trap _term SIGUSR1
+
+# Run murmur
+murmurd -fg -v -ini $CONFIG_FILE
diff --git a/pica-murmur/exporter.py b/pica-murmur/exporter.py
new file mode 100644
index 0000000000000000000000000000000000000000..0c806b73c143f13ba8e0e000b80c65d030e5b569
--- /dev/null
+++ b/pica-murmur/exporter.py
@@ -0,0 +1,151 @@
+# This script is adapted from another script developped by
+# Stefan Hacker <dd0t@users.sourceforge.net> : https://github.com/Natenom/munin-plugins/tree/master/murmur
+
+# Imports
+import tempfile
+import os
+import sys
+import signal
+import time
+import IcePy
+import Ice
+from prometheus_client import start_http_server, Counter, Gauge, REGISTRY, PROCESS_COLLECTOR, PLATFORM_COLLECTOR
+
+# Path to Murmur.ice; the script tries first to retrieve this file dynamically from Murmur itself; if this fails it tries this file.
+SLICE_FILE = "/usr/share/slice/Murmur.ice"
+# Ice.MessageSizeMax from Murmur server
+ICE_MESSAGE_SIZE_MAX = "65535"
+# ICE connection variable
+ICE_HOST = "127.0.0.1"
+ICE_PORT = 6502
+PROXY_STRING = "Meta -e 1.0:tcp -h %s -p %d -t 1000" % (ICE_HOST, ICE_PORT)
+
+# Number of seconds between 2 metrics collection
+COLLECT_INTERVAL = os.getenv('EXPORTER_COLLECT_INTERVAL', 10)
+
+# Get server name
+METRICS_SERVER_LABEL = os.getenv('METRICS_SERVER_LABEL')
+if not METRICS_SERVER_LABEL:
+    print('Must define METRICS_SERVER_LABEL environment variable !')
+    sys.exit(1)
+
+# Prepare ICE properties
+ice_props = Ice.createProperties()
+ice_props.setProperty("Ice.ImplicitContext", "Shared")
+ice_props.setProperty("Ice.MessageSizeMax", str(ICE_MESSAGE_SIZE_MAX))
+# Initialize ICE
+idata = Ice.InitializationData()
+idata.properties = ice_props
+ice = Ice.initialize(idata)
+
+###########################################################################################
+###### This part is (almost) an entire copy of the original script to connect to ICE ######
+####################### To be clear : I HAVE NO IDEA WHAT I'M DOING #######################
+###########################################################################################
+
+connection_done = False
+while not connection_done:
+    try:
+        ice_proxy = ice.stringToProxy(PROXY_STRING)
+
+        # Get slice directory
+        slice_dir = Ice.getSliceDir()
+        slice_dir = ['-I' + slice_dir]
+
+        try:
+            op = IcePy.Operation('getSlice', Ice.OperationMode.Idempotent, Ice.OperationMode.Idempotent,
+                                 True, None, (), (), (), ((), IcePy._t_string, False, 0), ())
+
+            slice = op.invoke(ice_proxy, ((), None))
+            (dynslicefiledesc, dynslicefilepath) = tempfile.mkstemp(suffix='.ice')
+            dynslicefile = os.fdopen(dynslicefiledesc, 'w')
+            dynslicefile.write(slice)
+            dynslicefile.flush()
+            Ice.loadSlice('', slice_dir + [dynslicefilepath])
+            dynslicefile.close()
+            os.remove(dynslicefilepath)
+        except Exception as e:
+            try:
+                Ice.loadSlice('', slice_dir + [SLICE_FILE])
+            except:
+                raise Ice.ConnectionRefusedException
+
+        import Murmur
+
+        # Check connection is working
+        Murmur.MetaPrx.checkedCast(ice_proxy)
+        connection_done = True
+
+    except Ice.ConnectionRefusedException:
+        print('Cannot connect exporter to ICE, retry in 5 seconds')
+        time.sleep(5)
+
+###########################################################################################
+######################## End of the "NO IDEA WHAT I'M DOING PART" #########################
+###########################################################################################
+
+# Remove unwanted Prometheus metrics
+[REGISTRY.unregister(c) for c in [PROCESS_COLLECTOR, PLATFORM_COLLECTOR,
+                                  REGISTRY._names_to_collectors['python_gc_objects_collected_total']]]
+
+# Start Prometheus exporter server
+start_http_server(8000)
+
+# Register metrics
+users_all_gauge = Gauge('murmur_online_users_all', 'Number of online users', ['server'])
+users_unregistered_gauge = Gauge('murmur_online_users_unregistered',
+                                 'Number of online unregistered users', ['server'])
+users_registered_gauge = Gauge('murmur_online_users_registered', 'Number of online registered users', ['server'])
+users_muted_gauge = Gauge('murmur_online_users_muted', 'Number of online muted users', ['server'])
+users_banned_gauge = Gauge('murmur_users_banned', 'Number of banned users', ['server'])
+chan_count_gauge = Gauge('murmur_channels', 'Number of channels', ['server'])
+uptime_gauge = Gauge('murmur_uptime', 'Number of seconds the server is uptime', ['server'])
+
+
+def exit_handler(sig, frame):
+    # Define handler for stop signals
+    print('Terminating...')
+    ice.destroy()
+    sys.exit(0)
+
+
+# Catch several signals
+signal.signal(signal.SIGINT, exit_handler)
+signal.signal(signal.SIGTERM, exit_handler)
+
+
+# Loop forever
+while True:
+    # Get data from Murmur server
+    meta = Murmur.MetaPrx.checkedCast(ice_proxy)
+    server = meta.getServer(1)
+
+    # Initialize metrics counters
+    users_muted_count = 0
+    users_unregistered_count = 0
+    users_registered_count = 0
+
+    # Collect and count users
+    onlineusers = server.getUsers()
+    for key in onlineusers.keys():
+        # Count user as registered
+        if onlineusers[key].userid == -1:
+            users_unregistered_count += 1
+        # Count user as not registered
+        if onlineusers[key].userid > 0:
+            users_registered_count += 1
+        # Count muted users
+        if onlineusers[key].mute or onlineusers[key].selfMute or onlineusers[key].suppress:
+            users_muted_count += 1
+
+    # Set metrics
+    users_all_gauge.labels(server=METRICS_SERVER_LABEL).set(len(onlineusers))
+    users_muted_gauge.labels(server=METRICS_SERVER_LABEL).set(users_muted_count)
+    users_unregistered_gauge.labels(server=METRICS_SERVER_LABEL).set(users_unregistered_count)
+    users_registered_gauge.labels(server=METRICS_SERVER_LABEL).set(users_registered_count)
+    users_banned_gauge.labels(server=METRICS_SERVER_LABEL).set(len(server.getBans()))
+    chan_count_gauge.labels(server=METRICS_SERVER_LABEL).set(len(server.getChannels()))
+    uptime_gauge.labels(server=METRICS_SERVER_LABEL).set(meta.getUptime())
+
+    # Wait beforce next metrics collection
+    time.sleep(COLLECT_INTERVAL)
diff --git a/pica-murmur/requirements.txt b/pica-murmur/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..5e01e40a21dddfc329f183c6d662849f8004482c
--- /dev/null
+++ b/pica-murmur/requirements.txt
@@ -0,0 +1,3 @@
+zeroc-ice==3.7.3 
+prometheus_client==0.7.1
+
diff --git a/pica-tls-certs-monitor/docker-compose.yml b/pica-tls-certs-monitor/docker-compose.yml
index 5fbdd3cea8c48f3d6ead228ec27c866866baf49e..63f9ed8fe32f25fb20a53b41079d6e6337d9aa8b 100644
--- a/pica-tls-certs-monitor/docker-compose.yml
+++ b/pica-tls-certs-monitor/docker-compose.yml
@@ -2,7 +2,7 @@ version: '3.7'
 
 services:
   tls-certs-monitor:
-    image: registry.picasoft.net/pica-tls-certs-monitor:v1.4
+    image: registry.picasoft.net/pica-tls-certs-monitor:v1.5
     container_name: tls-certs-monitor
     volumes:
       - /DATA/docker/traefik/certs/acme.json:/certs/acme.json