diff --git a/Dockerfile b/Dockerfile
index 058fdde5981958046807a814245df96331200649..4ff779eb274fd97e905f623a0cba80a153fae6fb 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,8 +1,17 @@
+FROM golang:1.19 AS build
+
+ADD . /src
+WORKDIR /src
+RUN go build -tags netgo -o server server.go
+
 FROM node:current-bullseye AS assets
 
 ADD . /src
 WORKDIR /src
 RUN npm install && ./node_modules/.bin/webpack
 
-FROM registry.git.autistici.org/ai3/docker/static-content:master
+FROM scratch
 COPY --from=assets /src/assets/ /var/www/
+COPY --from=build /src/server /server
+
+ENTRYPOINT ["/server"]
diff --git a/README.md b/README.md
index 33fd3114b1e55e33c95b4dd3f80a5e8af006a53f..99f9457766b51633b01f40e5dd08b0a44f1648b3 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,13 @@
 Simple admin dashboard for A/I.
 
 Contains links to useful internal systems and account management tools.
+
+The HTTP server serves static files but also proxies requests to some
+backends we're interested in (prometheus, alertmanager). This is
+necessary because we can't do client-side requests to these other
+services on other domains due to browser's CORS policies: in practice,
+CORS prevents the single-sign on request flow (through the sso-server
+and back) to work because redirects are [forbidden by
+CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed).
+
+
diff --git a/server.go b/server.go
new file mode 100644
index 0000000000000000000000000000000000000000..aa2b48f8400e8f63dab552cd57770f3d0c564e15
--- /dev/null
+++ b/server.go
@@ -0,0 +1,84 @@
+package main
+
+import (
+	"context"
+	"flag"
+	"log"
+	"net/http"
+	"net/http/httputil"
+	"net/url"
+	"os"
+	"os/signal"
+	"syscall"
+	"time"
+)
+
+var (
+	addr              = flag.String("addr", getenv("ADDR", ":3000"), "TCP `addr` to listen on")
+	karmaBackend      = flag.String("karma-url", getenv("ALERTS_URL", ""), "`URL` for the Karma backend")
+	prometheusBackend = flag.String("prometheus-url", getenv("PROMETHEUS_URL", ""), "`URL` for the Prometheus backend")
+
+	rootDir = "/var/www"
+
+	alertBackendURL      *url.URL
+	prometheusBackendURL *url.URL
+)
+
+func getenv(k, dflt string) string {
+	if s := os.Getenv(k); s != "" {
+		return s
+	}
+	return dflt
+}
+
+func main() {
+	log.SetFlags(0)
+	flag.Parse()
+
+	var err error
+	alertBackendURL, err = url.Parse(*karmaBackend)
+	if err != nil {
+		log.Fatalf("Error parsing --karma-url: %v", err)
+	}
+	prometheusBackendURL, err = url.Parse(*prometheusBackend)
+	if err != nil {
+		log.Fatalf("Error parsing --prometheus-url: %v", err)
+	}
+
+	// Proxy some specific URLs to certain backends. If we need
+	// more we'll add them.
+	http.Handle("/alerts.json", httputil.NewSingleHostReverseProxy(alertBackendURL))
+	http.Handle("/api/v1/", httputil.NewSingleHostReverseProxy(prometheusBackendURL))
+
+	// Simple static file server for /var/www.
+	http.Handle("/", http.FileServer(http.Dir(rootDir)))
+
+	srv := &http.Server{
+		Addr:         *addr,
+		ReadTimeout:  30 * time.Second,
+		WriteTimeout: 30 * time.Second,
+		IdleTimeout:  600 * time.Second,
+	}
+
+	sigCh := make(chan os.Signal, 1)
+	go func() {
+		<-sigCh
+		// Gracefully terminate for 3 seconds max, then shut
+		// down remaining clients.
+		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+		defer cancel()
+		err := srv.Shutdown(ctx)
+		if err == context.Canceled {
+			err = srv.Close()
+		}
+		if err != nil {
+			log.Printf("error terminating server: %v", err)
+		}
+	}()
+	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
+
+	log.Printf("starting static server on %s", *addr)
+	if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+		log.Fatalf("error: %v", err)
+	}
+}
diff --git a/src/dashboard.js b/src/dashboard.js
index 5d99f87312f7dd784aa1a53cb251d5256c03365a..8ee5a286c64e1b0efc99774015708c05665c3aa9 100644
--- a/src/dashboard.js
+++ b/src/dashboard.js
@@ -1,14 +1,14 @@
 import { PrometheusDriver } from 'prometheus-query';
 
 const prom = new PrometheusDriver({
-    endpoint: "https://monitor.autistici.org",
+    endpoint: "https://admin.autistici.org",
     baseURL: "/api/v1",
     withCredentials: true
 });
 
 const accountsQuery = "sum(max(accounts_count) by (shard,type,status)) by (type)";
 
-const alertsURL = "https://alerts.autistici.org/alerts.json";
+const alertsURL = "https://admin.autistici.org/alerts.json";
 
 const alertsQuery = {
     filters: ["@state=active", "severity=page"],