diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..d86c502
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,3 @@
+.git
+Dockerfile
+forgejo-runner
diff --git a/.forgejo/workflows/integration.yml b/.forgejo/workflows/integration.yml
new file mode 100644
index 0000000..bbb7bee
--- /dev/null
+++ b/.forgejo/workflows/integration.yml
@@ -0,0 +1,51 @@
+name: Integration tests for the release process
+
+on: 
+  push:
+    paths:
+      - .forgejo/workflows/release.yml
+      - .forgejo/workflows/integration.yml
+
+jobs:
+  release-simulation:
+    runs-on: self-hosted
+    steps:
+      - uses: actions/checkout@v3
+
+      - id: forgejo
+        uses: https://code.forgejo.org/actions/setup-forgejo@v1
+        with:
+          user: root
+          password: admin1234
+          image-version: 1.19
+          lxc-ip-prefix: 10.0.9
+
+      - name: publish the runner release
+        run: |
+          set -x
+
+          dir=$(mktemp -d)
+          trap "rm -fr $dir" EXIT
+
+          url=http://root:admin1234@${{ steps.forgejo.outputs.host-port }}
+          export FORGEJO_RUNNER_LOGS="${{ steps.forgejo.outputs.runner-logs }}"
+
+          #
+          # Create a new project with the runner and the release workflow only
+          #
+          rsync -a --exclude .git ./ $dir/
+          rm $(find $dir/.forgejo/workflows/*.yml | grep -v release.yml)
+          forgejo-test-helper.sh push $dir $url root runner |& tee $dir/pushed
+          eval $(grep '^sha=' < $dir/pushed)
+
+          #
+          # Push a tag to trigger the release workflow and wait for it to complete
+          #
+          forgejo-test-helper.sh api POST $url repos/root/runner/tags ${{ steps.forgejo.outputs.token }} --data-raw '{"tag_name": "v1.2.3", "target": "'$sha'"}'
+          LOOPS=180 forgejo-test-helper.sh wait_success "$url" root/runner $sha
+
+          #
+          # Minimal sanity checks. e2e test is for the setup-forgejo action
+          # and the infrastructure playbook.
+          #
+          curl -sS $url/root/runner/releases/download/v1.2.3/forgejo-runner-amd64 > /dev/null
diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml
new file mode 100644
index 0000000..3337b8d
--- /dev/null
+++ b/.forgejo/workflows/release.yml
@@ -0,0 +1,114 @@
+name: Publish release
+
+on: 
+  push:
+    tags: 'v*'
+
+jobs:
+  release:
+    runs-on: self-hosted
+    steps:
+      - uses: actions/checkout@v3
+
+      - id: verbose
+        run: |
+          # if there are no secrets, be verbose
+          if test -z "${{ secrets.TOKEN }}"; then
+            value=true
+          else
+            value=false
+          fi
+          echo "value=$value" >> "$GITHUB_OUTPUT"
+          echo "shell=set -x" >> "$GITHUB_OUTPUT"
+          
+      - id: registry
+        run: |
+          ${{ steps.verbose.outputs.shell }}
+          url="${{ env.GITHUB_SERVER_URL }}"
+          hostport=${url##http*://}
+          hostport=${hostport%%/}
+          echo "host-port=${hostport}" >> "$GITHUB_OUTPUT"
+          if ! [[ $url =~ ^http:// ]] ; then
+             exit 0
+          fi
+          cat >> "$GITHUB_OUTPUT" <<EOF
+          insecure=true
+          buildx-config<<ENDVAR
+          [registry."${hostport}"]
+            http = true
+          ENDVAR
+          EOF
+        
+      - id: secrets
+        run: |
+          token="${{ secrets.TOKEN }}"
+          doer="${{ secrets.DOER }}"
+          if test -z "$token"; then
+             apt-get -qq install -y jq
+             doer=root
+             api=http://$doer:admin1234@${{ steps.registry.outputs.host-port }}/api/v1/users/$doer/tokens
+             curl -sS -X DELETE $api/release
+             token=$(curl -sS -X POST -H 'Content-Type: application/json' --data-raw '{"name": "release", "scopes": ["all"]}' $api | jq --raw-output .sha1)
+          fi
+          echo "token=${token}" >> "$GITHUB_OUTPUT"
+          echo "doer=${doer}" >> "$GITHUB_OUTPUT"
+
+      - name: allow docker pull/push to forgejo
+        if: ${{ steps.registry.outputs.insecure }}
+        run: |-
+          mkdir /etc/docker
+          cat > /etc/docker/daemon.json <<EOF
+            {
+              "insecure-registries" : ["${{ steps.registry.outputs.host-port }}"],
+              "bip": "172.26.0.1/16"
+            }
+          EOF
+
+      - run: |
+          echo deb http://deb.debian.org/debian bullseye-backports main | tee /etc/apt/sources.list.d/backports.list && apt-get -qq update
+          DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -qq -y -t bullseye-backports docker.io
+          
+      - uses: https://github.com/docker/setup-buildx-action@v2
+        with:
+          config-inline: |
+           ${{ steps.registry.outputs.buildx-config }}
+
+      - run: |
+          BASE64_AUTH=`echo -n "${{ steps.secrets.outputs.doer }}:${{ steps.secrets.outputs.token }}" | base64`
+          mkdir -p ~/.docker
+          echo "{\"auths\": {\"$CI_REGISTRY\": {\"auth\": \"$BASE64_AUTH\"}}}" > ~/.docker/config.json
+        env:
+          CI_REGISTRY: "${{ env.GITHUB_SERVER_URL }}${{ env.GITHUB_REPOSITORY_OWNER }}"
+
+      - id: build
+        run: |
+          ${{ steps.verbose.outputs.shell }}
+          tag="${{ github.ref_name }}"
+          tag=${tag##*v}
+          echo "tag=$tag" >> "$GITHUB_OUTPUT"
+          echo "image=${{ steps.registry.outputs.host-port }}/${{ github.repository }}:${tag}" >> "$GITHUB_OUTPUT"
+          
+      - uses: https://github.com/docker/build-push-action@v4
+        with:
+          context: .
+          push: true
+          platforms: linux/amd64,linux/arm64
+          tags: ${{ steps.build.outputs.image }}
+
+      - run: |
+          ${{ steps.verbose.outputs.shell }}
+          mkdir -p release
+          for arch in amd64 arm64; do
+            docker create --platform linux/$arch --name runner ${{ steps.build.outputs.image }}
+            docker cp runner:/bin/forgejo-runner release/forgejo-runner-$arch
+            shasum -a 256 < release/forgejo-runner-$arch > release/forgejo-runner-$arch.sha256
+            docker rm runner
+          done
+
+      - uses: https://code.forgejo.org/actions/forgejo-release@v1
+        with:
+          direction: upload
+          release-dir: release
+          release-notes: "RELEASE-NOTES#${{ steps.build.outputs.tag }}"
+          token: ${{ steps.secrets.outputs.token }}
+          verbose: ${{ steps.verbose.outputs.value }}
diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml
index bc8e956..25ec300 100644
--- a/.forgejo/workflows/test.yml
+++ b/.forgejo/workflows/test.yml
@@ -1,7 +1,8 @@
 name: checks
 on: 
-  - push
   - pull_request
+  - push:
+      branches: [main]
 
 env:
   GOPROXY: https://goproxy.io,direct
@@ -21,3 +22,14 @@ jobs:
         run: make build
       - name: test
         run: make test
+      - run: |
+          mkdir release
+          mv forgejo-runner release
+      - if: ${{ startsWith(github.ref, 'refs/tags/v') }}
+        uses: https://code.forgejo.org/actions/forgejo-release@v1
+        with:
+          direction: upload
+          release-dir: release
+          release-notes: "RELEASE-NOTES#${{ github.ref_name }}"
+          token: ${{ secrets.TOKEN }}
+          verbose: true
diff --git a/.gitignore b/.gitignore
index a77f656..8328ce1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+*~
 forgejo-runner
 .env
 .runner
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..0fdae4a
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,15 @@
+#Build stage
+FROM golang:1.20-alpine3.17 AS build-env
+
+RUN apk --no-cache add build-base git
+
+COPY . /srv
+WORKDIR /srv
+RUN make build
+
+FROM alpine:3.17
+LABEL maintainer="contact@forgejo.org"
+
+COPY --from=build-env /srv/forgejo-runner /bin/forgejo-runner
+
+ENTRYPOINT ["/bin/forgejo-runner"]
diff --git a/Makefile b/Makefile
index 437138c..8496e1f 100644
--- a/Makefile
+++ b/Makefile
@@ -17,11 +17,7 @@ WINDOWS_ARCHS ?= windows/amd64
 GO_FMT_FILES := $(shell find . -type f -name "*.go" ! -name "generated.*")
 GOFILES := $(shell find . -type f -name "*.go" -o -name "go.mod" ! -name "generated.*")
 
-ifneq ($(shell uname), Darwin)
-	EXTLDFLAGS = -extldflags "-static" $(null)
-else
-	EXTLDFLAGS =
-endif
+EXTLDFLAGS = -extldflags "-static" $(null)
 
 ifeq ($(HAS_GO), GO)
 	GOPATH ?= $(shell $(GO) env GOPATH)
@@ -107,7 +103,7 @@ install: $(GOFILES)
 build: go-check $(EXECUTABLE)
 
 $(EXECUTABLE): $(GOFILES)
-	$(GO) build -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o $@
+	$(GO) build -v -tags 'netgo osusergo $(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o $@
 
 .PHONY: deps-backend
 deps-backend: