aboutsummaryrefslogtreecommitdiff
path: root/build
diff options
context:
space:
mode:
authorNeil Alexander <neilalexander@users.noreply.github.com>2022-11-02 14:04:08 +0000
committerGitHub <noreply@github.com>2022-11-02 14:04:08 +0000
commitca8bc873801c77f67378e542686d19ed388bba53 (patch)
tree1ab8f7f19362217d18b4020e9799eba0dc81ffef /build
parent51ab0a8ccfab539e127df0d97c29f364fbb57864 (diff)
Multi-stage Docker builds (#2850)
This builds on @S7evinK's work to make multi-stage Docker builds. Now that we can build SQLite without Cgo this should be much simpler and should make Docker builds in CI significantly faster. Co-authored-by: Till Faelligen <tfaelligen@gmail.com> Co-authored-by: Till Faelligen <davidf@element.io> Co-authored-by: Till Faelligen <2353100+S7evinK@users.noreply.github.com>
Diffstat (limited to 'build')
-rw-r--r--build/dendritejs-pinecone/jsServer.go101
-rw-r--r--build/dendritejs-pinecone/main.go234
-rw-r--r--build/dendritejs-pinecone/main_noop.go24
-rw-r--r--build/dendritejs-pinecone/main_test.go26
-rw-r--r--build/docker/Dockerfile.demo-pinecone25
-rw-r--r--build/docker/Dockerfile.demo-yggdrasil25
-rw-r--r--build/docker/Dockerfile.monolith25
-rw-r--r--build/docker/Dockerfile.polylith25
-rw-r--r--build/docker/README.md15
-rwxr-xr-xbuild/docker/images-build.sh6
10 files changed, 399 insertions, 107 deletions
diff --git a/build/dendritejs-pinecone/jsServer.go b/build/dendritejs-pinecone/jsServer.go
new file mode 100644
index 00000000..4298c2ae
--- /dev/null
+++ b/build/dendritejs-pinecone/jsServer.go
@@ -0,0 +1,101 @@
+// Copyright 2020 The Matrix.org Foundation C.I.C.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build wasm
+// +build wasm
+
+package main
+
+import (
+ "bufio"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "syscall/js"
+)
+
+// JSServer exposes an HTTP-like server interface which allows JS to 'send' requests to it.
+type JSServer struct {
+ // The router which will service requests
+ Mux http.Handler
+}
+
+// OnRequestFromJS is the function that JS will invoke when there is a new request.
+// The JS function signature is:
+// function(reqString: string): Promise<{result: string, error: string}>
+// Usage is like:
+// const res = await global._go_js_server.fetch(reqString);
+// if (res.error) {
+// // handle error: this is a 'network' error, not a non-2xx error.
+// }
+// const rawHttpResponse = res.result;
+func (h *JSServer) OnRequestFromJS(this js.Value, args []js.Value) interface{} {
+ // we HAVE to spawn a new goroutine and return immediately or else Go will deadlock
+ // if this request blocks at all e.g for /sync calls
+ httpStr := args[0].String()
+ promise := js.Global().Get("Promise").New(js.FuncOf(func(pthis js.Value, pargs []js.Value) interface{} {
+ // The initial callback code for new Promise() is also called on the critical path, which is why
+ // we need to put this in an immediately invoked goroutine.
+ go func() {
+ resolve := pargs[0]
+ resStr, err := h.handle(httpStr)
+ errStr := ""
+ if err != nil {
+ errStr = err.Error()
+ }
+ resolve.Invoke(map[string]interface{}{
+ "result": resStr,
+ "error": errStr,
+ })
+ }()
+ return nil
+ }))
+ return promise
+}
+
+// handle invokes the http.ServeMux for this request and returns the raw HTTP response.
+func (h *JSServer) handle(httpStr string) (resStr string, err error) {
+ req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(httpStr)))
+ if err != nil {
+ return
+ }
+ w := httptest.NewRecorder()
+
+ h.Mux.ServeHTTP(w, req)
+
+ res := w.Result()
+ var resBuffer strings.Builder
+ err = res.Write(&resBuffer)
+ return resBuffer.String(), err
+}
+
+// ListenAndServe registers a variable in JS-land with the given namespace. This variable is
+// a function which JS-land can call to 'send' HTTP requests. The function is attached to
+// a global object called "_go_js_server". See OnRequestFromJS for more info.
+func (h *JSServer) ListenAndServe(namespace string) {
+ globalName := "_go_js_server"
+ // register a hook in JS-land for it to invoke stuff
+ server := js.Global().Get(globalName)
+ if !server.Truthy() {
+ server = js.Global().Get("Object").New()
+ js.Global().Set(globalName, server)
+ }
+
+ server.Set(namespace, js.FuncOf(h.OnRequestFromJS))
+
+ fmt.Printf("Listening for requests from JS on function %s.%s\n", globalName, namespace)
+ // Block forever to mimic http.ListenAndServe
+ select {}
+}
diff --git a/build/dendritejs-pinecone/main.go b/build/dendritejs-pinecone/main.go
new file mode 100644
index 00000000..e070173a
--- /dev/null
+++ b/build/dendritejs-pinecone/main.go
@@ -0,0 +1,234 @@
+// Copyright 2020 The Matrix.org Foundation C.I.C.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build wasm
+// +build wasm
+
+package main
+
+import (
+ "crypto/ed25519"
+ "encoding/hex"
+ "fmt"
+ "syscall/js"
+
+ "github.com/gorilla/mux"
+ "github.com/matrix-org/dendrite/appservice"
+ "github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/conn"
+ "github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/rooms"
+ "github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
+ "github.com/matrix-org/dendrite/federationapi"
+ "github.com/matrix-org/dendrite/internal/httputil"
+ "github.com/matrix-org/dendrite/keyserver"
+ "github.com/matrix-org/dendrite/roomserver"
+ "github.com/matrix-org/dendrite/setup"
+ "github.com/matrix-org/dendrite/setup/base"
+ "github.com/matrix-org/dendrite/setup/config"
+ "github.com/matrix-org/dendrite/userapi"
+
+ "github.com/matrix-org/gomatrixserverlib"
+
+ "github.com/sirupsen/logrus"
+
+ _ "github.com/matrix-org/go-sqlite3-js"
+
+ pineconeConnections "github.com/matrix-org/pinecone/connections"
+ pineconeRouter "github.com/matrix-org/pinecone/router"
+ pineconeSessions "github.com/matrix-org/pinecone/sessions"
+)
+
+var GitCommit string
+
+func init() {
+ fmt.Printf("[%s] dendrite.js starting...\n", GitCommit)
+}
+
+const publicPeer = "wss://pinecone.matrix.org/public"
+const keyNameEd25519 = "_go_ed25519_key"
+
+func readKeyFromLocalStorage() (key ed25519.PrivateKey, err error) {
+ localforage := js.Global().Get("localforage")
+ if !localforage.Truthy() {
+ err = fmt.Errorf("readKeyFromLocalStorage: no localforage")
+ return
+ }
+ // https://localforage.github.io/localForage/
+ item, ok := await(localforage.Call("getItem", keyNameEd25519))
+ if !ok || !item.Truthy() {
+ err = fmt.Errorf("readKeyFromLocalStorage: no key in localforage")
+ return
+ }
+ fmt.Println("Found key in localforage")
+ // extract []byte and make an ed25519 key
+ seed := make([]byte, 32, 32)
+ js.CopyBytesToGo(seed, item)
+
+ return ed25519.NewKeyFromSeed(seed), nil
+}
+
+func writeKeyToLocalStorage(key ed25519.PrivateKey) error {
+ localforage := js.Global().Get("localforage")
+ if !localforage.Truthy() {
+ return fmt.Errorf("writeKeyToLocalStorage: no localforage")
+ }
+
+ // make a Uint8Array from the key's seed
+ seed := key.Seed()
+ jsSeed := js.Global().Get("Uint8Array").New(len(seed))
+ js.CopyBytesToJS(jsSeed, seed)
+ // write it
+ localforage.Call("setItem", keyNameEd25519, jsSeed)
+ return nil
+}
+
+// taken from https://go-review.googlesource.com/c/go/+/150917
+
+// await waits until the promise v has been resolved or rejected and returns the promise's result value.
+// The boolean value ok is true if the promise has been resolved, false if it has been rejected.
+// If v is not a promise, v itself is returned as the value and ok is true.
+func await(v js.Value) (result js.Value, ok bool) {
+ if v.Type() != js.TypeObject || v.Get("then").Type() != js.TypeFunction {
+ return v, true
+ }
+ done := make(chan struct{})
+ onResolve := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+ result = args[0]
+ ok = true
+ close(done)
+ return nil
+ })
+ defer onResolve.Release()
+ onReject := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+ result = args[0]
+ ok = false
+ close(done)
+ return nil
+ })
+ defer onReject.Release()
+ v.Call("then", onResolve, onReject)
+ <-done
+ return
+}
+
+func generateKey() ed25519.PrivateKey {
+ // attempt to look for a seed in JS-land and if it exists use it.
+ priv, err := readKeyFromLocalStorage()
+ if err == nil {
+ fmt.Println("Read key from localStorage")
+ return priv
+ }
+ // generate a new key
+ fmt.Println(err, " : Generating new ed25519 key")
+ _, priv, err = ed25519.GenerateKey(nil)
+ if err != nil {
+ logrus.Fatalf("Failed to generate ed25519 key: %s", err)
+ }
+ if err := writeKeyToLocalStorage(priv); err != nil {
+ fmt.Println("failed to write key to localStorage: ", err)
+ // non-fatal, we'll just have amnesia for a while
+ }
+ return priv
+}
+
+func main() {
+ startup()
+
+ // We want to block forever to let the fetch and libp2p handler serve the APIs
+ select {}
+}
+
+func startup() {
+ sk := generateKey()
+ pk := sk.Public().(ed25519.PublicKey)
+
+ pRouter := pineconeRouter.NewRouter(logrus.WithField("pinecone", "router"), sk, false)
+ pSessions := pineconeSessions.NewSessions(logrus.WithField("pinecone", "sessions"), pRouter, []string{"matrix"})
+ pManager := pineconeConnections.NewConnectionManager(pRouter)
+ pManager.AddPeer("wss://pinecone.matrix.org/public")
+
+ cfg := &config.Dendrite{}
+ cfg.Defaults(true)
+ cfg.UserAPI.AccountDatabase.ConnectionString = "file:/idb/dendritejs_account.db"
+ cfg.AppServiceAPI.Database.ConnectionString = "file:/idb/dendritejs_appservice.db"
+ cfg.FederationAPI.Database.ConnectionString = "file:/idb/dendritejs_fedsender.db"
+ cfg.MediaAPI.Database.ConnectionString = "file:/idb/dendritejs_mediaapi.db"
+ cfg.RoomServer.Database.ConnectionString = "file:/idb/dendritejs_roomserver.db"
+ cfg.SyncAPI.Database.ConnectionString = "file:/idb/dendritejs_syncapi.db"
+ cfg.KeyServer.Database.ConnectionString = "file:/idb/dendritejs_e2ekey.db"
+ cfg.Global.JetStream.StoragePath = "file:/idb/dendritejs/"
+ cfg.Global.TrustedIDServers = []string{}
+ cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
+ cfg.Global.PrivateKey = sk
+ cfg.Global.ServerName = gomatrixserverlib.ServerName(hex.EncodeToString(pk))
+ cfg.ClientAPI.RegistrationDisabled = false
+ cfg.ClientAPI.OpenRegistrationWithoutVerificationEnabled = true
+
+ if err := cfg.Derive(); err != nil {
+ logrus.Fatalf("Failed to derive values from config: %s", err)
+ }
+ base := base.NewBaseDendrite(cfg, "Monolith")
+ defer base.Close() // nolint: errcheck
+
+ federation := conn.CreateFederationClient(base, pSessions)
+ keyAPI := keyserver.NewInternalAPI(base, &base.Cfg.KeyServer, federation)
+
+ serverKeyAPI := &signing.YggdrasilKeys{}
+ keyRing := serverKeyAPI.KeyRing()
+
+ rsAPI := roomserver.NewInternalAPI(base)
+
+ userAPI := userapi.NewInternalAPI(base, &cfg.UserAPI, nil, keyAPI, rsAPI, base.PushGatewayHTTPClient())
+ keyAPI.SetUserAPI(userAPI)
+
+ asQuery := appservice.NewInternalAPI(
+ base, userAPI, rsAPI,
+ )
+ rsAPI.SetAppserviceAPI(asQuery)
+ fedSenderAPI := federationapi.NewInternalAPI(base, federation, rsAPI, base.Caches, keyRing, true)
+ rsAPI.SetFederationAPI(fedSenderAPI, keyRing)
+
+ monolith := setup.Monolith{
+ Config: base.Cfg,
+ Client: conn.CreateClient(base, pSessions),
+ FedClient: federation,
+ KeyRing: keyRing,
+
+ AppserviceAPI: asQuery,
+ FederationAPI: fedSenderAPI,
+ RoomserverAPI: rsAPI,
+ UserAPI: userAPI,
+ KeyAPI: keyAPI,
+ //ServerKeyAPI: serverKeyAPI,
+ ExtPublicRoomsProvider: rooms.NewPineconeRoomProvider(pRouter, pSessions, fedSenderAPI, federation),
+ }
+ monolith.AddAllPublicRoutes(base)
+
+ httpRouter := mux.NewRouter().SkipClean(true).UseEncodedPath()
+ httpRouter.PathPrefix(httputil.InternalPathPrefix).Handler(base.InternalAPIMux)
+ httpRouter.PathPrefix(httputil.PublicClientPathPrefix).Handler(base.PublicClientAPIMux)
+ httpRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(base.PublicMediaAPIMux)
+
+ p2pRouter := pSessions.Protocol("matrix").HTTP().Mux()
+ p2pRouter.Handle(httputil.PublicFederationPathPrefix, base.PublicFederationAPIMux)
+ p2pRouter.Handle(httputil.PublicMediaPathPrefix, base.PublicMediaAPIMux)
+
+ // Expose the matrix APIs via fetch - for local traffic
+ go func() {
+ logrus.Info("Listening for service-worker fetch traffic")
+ s := JSServer{
+ Mux: httpRouter,
+ }
+ s.ListenAndServe("fetch")
+ }()
+}
diff --git a/build/dendritejs-pinecone/main_noop.go b/build/dendritejs-pinecone/main_noop.go
new file mode 100644
index 00000000..0cc7e47e
--- /dev/null
+++ b/build/dendritejs-pinecone/main_noop.go
@@ -0,0 +1,24 @@
+// Copyright 2020 The Matrix.org Foundation C.I.C.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build !wasm
+// +build !wasm
+
+package main
+
+import "fmt"
+
+func main() {
+ fmt.Println("dendritejs: no-op when not compiling for WebAssembly")
+}
diff --git a/build/dendritejs-pinecone/main_test.go b/build/dendritejs-pinecone/main_test.go
new file mode 100644
index 00000000..17fea6cc
--- /dev/null
+++ b/build/dendritejs-pinecone/main_test.go
@@ -0,0 +1,26 @@
+// Copyright 2021 The Matrix.org Foundation C.I.C.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build wasm
+// +build wasm
+
+package main
+
+import (
+ "testing"
+)
+
+func TestStartup(t *testing.T) {
+ startup()
+}
diff --git a/build/docker/Dockerfile.demo-pinecone b/build/docker/Dockerfile.demo-pinecone
deleted file mode 100644
index 133c63c5..00000000
--- a/build/docker/Dockerfile.demo-pinecone
+++ /dev/null
@@ -1,25 +0,0 @@
-FROM docker.io/golang:1.19-alpine AS base
-
-RUN apk --update --no-cache add bash build-base
-
-WORKDIR /build
-
-COPY . /build
-
-RUN mkdir -p bin
-RUN go build -trimpath -o bin/ ./cmd/dendrite-demo-pinecone
-RUN go build -trimpath -o bin/ ./cmd/create-account
-RUN go build -trimpath -o bin/ ./cmd/generate-keys
-
-FROM alpine:latest
-LABEL org.opencontainers.image.title="Dendrite (Pinecone demo)"
-LABEL org.opencontainers.image.description="Next-generation Matrix homeserver written in Go"
-LABEL org.opencontainers.image.source="https://github.com/matrix-org/dendrite"
-LABEL org.opencontainers.image.licenses="Apache-2.0"
-
-COPY --from=base /build/bin/* /usr/bin/
-
-VOLUME /etc/dendrite
-WORKDIR /etc/dendrite
-
-ENTRYPOINT ["/usr/bin/dendrite-demo-pinecone"]
diff --git a/build/docker/Dockerfile.demo-yggdrasil b/build/docker/Dockerfile.demo-yggdrasil
deleted file mode 100644
index 76bf3582..00000000
--- a/build/docker/Dockerfile.demo-yggdrasil
+++ /dev/null
@@ -1,25 +0,0 @@
-FROM docker.io/golang:1.19-alpine AS base
-
-RUN apk --update --no-cache add bash build-base
-
-WORKDIR /build
-
-COPY . /build
-
-RUN mkdir -p bin
-RUN go build -trimpath -o bin/ ./cmd/dendrite-demo-yggdrasil
-RUN go build -trimpath -o bin/ ./cmd/create-account
-RUN go build -trimpath -o bin/ ./cmd/generate-keys
-
-FROM alpine:latest
-LABEL org.opencontainers.image.title="Dendrite (Yggdrasil demo)"
-LABEL org.opencontainers.image.description="Next-generation Matrix homeserver written in Go"
-LABEL org.opencontainers.image.source="https://github.com/matrix-org/dendrite"
-LABEL org.opencontainers.image.licenses="Apache-2.0"
-
-COPY --from=base /build/bin/* /usr/bin/
-
-VOLUME /etc/dendrite
-WORKDIR /etc/dendrite
-
-ENTRYPOINT ["/usr/bin/dendrite-demo-yggdrasil"]
diff --git a/build/docker/Dockerfile.monolith b/build/docker/Dockerfile.monolith
deleted file mode 100644
index 3180e962..00000000
--- a/build/docker/Dockerfile.monolith
+++ /dev/null
@@ -1,25 +0,0 @@
-FROM docker.io/golang:1.19-alpine AS base
-
-RUN apk --update --no-cache add bash build-base
-
-WORKDIR /build
-
-COPY . /build
-
-RUN mkdir -p bin
-RUN go build -trimpath -o bin/ ./cmd/dendrite-monolith-server
-RUN go build -trimpath -o bin/ ./cmd/create-account
-RUN go build -trimpath -o bin/ ./cmd/generate-keys
-
-FROM alpine:latest
-LABEL org.opencontainers.image.title="Dendrite (Monolith)"
-LABEL org.opencontainers.image.description="Next-generation Matrix homeserver written in Go"
-LABEL org.opencontainers.image.source="https://github.com/matrix-org/dendrite"
-LABEL org.opencontainers.image.licenses="Apache-2.0"
-
-COPY --from=base /build/bin/* /usr/bin/
-
-VOLUME /etc/dendrite
-WORKDIR /etc/dendrite
-
-ENTRYPOINT ["/usr/bin/dendrite-monolith-server"]
diff --git a/build/docker/Dockerfile.polylith b/build/docker/Dockerfile.polylith
deleted file mode 100644
index 79f8a5f2..00000000
--- a/build/docker/Dockerfile.polylith
+++ /dev/null
@@ -1,25 +0,0 @@
-FROM docker.io/golang:1.19-alpine AS base
-
-RUN apk --update --no-cache add bash build-base
-
-WORKDIR /build
-
-COPY . /build
-
-RUN mkdir -p bin
-RUN go build -trimpath -o bin/ ./cmd/dendrite-polylith-multi
-RUN go build -trimpath -o bin/ ./cmd/create-account
-RUN go build -trimpath -o bin/ ./cmd/generate-keys
-
-FROM alpine:latest
-LABEL org.opencontainers.image.title="Dendrite (Polylith)"
-LABEL org.opencontainers.image.description="Next-generation Matrix homeserver written in Go"
-LABEL org.opencontainers.image.source="https://github.com/matrix-org/dendrite"
-LABEL org.opencontainers.image.licenses="Apache-2.0"
-
-COPY --from=base /build/bin/* /usr/bin/
-
-VOLUME /etc/dendrite
-WORKDIR /etc/dendrite
-
-ENTRYPOINT ["/usr/bin/dendrite-polylith-multi"]
diff --git a/build/docker/README.md b/build/docker/README.md
index 261519fd..6111b830 100644
--- a/build/docker/README.md
+++ b/build/docker/README.md
@@ -9,11 +9,16 @@ They can be found on Docker Hub:
## Dockerfiles
-The `Dockerfile` builds the base image which contains all of the Dendrite
-components. The `Dockerfile.component` file takes the given component, as
-specified with `--buildarg component=` from the base image and produce
-smaller component-specific images, which are substantially smaller and do
-not contain the Go toolchain etc.
+The `Dockerfile` is a multistage file which can build all four Dendrite
+images depending on the supplied `--target`. From the root of the Dendrite
+repository, run:
+
+```
+docker build . --target monolith -t matrixdotorg/dendrite-monolith
+docker build . --target polylith -t matrixdotorg/dendrite-monolith
+docker build . --target demo-pinecone -t matrixdotorg/dendrite-demo-pinecone
+docker build . --target demo-yggdrasil -t matrixdotorg/dendrite-demo-yggdrasil
+```
## Compose files
diff --git a/build/docker/images-build.sh b/build/docker/images-build.sh
index c2c14068..d97a701e 100755
--- a/build/docker/images-build.sh
+++ b/build/docker/images-build.sh
@@ -6,5 +6,7 @@ TAG=${1:-latest}
echo "Building tag '${TAG}'"
-docker build -t matrixdotorg/dendrite-monolith:${TAG} -f build/docker/Dockerfile.monolith .
-docker build -t matrixdotorg/dendrite-polylith:${TAG} -f build/docker/Dockerfile.polylith . \ No newline at end of file
+docker build . --target monolith -t matrixdotorg/dendrite-monolith:${TAG}
+docker build . --target polylith -t matrixdotorg/dendrite-monolith:${TAG}
+docker build . --target demo-pinecone -t matrixdotorg/dendrite-demo-pinecone:${TAG}
+docker build . --target demo-yggdrasil -t matrixdotorg/dendrite-demo-yggdrasil:${TAG} \ No newline at end of file