Skip to content

build(image): ship one binary and keep the .so files out of the image - #9291

Merged
klesh merged 1 commit into
feat/single-binaryfrom
feat/single-binary-image
Oct 31, 2026
Merged

klesh merged 1 commit into
feat/single-binaryfrom
feat/single-binary-image

Conversation

@klesh

@klesh klesh commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Stacked on #9288 (base branch feat/single-binary), because it needs the generated plugin registry to have anything to link in.

make build now means build-server: the framework, the API and every plugin in plugins/all link into a single bin/lake. The image build stops producing .so files entirely.

How the image builds now

Stage Before After
build make all → build-plugin (47 × go build -buildmode=plugin) then build-server make all → build-server → one go build ./server
payload /app/bin/lake + /app/bin/plugins/*/*.so /app/bin/lake
runtime copy COPY --from=build /app/bin /app/bin COPY --from=build /app/bin/lake /app/bin/lake

The runtime stage gets an empty /app/bin/plugins as the sideload mount point. It is created as root, before the chgrp -R 0 /app, because the stage otherwise runs as the devlake user and could not create it.

Measured

core-only, stripped          58.4 MB
full binary, stripped       106.6 MB   (plugins add 48.2 MB)
full binary, unstripped     148.9 MB

So the Go payload goes from ~2.75 GB to ~107 MB. Against the current apache/devlake:main_* image (3.06 GB, of which the :base stage is 306 MB) the image should land around ~415 MB, roughly 7× smaller. The base stage is now the largest single piece.

Also in here

  • build-server.sh gains -trimpath and -s -w. A DEVLAKE_DEBUG=1 build keeps the symbols so make debug still works.
  • Dropped COPY --from=build /go/bin /usr/bin. mockery and swag are build tools; nothing at runtime uses them.
  • dev, godev and build no longer build plugins. build-plugin stays for sideload and core-only builds.
  • plugins-registry clears GOOS/GOARCH. backend/Dockerfile exports them to cross-compile the server, and go run would otherwise build the generator for the target platform and fail with exec format error on an amd64 host building the arm64 image.

GO_PLUGINS keeps working: it reaches the registry generator, so it now chooses which plugins are compiled in rather than which .so files are built. GO_PLUGINS=none yields a core-only binary.

Does this close any open issues?

Part of #9276.

Other Information

Verified locally:

VERSION=test@local scripts/build-server.sh   # bin/lake, 102 MB, stripped, no bin/plugins created
go build -tags devlake_external_plugins      # 58.4 MB, core only
GOOS=linux GOARCH=arm64 make plugins-registry  # no "exec format error"

The arm64 image build itself is not exercised on PRs (.github/workflows/build.yml runs on main and tags), so that path needs a careful look at merge time.

`make build` is now `build-server`: the framework, the API and every plugin in
the generated registry link into a single bin/lake, so the runtime stage has no
plugin directory to copy.

- backend/Dockerfile: copy only /app/bin/lake instead of the whole bin/, and
  create an empty /app/bin/plugins as the sideload mount point. It is created
  as root, before the chgrp that makes /app group-writable, because the stage
  otherwise runs as the devlake user.
- Drop `COPY --from=build /go/bin /usr/bin`: mockery and swag are build tools
  and nothing at runtime uses them.
- Dockerfile.local: one build step instead of build-plugin plus build-server,
  and copy only the binary.
- build-server.sh: add -trimpath and -s -w. A DEVLAKE_DEBUG build keeps the
  symbols so delve still works.
- Makefile: build, dev and godev no longer build plugins; build-plugin stays
  for sideload and core-only builds.
- env.example: PLUGIN_DIR now documents the sideload directory.

GO_PLUGINS keeps working: it reaches the registry generator, so it now selects
which plugins are compiled in rather than which .so files are built.

plugins-registry clears GOOS/GOARCH: the Dockerfile exports them to
cross-compile the server, and `go run` would otherwise build the generator for
the target platform and fail with "exec format error" on an amd64 build host.
@klesh klesh added the pr-type/feature-development This PR is to develop a new feature label Oct 31, 2026
@klesh
klesh merged commit e0bc27e into feat/single-binary Oct 31, 2026
5 checks passed
@klesh
klesh deleted the feat/single-binary-image branch October 31, 2026 04:51
Startrekzky pushed a commit that referenced this pull request Oct 31, 2026
…stry (#9288)

* feat(build): link plugins into the server binary via a generated registry

Plugins were only discoverable at runtime, through the .so files in
PLUGIN_DIR. Add a compile-time path so the whole plugin set can be linked
into one binary:

- core/plugin.MustRegister registers a plugin under the name it reports.
- tools/pluginsgen scans plugins/*/impl for the type implementing
  plugin.PluginMeta and generates plugins/all, which imports each plugin
  and registers it from an init function. It honours DEVLAKE_PLUGINS so
  subset builds keep working, and emits an empty package for "none".
- server/main.go blank-imports the registry. Building with
  -tags devlake_external_plugins swaps in an empty package for the
  shared-object-only build.
- runner.LoadGoPlugins treats a missing PLUGIN_DIR as "no shared-object
  plugins" instead of failing startup, so a single-binary deployment does
  not need to ship an empty directory.
- CI regenerates the registry and fails when the checked-in file drifts.

Build scripts and Dockerfiles are deliberately untouched: `make build`
still produces the .so files, and they register over the compiled-in ones
harmlessly. Repackaging follows separately.

* feat(plugin): treat sideloaded .so plugins as a supported extension path

Reframe the runtime loader around the shipping model: the official binary
compiles in every official plugin, and PLUGIN_DIR is where users drop
additional or replacement plugins.

- A broken or incompatible .so is logged and skipped instead of aborting
  startup. Sideloaded plugins are third-party code built outside this
  repo; one bad file must not take the server down.
- A shared object reporting a name that is already registered (an official
  plugin) overrides it, with a warning. That is the existing behaviour and
  it is what makes patching an official plugin possible, so keep it but
  make it visible.
- Extract open/lookup/assert into openGoPlugin and stop the loader
  goroutine from writing the walk's `err` variable.
- Update the generated registry and the core-only build tag docs to
  describe sideloading rather than presenting .so as a legacy fallback.

* build(image): ship one binary and keep the .so files out of the image (#9291)

`make build` is now `build-server`: the framework, the API and every plugin in
the generated registry link into a single bin/lake, so the runtime stage has no
plugin directory to copy.

- backend/Dockerfile: copy only /app/bin/lake instead of the whole bin/, and
  create an empty /app/bin/plugins as the sideload mount point. It is created
  as root, before the chgrp that makes /app group-writable, because the stage
  otherwise runs as the devlake user.
- Drop `COPY --from=build /go/bin /usr/bin`: mockery and swag are build tools
  and nothing at runtime uses them.
- Dockerfile.local: one build step instead of build-plugin plus build-server,
  and copy only the binary.
- build-server.sh: add -trimpath and -s -w. A DEVLAKE_DEBUG build keeps the
  symbols so delve still works.
- Makefile: build, dev and godev no longer build plugins; build-plugin stays
  for sideload and core-only builds.
- env.example: PLUGIN_DIR now documents the sideload directory.

GO_PLUGINS keeps working: it reaches the registry generator, so it now selects
which plugins are compiled in rather than which .so files are built.

plugins-registry clears GOOS/GOARCH: the Dockerfile exports them to
cross-compile the server, and `go run` would otherwise build the generator for
the target platform and fail with "exec format error" on an amd64 build host.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-type/feature-development This PR is to develop a new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant