diff --git a/.gitea/workflows/cleanup-branch.yaml b/.gitea/workflows/cleanup-branch.yaml
new file mode 100644
index 0000000..fee5035
--- /dev/null
+++ b/.gitea/workflows/cleanup-branch.yaml
@@ -0,0 +1,21 @@
+name: Clean up branch.dsv.su.se
+on:
+  pull_request:
+    types:
+      - closed
+jobs:
+  Cleanup-branch:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Generate mangled vhost name
+        id: mangled-name
+        run: |
+          MANGLED_BRANCH_NAME="$(echo ${{ gitea.headref}} | sed -r -e 's%/%-%g' -e 's/[^0-9a-z\-]//g')"
+          echo $MANGLED_BRANCH_NAME
+          echo "MANGLED_NAME=$MANGLED_BRANCH_NAME" >> "$GITHUB_OUTPUT"
+      - name: Set up SSH key
+        run: |
+          echo "${{ secrets.BRANCH_CLEANUP_KEY }}" >> ssh_key
+          chmod 0600 ssh_key
+      - name: Execute clean up script
+        run: echo "${{ gitea.serverurl }}/${{ gitea.repository }}.git ${{ gitea.headref }} ${{ steps.mangled-name.outputs.MANGLED_NAME }}" | ssh -o StrictHostKeyChecking=accept-new -i ssh_key branch.dsv.su.se
diff --git a/.gitea/workflows/deploy-branch.yaml b/.gitea/workflows/deploy-branch.yaml
new file mode 100644
index 0000000..00ec299
--- /dev/null
+++ b/.gitea/workflows/deploy-branch.yaml
@@ -0,0 +1,37 @@
+name: Deploy to branch.dsv.su.se
+on:
+  pull_request:
+    types:
+      - opened
+      - reopened
+      - ready_for_review
+jobs:
+  Deploy-branch:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Generate mangled vhost name
+        id: mangled-name
+        run: |
+          MANGLED_BRANCH_NAME="$(echo ${{ gitea.headref}} | sed -r -e 's%/%-%g' -e 's/[^0-9a-z\-]//g')"
+          echo $MANGLED_BRANCH_NAME
+          echo "MANGLED_NAME=$MANGLED_BRANCH_NAME" >> "$GITHUB_OUTPUT"
+      - name: Set up SSH key
+        run: |
+          echo "${{ secrets.BRANCH_DEPLOY_KEY }}" >> ssh_key
+          chmod 0600 ssh_key
+      - name: Execute deploy script
+        run: echo "${{ gitea.serverurl }}/${{ gitea.repository }}.git ${{ gitea.headref }} ${{ steps.mangled-name.outputs.MANGLED_NAME }}" | ssh -o StrictHostKeyChecking=accept-new -i ssh_key branch.dsv.su.se
+      - name: Post URL to deployment as comment
+        uses: actions/github-script@v7
+        env:
+          MANGLED_BRANCH_NAME: ${{ steps.mangled-name.outputs.MANGLED_NAME }}
+        with:
+          script: |
+            const repositoryName = context.repo.repo;
+            const mangledBranchName = process.env.MANGLED_BRANCH_NAME;
+            github.rest.issues.createComment({
+              issue_number: context.issue.number,
+              owner: context.repo.owner,
+              repo: context.repo.repo,
+              body: `https://${repositoryName}-${mangledBranchName}.branch.dsv.su.se`
+            })
diff --git a/Dockerfile b/Dockerfile
index 21df69f..54eef71 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -17,3 +17,5 @@ FROM tomcat:10-jdk21-openjdk-bookworm as run
 LABEL authors="thuning"
 
 COPY --from=build /build/target/*.war /usr/local/tomcat/webapps/ROOT.war
+
+EXPOSE 8080
diff --git a/docker-compose-branch.yml b/docker-compose-branch.yml
new file mode 100644
index 0000000..6ecc964
--- /dev/null
+++ b/docker-compose-branch.yml
@@ -0,0 +1,57 @@
+services:
+  whisper-api:
+    build:
+      context: .
+    environment:
+      - DBHOST=jdbc:mariadb://whisper-api-db:3306/whisper_api
+      - DBUSER=root
+      - DBPASS=mariadb
+      - OAUTH2_CLIENT_ID=whisper-frontend
+      - OAUTH2_CLIENT_SECRET=s3cr3t
+      - OAUTH2_AUTH_URI=https://oauth2-${VHOST}/authorize
+      - OAUTH2_TOKEN_URI=https://oauth2-${VHOST}/exchange
+      - OAUTH2_USER_INFO_URI=https://oauth2-${VHOST}/verify
+    depends_on:
+      - whisper-api-db
+      - whisper-api-oauth2
+    networks:
+      - whisper-network
+      - traefik
+    labels:
+      - "traefik.enable=true"
+      - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.rule=Host(`${VHOST}`)"
+      - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.entrypoints=secure"
+      - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.tls.certresolver=letsencrypt"
+
+  whisper-api-db:
+    image: mariadb
+    restart: on-failure
+    networks:
+      - whisper-network
+    environment:
+      - MARIADB_ROOT_PASSWORD=mariadb
+      - MARIADB_DATABASE=whisper_api
+      - MYSQL_ROOT_HOST=%
+
+  whisper-api-oauth2:
+    build:
+      context: https://github.com/dsv-su/toker.git
+      dockerfile: embedded.Dockerfile
+    restart: on-failure
+    networks:
+      - traefik
+    environment:
+      CLIENT_ID: whisper-frontend
+      CLIENT_SECRET: s3cr3t
+      CLIENT_REDIRECT_URI: https://${VHOST}/login/oauth2/code/su
+    labels:
+      - "traefik.enable=true"
+      - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-oauth2.rule=Host(`oauth2-${VHOST}`)"
+      - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-oauth2.entrypoints=secure"
+      - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-oauth2.tls.certresolver=letsencrypt"
+
+networks:
+  whisper-network:
+  traefik:
+    name: traefik
+    external: true
diff --git a/src/main/java/se/su/dsv/whisperapi/WhisperApiApplication.java b/src/main/java/se/su/dsv/whisperapi/WhisperApiApplication.java
index 0e652dd..078b689 100644
--- a/src/main/java/se/su/dsv/whisperapi/WhisperApiApplication.java
+++ b/src/main/java/se/su/dsv/whisperapi/WhisperApiApplication.java
@@ -2,10 +2,13 @@ package se.su.dsv.whisperapi;
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
 import org.springframework.context.annotation.Bean;
+import org.springframework.core.Ordered;
 import org.springframework.security.config.Customizer;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.web.filter.ForwardedHeaderFilter;
 
 @SpringBootApplication
 public class WhisperApiApplication {
@@ -21,4 +24,11 @@ public class WhisperApiApplication {
                 .oauth2Login(Customizer.withDefaults());
         return http.build();
     }
+
+    @Bean
+    public FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter() {
+        var filterRegistrationBean = new FilterRegistrationBean<>(new ForwardedHeaderFilter());
+        filterRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
+        return filterRegistrationBean;
+    }
 }