diff --git a/.gitignore b/.gitignore
index 66fd13c..fca6e36 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,9 @@
 
 # Dependency directories (remove the comment below to include it)
 # vendor/
+
+/.dapper
+/bin
+/dist
+/build
+*.swp
diff --git a/Dockerfile.dapper b/Dockerfile.dapper
new file mode 100644
index 0000000..ffde8c7
--- /dev/null
+++ b/Dockerfile.dapper
@@ -0,0 +1,37 @@
+FROM golang:1.16
+
+ARG DAPPER_HOST_ARCH
+ENV HOST_ARCH=${DAPPER_HOST_ARCH} ARCH=${DAPPER_HOST_ARCH}
+
+RUN apt-get update && \
+    apt-get install -y ca-certificates git wget curl xz-utils && \
+    rm -f /bin/sh && ln -s /bin/bash /bin/sh && \
+    curl -sL https://github.com/upx/upx/releases/download/v3.96/upx-3.96-${ARCH}_linux.tar.xz | tar xvJf - --strip-components=1 -C /tmp && \
+    mv /tmp/upx /usr/bin/
+
+RUN if [ "${ARCH}" == "amd64" ]; then \
+    curl -sL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s v1.27.0; \
+    fi
+
+ENV DOCKER_URL_amd64=https://get.docker.com/builds/Linux/x86_64/docker-1.10.3 \
+    DOCKER_URL_arm=https://github.com/rancher/docker/releases/download/v1.10.3-ros1/docker-1.10.3_arm \
+    DOCKER_URL_arm64=https://github.com/rancher/docker/releases/download/v1.10.3-ros1/docker-1.10.3_arm64 \
+    DOCKER_URL=DOCKER_URL_${ARCH}
+RUN wget -O - ${!DOCKER_URL} > /usr/bin/docker && chmod +x /usr/bin/docker
+
+ENV GIT_COMMIT="327be56d3a6a2b85cf4751148f6834402e8211d5" \
+    GIT_BRANCH="kube-explorer" \
+    GIT_SOURCE="/go/src/github.com/rancher/steve" \
+    CATTLE_DASHBOARD_UI_VERSION=v2.5.8-rc3
+
+ENV DAPPER_ENV REPO TAG DRONE_TAG CROSS
+ENV DAPPER_SOURCE /opt/kube-explorer
+ENV DAPPER_OUTPUT ./bin ./dist
+ENV DAPPER_DOCKER_SOCKET true
+ENV DAPPER_RUN_ARGS "-v ke-pkg:/go/pkg -v ke-cache:/root/.cache/go-build --privileged"
+ENV GOCACHE /root/.cache/go-build
+ENV HOME ${DAPPER_SOURCE}
+WORKDIR ${DAPPER_SOURCE}
+
+ENTRYPOINT ["./scripts/entry"]
+CMD ["ci"]
diff --git a/Makefile b/Makefile
index 3b99aa5..37ffb5a 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,16 @@
-build:
-	docker build -t niusmallnan/kube-explorer package/
+TARGETS := $(shell ls scripts)
 
-run: build
-	docker run $(DOCKER_ARGS) --rm -p 8989:9080 -it -v ${HOME}/.kube:/root/.kube niusmallnan/kube-explorer --https-listen-port 0 --kubeconfig /root/.kube/config
+.dapper:
+	@echo Downloading dapper
+	@curl -sL https://releases.rancher.com/dapper/latest/dapper-`uname -s`-`uname -m` > .dapper.tmp
+	@@chmod +x .dapper.tmp
+	@./.dapper.tmp -v
+	@mv .dapper.tmp .dapper
 
-run-host: build
-	docker run $(DOCKER_ARGS) --net=host --uts=host --rm -it -v ${HOME}/.kube:/root/.kube niusmallnan/kube-explorer --kubeconfig /root/.kube/config --http-listen-port 8989 --https-listen-port 0
+$(TARGETS): .dapper
+	./.dapper $@
+
+.DEFAULT_GOAL := ci
+
+shell-bind: .dapper
+	./.dapper -m bind -s
diff --git a/package/Dockerfile b/package/Dockerfile
index 3144af0..8b7ecba 100644
--- a/package/Dockerfile
+++ b/package/Dockerfile
@@ -1,27 +1,6 @@
-FROM golang:1.15.11 as build
-ENV GIT_COMMIT="8c327e08adf2905981d492c48a0618e6740d25d1"
-ENV GIT_BRANCH="kube-explorer"
-RUN \
-    mkdir -p /go/src/github.com/rancher/ && \
-    cd /go/src/github.com/rancher/ && \
-    git clone --depth=1 --branch ${GIT_BRANCH} https://github.com/niusmallnan/steve.git && \
-    cd steve && \
-    git reset --hard ${GIT_COMMIT} && \
-    go mod vendor && \
-    CGO_ENABLED=0 go build -ldflags "-X github.com/rancher/steve/pkg/ui.UIOffline=true -extldflags -static -s" -o /kube-explorer
-
 FROM alpine:3.13
 
-ENV CATTLE_DASHBOARD_UI_VERSION v2.5.8-rc3
-
-RUN apk -U --no-cache add bash curl ca-certificates && \
-    mkdir -p /usr/share/rancher/ui-dashboard/dashboard && \
-    cd /usr/share/rancher/ui-dashboard/dashboard && \
-    curl -sL https://releases.rancher.com/dashboard/${CATTLE_DASHBOARD_UI_VERSION}.tar.gz | tar xvzf - --strip-components=2 && \
-    ln -s dashboard/index.html ../index.html
-
-COPY --from=build /kube-explorer /usr/bin/
-COPY entrypoint.sh /usr/bin
+COPY kube-explorer entrypoint.sh /usr/bin/
 # Hack to make golang do files,dns search order
 ENV LOCALDOMAIN=""
 ENTRYPOINT ["entrypoint.sh"]
diff --git a/package/entrypoint.sh b/package/entrypoint.sh
index c1509d2..ff29fd0 100755
--- a/package/entrypoint.sh
+++ b/package/entrypoint.sh
@@ -1,3 +1,3 @@
 #!/bin/bash
 
-/usr/bin/kube-explorer --ui-path /usr/share/rancher/ui-dashboard "${@}"
+/usr/bin/kube-explorer "${@}"
diff --git a/scripts/build b/scripts/build
new file mode 100755
index 0000000..51c6c7e
--- /dev/null
+++ b/scripts/build
@@ -0,0 +1,55 @@
+#!/bin/bash
+set -e
+
+source $(dirname $0)/version
+
+OS_ARCH_ARG_LINUX="amd64 arm arm64"
+OS_ARCH_ARG_WINDOWS="amd64"
+OS_ARCH_ARG_DARWIN="amd64"
+
+LD_INJECT_VALUES="-X github.com/rancher/steve/pkg/version.Version=$VERSION
+                  -X github.com/rancher/steve/pkg/version.GitCommit=$COMMIT"
+
+[ "$(uname)" != "Darwin" ] && LINKFLAGS="-extldflags -static -s"
+
+pushd $GIT_SOURCE
+
+CGO_ENABLED=0 go build -tags embed \
+    -ldflags \
+    "$LD_INJECT_VALUES $LINKFLAGS" \
+    -o bin/kube-explorer
+
+if [ -n "$CROSS" ]; then
+    for ARCH in ${OS_ARCH_ARG_LINUX}; do
+        OUTPUT_BIN="bin/kube-explorer-linux-$ARCH"
+        echo "Building binary for linux/$ARCH..."
+        GOARCH=$ARCH GOOS=linux CGO_ENABLED=0 go build -tags embed \
+                -ldflags \
+                "$LD_INJECT_VALUES" \
+                -o ${OUTPUT_BIN}
+    done
+
+    for ARCH in ${OS_ARCH_ARG_WINDOWS}; do
+        OUTPUT_BIN="bin/kube-explorer-windows-$ARCH.exe"
+        echo "Building binary for windows/$ARCH..."
+        GOARCH=$ARCH GOOS=windows CGO_ENABLED=0 go build -tags embed \
+                -ldflags \
+                "$LD_INJECT_VALUES" \
+                -o ${OUTPUT_BIN}
+    done
+
+    for ARCH in ${OS_ARCH_ARG_DARWIN}; do
+        OUTPUT_BIN="bin/kube-explorer-darwin-$ARCH"
+        echo "Building binary for darwin/$ARCH..."
+        GOARCH=$ARCH GOOS=darwin CGO_ENABLED=0 go build -tags embed \
+                -ldflags \
+                "$LD_INJECT_VALUES" \
+                -o ${OUTPUT_BIN}
+    done
+fi
+
+for f in $(ls ./bin/); do
+    upx -o $DAPPER_SOURCE/bin/$f bin/$f
+done
+
+popd
diff --git a/scripts/ci b/scripts/ci
new file mode 100755
index 0000000..e1ea1b5
--- /dev/null
+++ b/scripts/ci
@@ -0,0 +1,9 @@
+#!/bin/bash
+set -e
+
+cd $(dirname $0)
+
+./download
+./validate
+./build
+./package
diff --git a/scripts/dev b/scripts/dev
new file mode 100755
index 0000000..6e91b66
--- /dev/null
+++ b/scripts/dev
@@ -0,0 +1,18 @@
+#!/bin/bash
+set -e
+
+cd $(dirname $0)
+./download
+
+[ "$(uname)" != "Darwin" ] && LINKFLAGS="-extldflags -static -s"
+
+pushd $GIT_SOURCE
+
+CGO_ENABLED=0 go build \
+    -ldflags \
+    "$LINKFLAGS" \
+    -o bin/kube-explorer
+
+mv bin/kube-explorer $DAPPER_SOURCE/bin/
+
+popd
diff --git a/scripts/download b/scripts/download
new file mode 100755
index 0000000..7377d29
--- /dev/null
+++ b/scripts/download
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+mkdir -p $(dirname $GIT_SOURCE)
+
+pushd $(dirname $GIT_SOURCE)
+
+git clone --depth=1 --branch ${GIT_BRANCH} https://github.com/niusmallnan/steve.git
+cd steve
+git reset --hard ${GIT_COMMIT}
+
+mkdir -p pkg/ui/ui/dashboard
+cd pkg/ui/ui/dashboard
+curl -sL https://releases.rancher.com/dashboard/${CATTLE_DASHBOARD_UI_VERSION}.tar.gz | tar xvzf - --strip-components=2
+cp index.html ../index.html
+
+popd
+
+$(dirname $0)/hack_fs $GIT_SOURCE/pkg/ui/ui/
diff --git a/scripts/entry b/scripts/entry
new file mode 100755
index 0000000..78fb567
--- /dev/null
+++ b/scripts/entry
@@ -0,0 +1,11 @@
+#!/bin/bash
+set -e
+
+mkdir -p bin dist
+if [ -e ./scripts/$1 ]; then
+    ./scripts/"$@"
+else
+    exec "$@"
+fi
+
+chown -R $DAPPER_UID:$DAPPER_GID .
diff --git a/scripts/hack_fs b/scripts/hack_fs
new file mode 100755
index 0000000..dee9502
--- /dev/null
+++ b/scripts/hack_fs
@@ -0,0 +1,42 @@
+#!/bin/bash
+
+set -ex
+
+#
+# find . -type f -name "_*"
+#
+function hack_files() {
+    for f in $(find $1 -type f -name "_*"); do
+        name=$(basename $f)
+        updir=$(dirname $f)
+        new_path=$updir/${name:1}
+        echo "move $f $new_path"
+        mv $f $new_path
+    done
+}
+
+#
+# find . -type d -name "_*"
+#
+function hack_dirs() {
+    for d in $(find $1 -mindepth 1 -maxdepth 1 -type d); do
+        if [[ ! -d $d ]]; then
+            continue
+        fi
+        name=$(basename $d)
+        if [[ ${name:0:1} == "_" ]]; then
+            updir=$(dirname $d)
+            new_path=$updir/${name:1}
+            echo "move $d $new_path"
+            mv $d $new_path
+            hack_dirs $new_path
+            continue
+        fi
+        hack_dirs $d
+    done
+}
+
+pushd $1
+hack_files .
+hack_dirs .
+popd
diff --git a/scripts/package b/scripts/package
new file mode 100755
index 0000000..da9937d
--- /dev/null
+++ b/scripts/package
@@ -0,0 +1,12 @@
+#!/bin/bash
+set -e
+
+source $(dirname $0)/version
+
+pushd $DAPPER_SOURCE
+
+cp bin/kube-explorer package/
+cd package
+docker build -f Dockerfile -t niusmallnan/kube-explorer:$VERSION .
+
+popd
diff --git a/scripts/validate b/scripts/validate
new file mode 100755
index 0000000..8e4e659
--- /dev/null
+++ b/scripts/validate
@@ -0,0 +1,21 @@
+#!/bin/bash
+set -e
+
+pushd $GIT_SOURCE
+
+if ! command -v golangci-lint; then
+    echo Running: go fmt
+    echo Skipping validation: no golangci-lint available test -z "$(go fmt ./... | tee /dev/stderr)"
+    exit
+fi
+
+#echo Running: golangci-lint
+#golangci-lint run
+
+echo Tidying up modules
+go mod tidy
+
+echo Verifying modules
+go mod verify
+
+popd
diff --git a/scripts/version b/scripts/version
new file mode 100755
index 0000000..9e7a5b8
--- /dev/null
+++ b/scripts/version
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+if [ -n "$(git status --porcelain --untracked-files=no)" ]; then
+    DIRTY="-dirty"
+fi
+
+COMMIT=$(git rev-parse --short HEAD)
+GIT_TAG=${DRONE_TAG:-$(git tag -l --contains HEAD | head -n 1)}
+
+if [[ -z "$DIRTY" && -n "$GIT_TAG" ]]; then
+    VERSION=$GIT_TAG
+else
+    VERSION="${COMMIT}${DIRTY}"
+fi
+
+if [ -z "$ARCH" ]; then
+    ARCH=$(go env GOHOSTARCH)
+fi