diff --git a/panel/src/panel/static/htmx.min.js b/panel/src/panel/static/htmx.min.js
index e4006f42..76d10777 120000
--- a/panel/src/panel/static/htmx.min.js
+++ b/panel/src/panel/static/htmx.min.js
@@ -1 +1 @@
-/home/vg/src/Fediversity/nix/store/mwqqk0qmldzvv4xj9kq2lbah2flhc44z-source/dist/htmx.js
\ No newline at end of file
+/nix/store/mwqqk0qmldzvv4xj9kq2lbah2flhc44z-source/dist/htmx.js
\ No newline at end of file
diff --git a/panel/src/panel/static/style.sass b/panel/src/panel/static/style.sass
index 53e52a36..f860ae38 100644
--- a/panel/src/panel/static/style.sass
+++ b/panel/src/panel/static/style.sass
@@ -3,3 +3,24 @@ body
   margin: 0
   font-family: sans-serif
   box-sizing: border-box
+
+.loader
+  width: 48px
+  height: 48px
+  border: 5px solid #000
+  border-bottom-color: #F34508
+  border-radius: 50%
+  box-sizing: border-box
+  animation: rotation 1s linear infinite
+  display: inline-block
+
+@keyframes rotation
+  0% { transform: rotate(0deg) }
+  100% { transform: rotate(360deg) }
+
+#spinner-container
+  position: absolute
+  top: 50%
+  left: 50%
+  transform: translate(-50%, -50%)
+  display: block
\ No newline at end of file
diff --git a/panel/src/panel/templates/configuration_form.html b/panel/src/panel/templates/configuration_form.html
index 474aa4f6..ffc78e3d 100644
--- a/panel/src/panel/templates/configuration_form.html
+++ b/panel/src/panel/templates/configuration_form.html
@@ -1,11 +1,23 @@
 {% extends "base.html" %}
 {% block content %}
-<form method="post" enctype="multipart/form-data" action="{% url 'configuration_form' %}">
+<form method="post" enctype="multipart/form-data" action="{% url 'save' %}">
   {% csrf_token %}
 
   {{ form.as_p }}
+  <button id="deploy-button" class="button"
+          hx-post="{% url 'deployment_status' %}"
+          hx-trigger="click"
+          hx-indicator="#spinner-container"
+          hx-disabled-elt="this"
+          hx-swap="none"
+          name="deploy">
+      Deploy
+  </button>
 
-  <button class="button" type="submit" name="deploy">Deploy</button>
   <button class="button" type="submit" name="save">Save</button>
+
+  <div id="spinner-container" class="htmx-indicator">
+    <span class="loader"></span>
+  </div>
 </form>
 {% endblock %}
diff --git a/panel/src/panel/tests/test_configuration_form.py b/panel/src/panel/tests/test_configuration_form.py
index a96d8441..805b0e34 100644
--- a/panel/src/panel/tests/test_configuration_form.py
+++ b/panel/src/panel/tests/test_configuration_form.py
@@ -13,7 +13,7 @@ class ConfigurationForm(TestCase):
             password=self.password
         )
 
-        self.config_url = reverse('configuration_form')
+        self.config_url = reverse('save')
 
     def test_configuration_form_submission(self):
         config = Configuration.objects.create(
@@ -36,12 +36,13 @@ class ConfigurationForm(TestCase):
             enable=True,
             mastodon_enable=True,
         )
-
+        print(form_data)
         response = self.client.post(self.config_url, data=form_data)
 
         self.assertEqual(response.status_code, 302)
         config.refresh_from_db()
 
+        print(config.parsed_value)
         self.assertTrue(config.parsed_value.enable)
         self.assertTrue(config.parsed_value.mastodon.enable)
         # this should not have changed
diff --git a/panel/src/panel/urls.py b/panel/src/panel/urls.py
index 2f0ba434..011a2843 100644
--- a/panel/src/panel/urls.py
+++ b/panel/src/panel/urls.py
@@ -26,4 +26,6 @@ urlpatterns = [
     path("account/", views.AccountDetail.as_view(), name='account_detail'),
     path("services/", views.ServiceList.as_view(), name='service_list'),
     path("configuration/", views.ConfigurationForm.as_view(), name='configuration_form'),
+    path("deployment/status/", views.DeploymentStatus.as_view(), name='deployment_status'),
+    path("save/", views.Save.as_view(), name='save'),
 ]
diff --git a/panel/src/panel/views.py b/panel/src/panel/views.py
index 16d29849..6e45df92 100644
--- a/panel/src/panel/views.py
+++ b/panel/src/panel/views.py
@@ -1,6 +1,7 @@
 from enum import Enum
 import json
 import subprocess
+import os
 
 from django.urls import reverse_lazy
 from django.contrib.auth.mixins import LoginRequiredMixin
@@ -9,9 +10,9 @@ from django.views.generic import TemplateView, DetailView
 from django.views.generic.edit import FormView
 
 from panel import models, settings
+from panel import models
 from panel.configuration import forms
 
-
 class Index(TemplateView):
     template_name = 'index.html'
 
@@ -33,51 +34,12 @@ class ConfigurationForm(LoginRequiredMixin, FormView):
     success_url = reverse_lazy('configuration_form')
     form_class = forms.Form
 
-    def get_context_data(self, **kwargs):
-        context = super().get_context_data(**kwargs)
-        return context
-
     def get_object(self):
         """Get or create the configuration object for the current user"""
         obj, created = models.Configuration.objects.get_or_create(
             operator=self.request.user,
         )
 
-        # Check for deploy button
-        if "deploy" in self.request.POST.keys():
-            submission = obj.parsed_value.model_dump_json()
-            # FIXME: let the user specify these from the form (#190)
-            dummy_user = {
-              "initialUser": {
-                "displayName": "Testy McTestface",
-                "username": "test",
-                "password": "testtest",
-                "email": "test@test.com",
-              },
-            }
-            # serialize back and forth now we still need to manually inject the dummy user
-            deployment = json.dumps(dummy_user | json.loads(submission))
-            env = {
-                "PATH": settings.bin_path,
-                # pass in form info to our deployment
-                "DEPLOYMENT": deployment,
-            }
-            cmd = [
-                "nix",
-                "develop",
-                # workaround to pass in info to nixops4 thru env vars, tho impure :(
-                "--extra-experimental-features",
-                "configurable-impure-env",
-                "--command",
-                "nixops4",
-                "apply",
-                "test",
-            ]
-            subprocess.run(
-               cmd,
-               cwd=settings.repo_dir,
-               env=env,
-           )
         return obj
 
     # TODO(@fricklerhandwerk):
@@ -120,9 +82,57 @@ class ConfigurationForm(LoginRequiredMixin, FormView):
         initial.update(self.convert_enums_to_names(config_dict))
         return initial
 
+class Save(ConfigurationForm):
     def form_valid(self, form):
         obj = self.get_object()
         obj.value = form.to_python().model_dump_json()
         obj.save()
 
         return super().form_valid(form)
+
+class DeploymentStatus(ConfigurationForm):
+    def form_valid(self, form):
+        obj = self.get_object()
+        obj.save()
+
+        # Check for deploy button
+        if "deploy" in self.request.POST.keys():
+            self.deployment(obj)
+
+        return super().form_valid(form)
+
+    def deployment(self, obj):
+        submission = obj.parsed_value.model_dump_json()
+        # FIXME: let the user specify these from the form (#190)
+        dummy_user = {
+            "initialUser": {
+            "displayName": "Testy McTestface",
+            "username": "test",
+            "password": "testtest",
+            "email": "test@test.com",
+            },
+        }
+        # serialize back and forth now we still need to manually inject the dummy user
+        deployment = json.dumps(dummy_user | json.loads(submission))
+        env = {
+            "PATH": settings.bin_path,
+            # pass in form info to our deployment
+            "DEPLOYMENT": deployment,
+        }
+        cmd = [
+            "nix",
+            "develop",
+            "--extra-experimental-features",
+            "configurable-impure-env",
+            "--command",
+            "nixops4",
+            "apply",
+            "test",
+        ]
+        deployment_result = subprocess.run(
+            cmd,
+            cwd=settings.repo_dir,
+            env=env,
+        )
+        print(deployment_result)
+        return deployment_result
\ No newline at end of file