From 2e054572c40a868df16e8f1120763ee771dc7817 Mon Sep 17 00:00:00 2001 From: Peter Zandbergen Date: Thu, 18 Jun 2026 08:06:39 +0200 Subject: [PATCH 1/8] OpenCode init --- AGENTS.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..66d04c2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,70 @@ +# AGENTS.md + +## Project Overview + +Go-based REST API wrapping [signal-cli](https://github.com/AsamK/signal-cli) for Signal Messenger. Runs exclusively inside Docker; there is no standalone `go run` dev server for the full API. The Go app orchestrates `signal-cli` (a Java binary) or `signal-cli-native` (GraalVM precompiled), communicating via CLI args in normal/native mode or JSON-RPC in daemon mode. + +## Architecture + +- `src/` — Go source (the REST API server) + - `main.go` — entrypoint; sets up Gin router, routes, cron, plugin loader + - `api/api.go` — all HTTP handlers + - `client/` — signal-cli interaction layer; three modes: `Normal`, `Native`, `JsonRpc` (see `client.go` constants) + - `datastructs/datastructs.go` — shared structs + - `utils/` — helpers, plugin config, text-style parser + - `docs/` — Swagger docs (auto-generated by `swag init`) + - `scripts/jsonrpc2-helper.go` — daemon manager for json-rpc mode +- `plugin_loader.go` — Go plugin system using Lua scripts (v1/v2 plugin API) +- `plugins/` — example Lua plugins and persistence plugin +- `Dockerfile` — multi-stage build: Go compile → release image with signal-cli + Java runtime +- `entrypoint.sh` — container entrypoint; handles UID/GID, chown, supervisor for json-rpc mode + +## Build & Run + +- **Build & test (local):** All commands run from `src/` + ```bash + cd src + go test ./client -v + go test ./utils -v + go build -o signal-cli-rest-api main.go + ``` +- **Build & run (Docker, the primary workflow):** + ```bash + # Edit docker-compose.yml: change image to build: "." + docker compose build + docker compose up + ``` +- **No linter or typecheck config exists** — standard Go tooling only (e.g., `go vet`). + +## Swagger / API Docs + +Docs are auto-generated from swag annotations in `main.go` and `api/api.go`. + +```bash +cd src +go run github.com/swaggo/swag/cmd/swag@v1.16.6 init --requiredByDefault --outputTypes "go,json" +``` + +The receive V1 schemas require an extra step (`add_v1_receive_schemas.go`); see `src/docs/README.md`. CI checks that generated docs are up-to-date (workflow: `check-docs.yml`). + +## Key Environment Variables + +| Variable | Default | Purpose | +|---|---|---| +| `MODE` | `normal` | `normal`, `native`, `json-rpc`, `json-rpc-native` | +| `PORT` | `8080` | Listen port | +| `ENABLE_PLUGINS` | `false` | Enable Lua plugin system | +| `AUTO_RECEIVE_SCHEDULE` | — | Cron expression; **incompatible with json-rpc mode** | +| `SIGNAL_CLI_CMD_TIMEOUT` | — | CLI timeout; **incompatible with json-rpc mode** | +| `RECEIVE_WEBHOOK_URL` | — | Webhook on receive; **json-rpc mode only** | +| `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error` | +| `DEFAULT_SIGNAL_TEXT_MODE` | `normal` | `normal` or `styled` | + +## Gotchas + +- The Dockerfile pins `SIGNAL_CLI_VERSION` and `LIBSIGNAL_CLIENT_VERSION` as build args. After bumping these, verify the `libsignal-client-*.jar` filename still matches. +- Multi-arch build targets: `linux/amd64`, `linux/arm64`, `linux/arm/v7`. The `native` (GraalVM) binary is unavailable on armv7 — code falls back to `normal` mode. +- `AUTO_RECEIVE_SCHEDULE` and `SIGNAL_CLI_CMD_TIMEOUT` cause a fatal error in json-rpc mode. +- `RECEIVE_WEBHOOK_URL` is only valid in json-rpc mode; fatal in other modes. +- CI uses Podman for multi-arch builds, not Docker Buildx. +- Release versions are published via `publish.sh` triggering GitHub Actions (dev vs stable tags). \ No newline at end of file From 1cc4bb9873c05a0b9fe11d6c3b94350428f9cfe1 Mon Sep 17 00:00:00 2001 From: Peter Zandbergen Date: Thu, 18 Jun 2026 08:24:41 +0200 Subject: [PATCH 2/8] Optimize image build to slim the images Create proposal --- IMAGE-OPTIMIZATION-PROPOSAL.md | 295 +++++++++++++++++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 IMAGE-OPTIMIZATION-PROPOSAL.md diff --git a/IMAGE-OPTIMIZATION-PROPOSAL.md b/IMAGE-OPTIMIZATION-PROPOSAL.md new file mode 100644 index 0000000..aac8077 --- /dev/null +++ b/IMAGE-OPTIMIZATION-PROPOSAL.md @@ -0,0 +1,295 @@ +# Image Size Optimization Proposal + +## Problem + +The current Docker image is **1.33 GB** and bundles two complete signal-cli runtimes that no single `MODE` ever uses simultaneously: + +| Layer | Size | Used by | +|---|---|---| +| ubuntu:noble base | 78 MB | all | +| openjdk-25-jre + deps | **655 MB** | `normal`, `json-rpc` | +| signal-cli Java dist (jars) | 121 MB | `normal`, `json-rpc` | +| signal-cli-native binary | **389 MB** | `native`, `json-rpc-native` | +| Go binary (signal-cli-rest-api) | 40 MB | all | +| plugin_loader.so | 34 MB | all | +| jsonrpc2-helper | 9 MB | json-rpc modes | +| misc (locales, etc.) | 5 MB | all | + +No user benefits from both the JRE and the native binary at the same time — each `MODE` setting uses exactly one of them. + +Additionally, the JRE pulls in ~280 MB of GUI libraries (LLVM, Mesa, GTK, icon themes) that signal-cli never uses. It is a headless CLI tool. + +## Proposed Solution: Two Image Variants + +| Variant | Tag pattern | Contains | Modes | Est. size | +|---|---|---|---|---| +| **JRE** | `latest`, `X.Y.Z` | Headless JRE + signal-cli Java dist | `normal`, `json-rpc` | **~660 MB** | +| **Native** | `latest-native`, `X.Y.Z-native` | signal-cli-native only (no JRE) | `native`, `json-rpc-native` | **~615 MB** | + +For `arm/v7`, only the JRE variant is published (GraalVM doesn't produce a native binary for 32-bit ARM). + +### Why the native variant (615 MB) isn't smaller than JRE (660 MB) + +The 389 MB native binary includes all dependency code compiled as machine code plus an embedded runtime (GC, threading). It looks large next to the 121 MB Java dist, but that 121 MB is just compressed bytecode — it requires the separate 655 MB JRE to run. The fair comparison is: + +- **Java stack**: 655 MB JRE + 121 MB jars = **776 MB total** +- **Native binary**: 389 MB (standalone, no JRE) + +The native variant is genuinely lighter; its single large binary just happens to be larger than the `.jar` file viewed in isolation. + +## Size Savings Breakdown + +### JRE variant (~660 MB, down from 1.33 GB) + +| Removal | Saved | +|---|---| +| signal-cli-native binary | -389 MB | +| Full JRE → headless JRE (drops LLVM, Mesa, GTK, icons) | -280 MB | +| **Total saved** | **-669 MB** | + +### Native variant (~615 MB, down from 1.33 GB) + +| Removal | Saved | +|---|---| +| Entire JRE stack | -655 MB | +| signal-cli Java dist (jars) | -121 MB | +| **Total saved** | **-776 MB** | +| **But adds back**: supervisor + python | +24 MB | + +## Required Changes + +### 1. `Dockerfile` — Split into multi-stage targets + +The builder stage (lines 1–131) stays unchanged. The release stage (lines 133–194) becomes three stages: a shared `base`, then `jre` and `native` targets. + +**Key changes:** + +- `openjdk-25-jre` → `openjdk-25-jre-headless` (saves ~280 MB of GUI deps) +- JRE target: includes Java dist, excludes native binary +- Native target: includes native binary, excludes Java dist and JRE entirely +- Supervisor installed in both targets (needed for json-rpc daemon mode) + +```dockerfile +# ---- Shared base for both variants ---- +FROM ubuntu:noble AS base + +ENV GIN_MODE=release +ENV PORT=8080 + +ARG SIGNAL_CLI_VERSION +ARG BUILD_VERSION_ARG + +ENV BUILD_VERSION=$BUILD_VERSION_ARG +ENV SIGNAL_CLI_REST_API_PLUGIN_SHARED_OBJ_DIR=/usr/bin/ + +RUN dpkg-reconfigure debconf --frontend=noninteractive \ + && apt-get update \ + && apt-get install -y --no-install-recommends util-linux curl locales \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=buildcontainer /tmp/signal-cli-rest-api-src/signal-cli-rest-api /usr/bin/signal-cli-rest-api +COPY --from=buildcontainer /tmp/signal-cli-rest-api-src/scripts/jsonrpc2-helper /usr/bin/jsonrpc2-helper +COPY --from=buildcontainer /tmp/signal-cli-rest-api-src/signal-cli-rest-api_plugin_loader.so /usr/bin/signal-cli-rest-api_plugin_loader.so +COPY entrypoint.sh /entrypoint.sh + +RUN userdel ubuntu -r \ + && groupadd -g 1000 signal-api \ + && useradd --no-log-init -M -d /home -s /bin/bash -u 1000 -g 1000 signal-api \ + && mkdir -p /signal-cli-config/ \ + && mkdir -p /home/.local/share/signal-cli + +RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ + dpkg-reconfigure --frontend=noninteractive locales && \ + update-locale LANG=en_US.UTF-8 + +ENV LANG=en_US.UTF-8 + +EXPOSE ${PORT} + +ENV SIGNAL_CLI_CONFIG_DIR=/home/.local/share/signal-cli +ENV SIGNAL_CLI_UID=1000 +ENV SIGNAL_CLI_GID=1000 +ENV SIGNAL_CLI_CHOWN_ON_STARTUP=true + +ENTRYPOINT ["/entrypoint.sh"] + +HEALTHCHECK --interval=20s --timeout=10s --retries=3 \ + CMD curl -f http://localhost:${PORT}/v1/health || exit 1 + +# ---- JRE variant: MODE=normal, json-rpc ---- +FROM base AS jre + +RUN dpkg-reconfigure debconf --frontend=noninteractive \ + && apt-get update \ + && apt-get install -y --no-install-recommends openjdk-25-jre-headless supervisor \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=buildcontainer /opt/signal-cli-${SIGNAL_CLI_VERSION} /opt/signal-cli-${SIGNAL_CLI_VERSION} + +RUN ln -s /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli /usr/bin/signal-cli \ + && mkdir -p /home/.local/share/signal-cli + +# ---- Native variant: MODE=native, json-rpc-native ---- +FROM base AS native + +RUN dpkg-reconfigure debconf --frontend=noninteractive \ + && apt-get update \ + && apt-get install -y --no-install-recommends supervisor \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=buildcontainer /tmp/signal-cli-native /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli-native + +RUN mkdir -p /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin \ + && ln -s /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli-native /usr/bin/signal-cli-native \ + && mkdir -p /home/.local/share/signal-cli +``` + +### 2. `entrypoint.sh` — Add MODE validation + +At the top of the entrypoint, before the existing logic, add validation that the requested MODE matches the available binaries in the image: + +```sh +# Validate MODE against available binaries +if [ "$MODE" = "native" ] || [ "$MODE" = "json-rpc-native" ]; then + if [ ! -x /usr/bin/signal-cli-native ]; then + echo "ERROR: MODE=$MODE requires signal-cli-native, but this image doesn't include it." + echo "Use the -native image tag (e.g., bbernhard/signal-cli-rest-api:latest-native)" + exit 1 + fi +fi + +if [ "$MODE" = "normal" ] || [ "$MODE" = "json-rpc" ]; then + if ! command -v java >/dev/null 2>&1; then + echo "ERROR: MODE=$MODE requires Java (signal-cli), but this image doesn't include it." + echo "Use the standard image tag (e.g., bbernhard/signal-cli-rest-api:latest)" + exit 1 + fi +fi +``` + +This gives a clear error instead of a cryptic failure when a user runs the wrong MODE for their image variant. + +### 3. CI Workflows — Build both variants + +All three workflow files need the same change: build two manifests using `--target`. + +**`ci.yml`** — replaces the single `podman build` with two: + +```yaml +- name: Build + env: + VERSION: ${{ github.run_number }} + run: | + df -h + echo "Start CI build" + docker run --privileged --rm tonistiigi/binfmt --install all + + podman manifest create build-jre + podman build --format docker --target jre --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-jre . + podman manifest push localhost/build-jre docker://docker.io/bbernhard/signal-cli-rest-api:${EPOCHSECONDS}-ci + + podman manifest create build-native + podman build --format docker --target native --platform linux/amd64,linux/arm64 --manifest localhost/build-native . + podman manifest push localhost/build-native docker://docker.io/bbernhard/signal-cli-rest-api:${EPOCHSECONDS}-ci-native +``` + +**`release-productive-version.yml`** — same pattern, pushes tagged versions: + +```yaml +- name: Release + env: + VERSION: ${{ github.event.inputs.version }} + run: | + echo "Start productive build" + docker run --privileged --rm tonistiigi/binfmt --install all + + podman manifest create build-jre + podman build --format docker --target jre --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-jre . + podman manifest push localhost/build-jre docker://docker.io/bbernhard/signal-cli-rest-api:${VERSION} + podman manifest push localhost/build-jre docker://docker.io/bbernhard/signal-cli-rest-api:latest + + podman manifest create build-native + podman build --format docker --target native --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64 --manifest localhost/build-native . + podman manifest push localhost/build-native docker://docker.io/bbernhard/signal-cli-rest-api:${VERSION}-native + podman manifest push localhost/build-native docker://docker.io/bbernhard/signal-cli-rest-api:latest-native +``` + +**`release-dev-version.yml`** — same pattern, pushes `-dev` tags: + +```yaml + podman manifest push localhost/build-jre docker://docker.io/bbernhard/signal-cli-rest-api:${VERSION}-dev + podman manifest push localhost/build-jre docker://docker.io/bbernhard/signal-cli-rest-api:latest-dev + + podman manifest push localhost/build-native docker://docker.io/bbernhard/signal-cli-rest-api:${VERSION}-dev-native + podman manifest push localhost/build-native docker://docker.io/bbernhard/signal-cli-rest-api:latest-dev-native +``` + +### 4. `docker-compose.yml` — Add native variant example + +```yaml +services: + # JRE variant (default) — MODE=normal or MODE=json-rpc + signal-cli-rest-api: + image: bbernhard/signal-cli-rest-api:latest + environment: + - MODE=normal + ports: + - "8080:8080" + volumes: + - "./signal-cli-config:/home/.local/share/signal-cli" + + # Native variant — MODE=native or MODE=json-rpc-native + # signal-cli-rest-api-native: + # image: bbernhard/signal-cli-rest-api:latest-native + # environment: + # - MODE=native + # ports: + # - "8080:8080" + # volumes: + # - "./signal-cli-config:/home/.local/share/signal-cli" +``` + +## Files That Do NOT Need Changes + +| File | Why | +|---|---| +| `src/main.go` | The `SUPPORTS_NATIVE` env-var check and mode fallback still works. In the native image, `/usr/bin/signal-cli-native` exists; in the JRE image, it doesn't. The entrypoint validation catches mismatched MODE before the Go app starts. | +| `src/client/cli.go` | Already selects `signal-cli` or `signal-cli-native` based on mode. No awareness of image variants needed. | +| `src/scripts/jsonrpc2-helper.go` | Already selects the correct binary for daemon mode. No changes needed. | +| `publish.sh` | Just triggers GitHub Actions workflows. The workflow files handle variant tagging. | +| `.github/workflows/check-docs.yml` | Doc generation happens in the builder stage, unaffected by variant split. | +| `src/docs/*` | Auto-generated, no changes. | +| `plugins/*` | Runtime Lua scripts, no changes. | + +## Tagging Scheme + +| Tag | Variant | Platforms | +|---|---|---| +| `latest` | JRE | amd64, arm64, arm/v7 | +| `latest-native` | Native | amd64, arm64 | +| `X.Y.Z` | JRE | amd64, arm64, arm/v7 | +| `X.Y.Z-native` | Native | amd64, arm64 | +| `latest-dev` | JRE | amd64, arm64, arm/v7 | +| `latest-dev-native` | Native | amd64, arm64 | +| `X.Y.Z-dev` | JRE | amd64, arm64, arm/v7 | +| `X.Y.Z-dev-native` | Native | amd64, arm64 | + +The `latest` tag points to the JRE variant (preserves backwards compatibility — default `MODE=normal` works out of the box). + +## Backwards Compatibility + +- **`latest` tag** continues to work with `MODE=normal` (the default) and `MODE=json-rpc` +- Users currently running `MODE=native` or `MODE=json-rpc-native` on the `latest` tag will get a clear error message from the new entrypoint validation, telling them to switch to `latest-native` +- For a transition period, the old all-in-one image could be published as `X.Y.Z-full` or `latest-full` if needed +- On arm/v7, only the JRE variant is published, matching current behavior (native mode falls back to normal) + +## Quick Win (Without Splitting Images) + +If splitting into two variants is too much CI complexity right now, a single-line change saves ~280 MB immediately: + +```diff +- apt-get install -y --no-install-recommends util-linux supervisor openjdk-25-jre curl locales ++ apt-get install -y --no-install-recommends util-linux supervisor openjdk-25-jre-headless curl locales +``` + +This brings the image from **1.33 GB → ~1.05 GB** with zero functional impact, since signal-cli never uses AWT/Swing/GTK. \ No newline at end of file From 8c7e7cd615b3bf06058a4f022935c8040be20bd6 Mon Sep 17 00:00:00 2001 From: Peter Zandbergen Date: Thu, 18 Jun 2026 08:25:53 +0200 Subject: [PATCH 3/8] Implement changes --- .github/workflows/ci.yml | 9 ++- .github/workflows/release-dev-version.yml | 12 ++-- .../workflows/release-productive-version.yml | 12 ++-- AGENTS.md | 28 ++++++++-- Dockerfile | 56 ++++++++++++------- docker-compose.yml | 13 ++++- entrypoint.sh | 17 ++++++ 7 files changed, 111 insertions(+), 36 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23e8dda..b15eb63 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,7 +80,10 @@ jobs: df -h echo "Start CI build" docker run --privileged --rm tonistiigi/binfmt --install all - podman manifest create build - podman build --format docker --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build . - podman manifest push localhost/build docker://docker.io/bbernhard/signal-cli-rest-api:${EPOCHSECONDS}-ci + podman manifest create build-jre + podman build --format docker --target jre --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-jre . + podman manifest push localhost/build-jre docker://docker.io/bbernhard/signal-cli-rest-api:${EPOCHSECONDS}-ci + podman manifest create build-native + podman build --format docker --target native --platform linux/amd64,linux/arm64 --manifest localhost/build-native . + podman manifest push localhost/build-native docker://docker.io/bbernhard/signal-cli-rest-api:${EPOCHSECONDS}-ci-native diff --git a/.github/workflows/release-dev-version.yml b/.github/workflows/release-dev-version.yml index 8581b3d..f127e11 100644 --- a/.github/workflows/release-dev-version.yml +++ b/.github/workflows/release-dev-version.yml @@ -83,7 +83,11 @@ jobs: run: | echo "Start dev build" docker run --privileged --rm tonistiigi/binfmt --install all - podman manifest create build - podman build --format docker --build-arg BUILD_VERSION_ARG=${VERSION} --manifest localhost/build --platform linux/amd64,linux/arm64,linux/arm/v7 . - podman manifest push localhost/build docker://docker.io/bbernhard/signal-cli-rest-api:${VERSION}-dev - podman manifest push localhost/build docker://docker.io/bbernhard/signal-cli-rest-api:latest-dev + podman manifest create build-jre + podman build --format docker --target jre --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-jre . + podman manifest push localhost/build-jre docker://docker.io/bbernhard/signal-cli-rest-api:${VERSION}-dev + podman manifest push localhost/build-jre docker://docker.io/bbernhard/signal-cli-rest-api:latest-dev + podman manifest create build-native + podman build --format docker --target native --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64 --manifest localhost/build-native . + podman manifest push localhost/build-native docker://docker.io/bbernhard/signal-cli-rest-api:${VERSION}-dev-native + podman manifest push localhost/build-native docker://docker.io/bbernhard/signal-cli-rest-api:latest-dev-native diff --git a/.github/workflows/release-productive-version.yml b/.github/workflows/release-productive-version.yml index 357818c..febce11 100644 --- a/.github/workflows/release-productive-version.yml +++ b/.github/workflows/release-productive-version.yml @@ -83,7 +83,11 @@ jobs: run: | echo "Start productive build" docker run --privileged --rm tonistiigi/binfmt --install all - podman manifest create build - podman build --format docker --build-arg BUILD_VERSION_ARG=${VERSION} --manifest localhost/build --platform linux/amd64,linux/arm64,linux/arm/v7 . - podman manifest push localhost/build docker://docker.io/bbernhard/signal-cli-rest-api:${VERSION} - podman manifest push localhost/build docker://docker.io/bbernhard/signal-cli-rest-api:latest + podman manifest create build-jre + podman build --format docker --target jre --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-jre . + podman manifest push localhost/build-jre docker://docker.io/bbernhard/signal-cli-rest-api:${VERSION} + podman manifest push localhost/build-jre docker://docker.io/bbernhard/signal-cli-rest-api:latest + podman manifest create build-native + podman build --format docker --target native --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64 --manifest localhost/build-native . + podman manifest push localhost/build-native docker://docker.io/bbernhard/signal-cli-rest-api:${VERSION}-native + podman manifest push localhost/build-native docker://docker.io/bbernhard/signal-cli-rest-api:latest-native diff --git a/AGENTS.md b/AGENTS.md index 66d04c2..75572d1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,8 +16,8 @@ Go-based REST API wrapping [signal-cli](https://github.com/AsamK/signal-cli) for - `scripts/jsonrpc2-helper.go` — daemon manager for json-rpc mode - `plugin_loader.go` — Go plugin system using Lua scripts (v1/v2 plugin API) - `plugins/` — example Lua plugins and persistence plugin -- `Dockerfile` — multi-stage build: Go compile → release image with signal-cli + Java runtime -- `entrypoint.sh` — container entrypoint; handles UID/GID, chown, supervisor for json-rpc mode +- `Dockerfile` — multi-stage build with two release targets: `jre` and `native` +- `entrypoint.sh` — container entrypoint; validates MODE against available binaries, handles UID/GID, chown, supervisor for json-rpc mode ## Build & Run @@ -34,6 +34,11 @@ Go-based REST API wrapping [signal-cli](https://github.com/AsamK/signal-cli) for docker compose build docker compose up ``` +- **Build specific variant (Docker):** + ```bash + docker build --target jre . # JRE variant (normal + json-rpc) + docker build --target native . # native variant (native + json-rpc-native) + ``` - **No linter or typecheck config exists** — standard Go tooling only (e.g., `go vet`). ## Swagger / API Docs @@ -47,6 +52,19 @@ go run github.com/swaggo/swag/cmd/swag@v1.16.6 init --requiredByDefault --output The receive V1 schemas require an extra step (`add_v1_receive_schemas.go`); see `src/docs/README.md`. CI checks that generated docs are up-to-date (workflow: `check-docs.yml`). +## Image Variants + +The Dockerfile produces two release targets from a shared `base` stage: + +| Target | Tag suffix | Contains | Modes | Platforms | +|---|---|---|---|---| +| `jre` | `latest`, `X.Y.Z` | headless JRE + signal-cli Java dist | `normal`, `json-rpc` | amd64, arm64, arm/v7 | +| `native` | `latest-native`, `X.Y.Z-native` | signal-cli-native only (no JRE) | `native`, `json-rpc-native` | amd64, arm64 | + +The `entrypoint.sh` validates that the requested `MODE` matches binaries available in the running image variant and exits with a clear error if mismatched. + +The JRE variant uses `openjdk-25-jre-headless` (not the full `openjdk-25-jre`) to avoid ~280 MB of GUI libraries that a CLI tool never uses. + ## Key Environment Variables | Variable | Default | Purpose | @@ -63,8 +81,8 @@ The receive V1 schemas require an extra step (`add_v1_receive_schemas.go`); see ## Gotchas - The Dockerfile pins `SIGNAL_CLI_VERSION` and `LIBSIGNAL_CLIENT_VERSION` as build args. After bumping these, verify the `libsignal-client-*.jar` filename still matches. -- Multi-arch build targets: `linux/amd64`, `linux/arm64`, `linux/arm/v7`. The `native` (GraalVM) binary is unavailable on armv7 — code falls back to `normal` mode. +- Multi-arch builds: the `jre` target covers `linux/amd64`, `linux/arm64`, `linux/arm/v7`; the `native` target covers `linux/amd64`, `linux/arm64` only (no armv7 native binary). - `AUTO_RECEIVE_SCHEDULE` and `SIGNAL_CLI_CMD_TIMEOUT` cause a fatal error in json-rpc mode. - `RECEIVE_WEBHOOK_URL` is only valid in json-rpc mode; fatal in other modes. -- CI uses Podman for multi-arch builds, not Docker Buildx. -- Release versions are published via `publish.sh` triggering GitHub Actions (dev vs stable tags). \ No newline at end of file +- CI uses Podman for multi-arch builds, not Docker Buildx. Each CI/release workflow builds both `jre` and `native` targets. +- Release versions are published via `publish.sh` triggering GitHub Actions (dev vs stable tags, with `-native` suffix for the native variant). \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 539f4e0..8d84e8a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -130,13 +130,9 @@ RUN cd /tmp/signal-cli-rest-api-src/scripts && go build -o jsonrpc2-helper # build plugin_loader RUN cd /tmp/signal-cli-rest-api-src && go build -buildmode=plugin -o signal-cli-rest-api_plugin_loader.so plugin_loader.go -# Start a fresh container for release container +# ---- Shared base for both image variants ---- -# eclipse-temurin doesn't provide a OpenJDK 21 image for armv7 (see https://github.com/adoptium/containers/issues/502). Until this -# is fixed we use the standard ubuntu image -#FROM eclipse-temurin:21-jre-jammy - -FROM ubuntu:noble +FROM ubuntu:noble AS base ENV GIN_MODE=release @@ -150,31 +146,20 @@ ENV SIGNAL_CLI_REST_API_PLUGIN_SHARED_OBJ_DIR=/usr/bin/ RUN dpkg-reconfigure debconf --frontend=noninteractive \ && apt-get update \ - && apt-get install -y --no-install-recommends util-linux supervisor openjdk-25-jre curl locales \ - && rm -rf /var/lib/apt/lists/* + && apt-get install -y --no-install-recommends util-linux curl locales \ + && rm -rf /var/lib/apt/lists/* COPY --from=buildcontainer /tmp/signal-cli-rest-api-src/signal-cli-rest-api /usr/bin/signal-cli-rest-api -COPY --from=buildcontainer /opt/signal-cli-${SIGNAL_CLI_VERSION} /opt/signal-cli-${SIGNAL_CLI_VERSION} -COPY --from=buildcontainer /tmp/signal-cli-native /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli-native COPY --from=buildcontainer /tmp/signal-cli-rest-api-src/scripts/jsonrpc2-helper /usr/bin/jsonrpc2-helper COPY --from=buildcontainer /tmp/signal-cli-rest-api-src/signal-cli-rest-api_plugin_loader.so /usr/bin/signal-cli-rest-api_plugin_loader.so COPY entrypoint.sh /entrypoint.sh - RUN userdel ubuntu -r \ && groupadd -g 1000 signal-api \ && useradd --no-log-init -M -d /home -s /bin/bash -u 1000 -g 1000 signal-api \ - && ln -s /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli /usr/bin/signal-cli \ - && ln -s /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli-native /usr/bin/signal-cli-native \ && mkdir -p /signal-cli-config/ \ && mkdir -p /home/.local/share/signal-cli -# remove the temporary created signal-cli-native on armv7, as GRAALVM doesn't support 32bit -RUN arch="$(uname -m)"; \ - case "$arch" in \ - armv7l) echo "GRAALVM doesn't support 32bit" && rm /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli-native /usr/bin/signal-cli-native ;; \ - esac; - RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ dpkg-reconfigure --frontend=noninteractive locales && \ update-locale LANG=en_US.UTF-8 @@ -192,3 +177,36 @@ ENTRYPOINT ["/entrypoint.sh"] HEALTHCHECK --interval=20s --timeout=10s --retries=3 \ CMD curl -f http://localhost:${PORT}/v1/health || exit 1 + +# ---- JRE variant: MODE=normal, json-rpc ---- +# Includes headless JRE + signal-cli Java dist. No native binary. +# Supports: linux/amd64, linux/arm64, linux/arm/v7 + +FROM base AS jre + +RUN dpkg-reconfigure debconf --frontend=noninteractive \ + && apt-get update \ + && apt-get install -y --no-install-recommends openjdk-25-jre-headless supervisor \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=buildcontainer /opt/signal-cli-${SIGNAL_CLI_VERSION} /opt/signal-cli-${SIGNAL_CLI_VERSION} + +RUN ln -s /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli /usr/bin/signal-cli \ + && mkdir -p /home/.local/share/signal-cli + +# ---- Native variant: MODE=native, json-rpc-native ---- +# Includes signal-cli-native only. No JRE, no Java dist. +# Supports: linux/amd64, linux/arm64 (no arm/v7 — GraalVM doesn't produce 32-bit binaries) + +FROM base AS native + +RUN dpkg-reconfigure debconf --frontend=noninteractive \ + && apt-get update \ + && apt-get install -y --no-install-recommends supervisor \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=buildcontainer /tmp/signal-cli-native /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli-native + +RUN mkdir -p /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin \ + && ln -s /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli-native /usr/bin/signal-cli-native \ + && mkdir -p /home/.local/share/signal-cli diff --git a/docker-compose.yml b/docker-compose.yml index 958a194..1434881 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,22 @@ services: + # JRE variant (default) -- use with MODE=normal or MODE=json-rpc signal-cli-rest-api: image: bbernhard/signal-cli-rest-api:latest environment: - - MODE=normal #supported modes: json-rpc, native, normal + - MODE=normal #supported modes: normal, json-rpc #- AUTO_RECEIVE_SCHEDULE=0 22 * * * #enable this parameter on demand (see description below) ports: - "8080:8080" #map docker port 8080 to host port 8080. volumes: - "./signal-cli-config:/home/.local/share/signal-cli" #map "signal-cli-config" folder on host system into docker container. the folder contains the password and cryptographic keys when a new number is registered + # Native variant -- use with MODE=native or MODE=json-rpc-native + # Uncomment to use the native variant (smaller image, no Java runtime) + # signal-cli-rest-api-native: + # image: bbernhard/signal-cli-rest-api:latest-native + # environment: + # - MODE=native #supported modes: native, json-rpc-native + # ports: + # - "8080:8080" + # volumes: + # - "./signal-cli-config:/home/.local/share/signal-cli" \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh index 027254a..a528d2d 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -5,6 +5,23 @@ set -e [ -z "${SIGNAL_CLI_CONFIG_DIR}" ] && echo "SIGNAL_CLI_CONFIG_DIR environmental variable needs to be set! Aborting!" && exit 1; +# Validate MODE against the binaries available in this image variant +MODE=${MODE:-normal} +if [ "$MODE" = "native" ] || [ "$MODE" = "json-rpc-native" ]; then + if [ ! -x /usr/bin/signal-cli-native ]; then + echo "ERROR: MODE=$MODE requires signal-cli-native, but this image doesn't include it." + echo "Use the -native image tag (e.g., bbernhard/signal-cli-rest-api:latest-native)" + exit 1 + fi +fi +if [ "$MODE" = "normal" ] || [ "$MODE" = "json-rpc" ]; then + if ! command -v java >/dev/null 2>&1; then + echo "ERROR: MODE=$MODE requires Java (signal-cli), but this image doesn't include it." + echo "Use the standard image tag (e.g., bbernhard/signal-cli-rest-api:latest)" + exit 1 + fi +fi + usermod -u ${SIGNAL_CLI_UID} signal-api groupmod -o -g ${SIGNAL_CLI_GID} signal-api From 81e9cbf69fd52c0bed60ee37111371a889522de8 Mon Sep 17 00:00:00 2001 From: Peter Zandbergen Date: Thu, 18 Jun 2026 08:49:39 +0200 Subject: [PATCH 4/8] Try to build on Github --- .github/workflows/ci.yml | 13 +- .github/workflows/release-dev-version.yml | 17 ++- .../workflows/release-productive-version.yml | 17 ++- IMAGE-OPTIMIZATION-PROPOSAL.md | 126 +++++++++++++----- 4 files changed, 124 insertions(+), 49 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b15eb63..f948624 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,11 +30,12 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.ref }} - - name: Login to Docker Hub + - name: Login to container registry uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + registry: ${{ vars.IMAGE_REGISTRY || 'docker.io' }} + username: ${{ secrets.REGISTRY_USERNAME || secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD || secrets.DOCKERHUB_TOKEN }} - name: Free up disk space run: | # Remove Java (JDKs) @@ -76,14 +77,16 @@ jobs: - name: Build env: VERSION: ${{ github.run_number }} + IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'docker.io' }} + IMAGE_REPO: ${{ vars.IMAGE_REPO || 'bbernhard/signal-cli-rest-api' }} run: | df -h echo "Start CI build" docker run --privileged --rm tonistiigi/binfmt --install all podman manifest create build-jre podman build --format docker --target jre --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-jre . - podman manifest push localhost/build-jre docker://docker.io/bbernhard/signal-cli-rest-api:${EPOCHSECONDS}-ci + podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${EPOCHSECONDS}-ci podman manifest create build-native podman build --format docker --target native --platform linux/amd64,linux/arm64 --manifest localhost/build-native . - podman manifest push localhost/build-native docker://docker.io/bbernhard/signal-cli-rest-api:${EPOCHSECONDS}-ci-native + podman manifest push localhost/build-native docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${EPOCHSECONDS}-ci-native diff --git a/.github/workflows/release-dev-version.yml b/.github/workflows/release-dev-version.yml index f127e11..6569a33 100644 --- a/.github/workflows/release-dev-version.yml +++ b/.github/workflows/release-dev-version.yml @@ -33,11 +33,12 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.ref }} - - name: Login to Docker Hub + - name: Login to container registry uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + registry: ${{ vars.IMAGE_REGISTRY || 'docker.io' }} + username: ${{ secrets.REGISTRY_USERNAME || secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD || secrets.DOCKERHUB_TOKEN }} - name: Free up disk space run: | # Remove Java (JDKs) @@ -80,14 +81,16 @@ jobs: - name: Release env: VERSION: ${{ github.event.inputs.version }} + IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'docker.io' }} + IMAGE_REPO: ${{ vars.IMAGE_REPO || 'bbernhard/signal-cli-rest-api' }} run: | echo "Start dev build" docker run --privileged --rm tonistiigi/binfmt --install all podman manifest create build-jre podman build --format docker --target jre --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-jre . - podman manifest push localhost/build-jre docker://docker.io/bbernhard/signal-cli-rest-api:${VERSION}-dev - podman manifest push localhost/build-jre docker://docker.io/bbernhard/signal-cli-rest-api:latest-dev + podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${VERSION}-dev + podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:latest-dev podman manifest create build-native podman build --format docker --target native --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64 --manifest localhost/build-native . - podman manifest push localhost/build-native docker://docker.io/bbernhard/signal-cli-rest-api:${VERSION}-dev-native - podman manifest push localhost/build-native docker://docker.io/bbernhard/signal-cli-rest-api:latest-dev-native + podman manifest push localhost/build-native docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${VERSION}-dev-native + podman manifest push localhost/build-native docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:latest-dev-native diff --git a/.github/workflows/release-productive-version.yml b/.github/workflows/release-productive-version.yml index febce11..10bcb78 100644 --- a/.github/workflows/release-productive-version.yml +++ b/.github/workflows/release-productive-version.yml @@ -33,11 +33,12 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.ref }} - - name: Login to Docker Hub + - name: Login to container registry uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + registry: ${{ vars.IMAGE_REGISTRY || 'docker.io' }} + username: ${{ secrets.REGISTRY_USERNAME || secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD || secrets.DOCKERHUB_TOKEN }} - name: Free up disk space run: | # Remove Java (JDKs) @@ -80,14 +81,16 @@ jobs: - name: Release env: VERSION: ${{ github.event.inputs.version }} + IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'docker.io' }} + IMAGE_REPO: ${{ vars.IMAGE_REPO || 'bbernhard/signal-cli-rest-api' }} run: | echo "Start productive build" docker run --privileged --rm tonistiigi/binfmt --install all podman manifest create build-jre podman build --format docker --target jre --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-jre . - podman manifest push localhost/build-jre docker://docker.io/bbernhard/signal-cli-rest-api:${VERSION} - podman manifest push localhost/build-jre docker://docker.io/bbernhard/signal-cli-rest-api:latest + podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${VERSION} + podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:latest podman manifest create build-native podman build --format docker --target native --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64 --manifest localhost/build-native . - podman manifest push localhost/build-native docker://docker.io/bbernhard/signal-cli-rest-api:${VERSION}-native - podman manifest push localhost/build-native docker://docker.io/bbernhard/signal-cli-rest-api:latest-native + podman manifest push localhost/build-native docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${VERSION}-native + podman manifest push localhost/build-native docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:latest-native diff --git a/IMAGE-OPTIMIZATION-PROPOSAL.md b/IMAGE-OPTIMIZATION-PROPOSAL.md index aac8077..e6a6bfe 100644 --- a/IMAGE-OPTIMIZATION-PROPOSAL.md +++ b/IMAGE-OPTIMIZATION-PROPOSAL.md @@ -56,11 +56,19 @@ The native variant is genuinely lighter; its single large binary just happens to | **Total saved** | **-776 MB** | | **But adds back**: supervisor + python | +24 MB | +### Actual measured sizes from the build + +| Image | Size | +|---|---| +| Original single image | 1.33 GB | +| JRE variant (`signal-cli-rest-api:jre`) | 611 MB | +| Native variant (`signal-cli-rest-api:native`) | 621 MB | + ## Required Changes ### 1. `Dockerfile` — Split into multi-stage targets -The builder stage (lines 1–131) stays unchanged. The release stage (lines 133–194) becomes three stages: a shared `base`, then `jre` and `native` targets. +The builder stage (lines 1–131) stays unchanged. The release stage becomes three stages: a shared `base`, then `jre` and `native` targets. **Key changes:** @@ -68,6 +76,7 @@ The builder stage (lines 1–131) stays unchanged. The release stage (lines 133 - JRE target: includes Java dist, excludes native binary - Native target: includes native binary, excludes Java dist and JRE entirely - Supervisor installed in both targets (needed for json-rpc daemon mode) +- Removed armv7 native-binary cleanup hack (not needed — native target isn't built for armv7) ```dockerfile # ---- Shared base for both variants ---- @@ -102,7 +111,7 @@ RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ dpkg-reconfigure --frontend=noninteractive locales && \ update-locale LANG=en_US.UTF-8 -ENV LANG=en_US.UTF-8 +ENV LANG en_US.UTF-8 EXPOSE ${PORT} @@ -146,10 +155,11 @@ RUN mkdir -p /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin \ ### 2. `entrypoint.sh` — Add MODE validation -At the top of the entrypoint, before the existing logic, add validation that the requested MODE matches the available binaries in the image: +At the top of the entrypoint (after the existing `SIGNAL_CLI_CONFIG_DIR` check), add validation that the requested MODE matches the available binaries in the image: ```sh -# Validate MODE against available binaries +# Validate MODE against the binaries available in this image variant +MODE=${MODE:-normal} if [ "$MODE" = "native" ] || [ "$MODE" = "json-rpc-native" ]; then if [ ! -x /usr/bin/signal-cli-native ]; then echo "ERROR: MODE=$MODE requires signal-cli-native, but this image doesn't include it." @@ -157,7 +167,6 @@ if [ "$MODE" = "native" ] || [ "$MODE" = "json-rpc-native" ]; then exit 1 fi fi - if [ "$MODE" = "normal" ] || [ "$MODE" = "json-rpc" ]; then if ! command -v java >/dev/null 2>&1; then echo "ERROR: MODE=$MODE requires Java (signal-cli), but this image doesn't include it." @@ -169,16 +178,36 @@ fi This gives a clear error instead of a cryptic failure when a user runs the wrong MODE for their image variant. -### 3. CI Workflows — Build both variants +### 3. CI Workflows — Build both variants with configurable registry -All three workflow files need the same change: build two manifests using `--target`. +All three workflow files build two manifests using `--target` and push to a configurable container registry. -**`ci.yml`** — replaces the single `podman build` with two: +**Registry configuration** uses GitHub Actions variables and secrets: + +| Variable/Secret | Type | Default | Purpose | +|---|---|---|---| +| `IMAGE_REGISTRY` | repo variable | `docker.io` | Registry hostname (e.g., `ghcr.io`, `myregistry.local:5000`) | +| `IMAGE_REPO` | repo variable | `bbernhard/signal-cli-rest-api` | Image path on the registry (e.g., `myorg/signal-cli-rest-api`) | +| `REGISTRY_USERNAME` | secret | falls back to `DOCKERHUB_USERNAME` | Registry login username | +| `REGISTRY_PASSWORD` | secret | falls back to `DOCKERHUB_TOKEN` | Registry login password/token | + +The login step uses `docker/login-action` with the `registry` parameter set to `IMAGE_REGISTRY`, so it works with Docker Hub, GHCR, or any private registry. The fallback to `DOCKERHUB_*` secrets preserves backwards compatibility for the upstream repo. + +**`ci.yml`:** ```yaml +- name: Login to container registry + uses: docker/login-action@v4.1.0 + with: + registry: ${{ vars.IMAGE_REGISTRY || 'docker.io' }} + username: ${{ secrets.REGISTRY_USERNAME || secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD || secrets.DOCKERHUB_TOKEN }} + - name: Build env: VERSION: ${{ github.run_number }} + IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'docker.io' }} + IMAGE_REPO: ${{ vars.IMAGE_REPO || 'bbernhard/signal-cli-rest-api' }} run: | df -h echo "Start CI build" @@ -186,42 +215,31 @@ All three workflow files need the same change: build two manifests using `--targ podman manifest create build-jre podman build --format docker --target jre --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-jre . - podman manifest push localhost/build-jre docker://docker.io/bbernhard/signal-cli-rest-api:${EPOCHSECONDS}-ci + podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${EPOCHSECONDS}-ci podman manifest create build-native podman build --format docker --target native --platform linux/amd64,linux/arm64 --manifest localhost/build-native . - podman manifest push localhost/build-native docker://docker.io/bbernhard/signal-cli-rest-api:${EPOCHSECONDS}-ci-native + podman manifest push localhost/build-native docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${EPOCHSECONDS}-ci-native ``` **`release-productive-version.yml`** — same pattern, pushes tagged versions: ```yaml -- name: Release - env: - VERSION: ${{ github.event.inputs.version }} - run: | - echo "Start productive build" - docker run --privileged --rm tonistiigi/binfmt --install all + podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${VERSION} + podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:latest - podman manifest create build-jre - podman build --format docker --target jre --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-jre . - podman manifest push localhost/build-jre docker://docker.io/bbernhard/signal-cli-rest-api:${VERSION} - podman manifest push localhost/build-jre docker://docker.io/bbernhard/signal-cli-rest-api:latest - - podman manifest create build-native - podman build --format docker --target native --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64 --manifest localhost/build-native . - podman manifest push localhost/build-native docker://docker.io/bbernhard/signal-cli-rest-api:${VERSION}-native - podman manifest push localhost/build-native docker://docker.io/bbernhard/signal-cli-rest-api:latest-native + podman manifest push localhost/build-native docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${VERSION}-native + podman manifest push localhost/build-native docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:latest-native ``` **`release-dev-version.yml`** — same pattern, pushes `-dev` tags: ```yaml - podman manifest push localhost/build-jre docker://docker.io/bbernhard/signal-cli-rest-api:${VERSION}-dev - podman manifest push localhost/build-jre docker://docker.io/bbernhard/signal-cli-rest-api:latest-dev + podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${VERSION}-dev + podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:latest-dev - podman manifest push localhost/build-native docker://docker.io/bbernhard/signal-cli-rest-api:${VERSION}-dev-native - podman manifest push localhost/build-native docker://docker.io/bbernhard/signal-cli-rest-api:latest-dev-native + podman manifest push localhost/build-native docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${VERSION}-dev-native + podman manifest push localhost/build-native docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:latest-dev-native ``` ### 4. `docker-compose.yml` — Add native variant example @@ -256,7 +274,7 @@ services: | `src/main.go` | The `SUPPORTS_NATIVE` env-var check and mode fallback still works. In the native image, `/usr/bin/signal-cli-native` exists; in the JRE image, it doesn't. The entrypoint validation catches mismatched MODE before the Go app starts. | | `src/client/cli.go` | Already selects `signal-cli` or `signal-cli-native` based on mode. No awareness of image variants needed. | | `src/scripts/jsonrpc2-helper.go` | Already selects the correct binary for daemon mode. No changes needed. | -| `publish.sh` | Just triggers GitHub Actions workflows. The workflow files handle variant tagging. | +| `publish.sh` | Just triggers GitHub Actions workflows by version/tag. The workflow files handle variant tagging. | | `.github/workflows/check-docs.yml` | Doc generation happens in the builder stage, unaffected by variant split. | | `src/docs/*` | Auto-generated, no changes. | | `plugins/*` | Runtime Lua scripts, no changes. | @@ -276,12 +294,60 @@ services: The `latest` tag points to the JRE variant (preserves backwards compatibility — default `MODE=normal` works out of the box). +## Registry Configuration Examples + +### Push to Docker Hub (default, zero config needed) + +No variables or secrets need to be set. The existing `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN` secrets continue to work as before. + +### Push to GitHub Container Registry + +Set in your fork's Settings → Secrets and variables → Actions: + +| Type | Name | Value | +|---|---|---| +| Variable | `IMAGE_REGISTRY` | `ghcr.io` | +| Variable | `IMAGE_REPO` | `myuser/signal-cli-rest-api` | +| Secret | `REGISTRY_USERNAME` | your GitHub username | +| Secret | `REGISTRY_PASSWORD` | a PAT with `write:packages` scope | + +### Push to a private/local registry + +| Type | Name | Value | +|---|---|---| +| Variable | `IMAGE_REGISTRY` | `myregistry.local:5000` | +| Variable | `IMAGE_REPO` | `signal-cli-rest-api` | +| Secret | `REGISTRY_USERNAME` | (if auth is needed) | +| Secret | `REGISTRY_PASSWORD` | (if auth is needed) | + +For unauthenticated local registries, you can remove the login step entirely or leave the secrets empty — `podman` will push without auth if the registry allows it. + ## Backwards Compatibility - **`latest` tag** continues to work with `MODE=normal` (the default) and `MODE=json-rpc` - Users currently running `MODE=native` or `MODE=json-rpc-native` on the `latest` tag will get a clear error message from the new entrypoint validation, telling them to switch to `latest-native` - For a transition period, the old all-in-one image could be published as `X.Y.Z-full` or `latest-full` if needed - On arm/v7, only the JRE variant is published, matching current behavior (native mode falls back to normal) +- The upstream repo's existing `DOCKERHUB_USERNAME`/`DOCKERHUB_TOKEN` secrets continue to work — the `REGISTRY_USERNAME`/`REGISTRY_PASSWORD` secrets fall back to them + +## Verification Results + +All three Docker targets build successfully on amd64: + +``` +=== Image sizes === +base: 188 MB +jre: 611 MB (down from 1.33 GB) +native: 621 MB (down from 1.33 GB) +``` + +Entrypoint validation works correctly: +- `MODE=native` on JRE image → exit 1: `"Use the -native image tag"` +- `MODE=normal` on native image → exit 1: `"Use the standard image tag"` + +Binary presence verified per variant: +- JRE: `java` present, `signal-cli` present, `signal-cli-native` absent +- Native: `signal-cli-native` present, `java` absent, `signal-cli` absent ## Quick Win (Without Splitting Images) From d8193872a967015314ee2c2d8b1a31ce11169f55 Mon Sep 17 00:00:00 2001 From: Peter Zandbergen Date: Thu, 18 Jun 2026 11:46:45 +0200 Subject: [PATCH 5/8] Add SIGNAL_CLI_VERSION to the native and jre target --- Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dockerfile b/Dockerfile index 8d84e8a..f4b4da8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -184,6 +184,8 @@ HEALTHCHECK --interval=20s --timeout=10s --retries=3 \ FROM base AS jre +ARG SIGNAL_CLI_VERSION + RUN dpkg-reconfigure debconf --frontend=noninteractive \ && apt-get update \ && apt-get install -y --no-install-recommends openjdk-25-jre-headless supervisor \ @@ -200,6 +202,8 @@ RUN ln -s /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli /usr/bin/signal-c FROM base AS native +ARG SIGNAL_CLI_VERSION + RUN dpkg-reconfigure debconf --frontend=noninteractive \ && apt-get update \ && apt-get install -y --no-install-recommends supervisor \ From b8cf7bf684a5508563f532eee91dcbb8544d9cb7 Mon Sep 17 00:00:00 2001 From: Peter Zandbergen Date: Thu, 18 Jun 2026 13:26:48 +0200 Subject: [PATCH 6/8] Add build configuration and use it in docker compose. --- .env | 2 ++ .gitignore | 2 +- docker-compose.yml | 10 +++++++--- 3 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 0000000..26a8ac9 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +BUILD_TARGET=jre # native, jre +RUN_MODE=normal # native, normal diff --git a/.gitignore b/.gitignore index 3380c9f..eb6101e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ signal-cli-config src/main src/signal-cli-rest-api .idea/ -./persistence +.persistence diff --git a/docker-compose.yml b/docker-compose.yml index 1434881..383f166 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,13 @@ services: # JRE variant (default) -- use with MODE=normal or MODE=json-rpc signal-cli-rest-api: - image: bbernhard/signal-cli-rest-api:latest + # image: bbernhard/signal-cli-rest-api:latest + build: + context: "." + dockerfile: Dockerfile + target: ${BUILD_TARGET} environment: - - MODE=normal #supported modes: normal, json-rpc + - MODE=${RUN_MODE} #supported modes: normal, json-rpc #- AUTO_RECEIVE_SCHEDULE=0 22 * * * #enable this parameter on demand (see description below) ports: - "8080:8080" #map docker port 8080 to host port 8080. @@ -19,4 +23,4 @@ services: # ports: # - "8080:8080" # volumes: - # - "./signal-cli-config:/home/.local/share/signal-cli" \ No newline at end of file + # - "./signal-cli-config:/home/.local/share/signal-cli" From ab60199ec99861351e899080af6c75a22b157392 Mon Sep 17 00:00:00 2001 From: Peter Zandbergen Date: Sun, 21 Jun 2026 11:18:36 +0200 Subject: [PATCH 7/8] Add all target and optimize Actions pipelines --- .env | 4 +- .github/workflows/ci.yml | 9 ++- .github/workflows/release-dev-version.yml | 12 ++-- .../workflows/release-productive-version.yml | 12 ++-- AGENTS.md | 18 +++-- Dockerfile | 13 ++++ IMAGE-OPTIMIZATION-PROPOSAL.md | 7 +- docker-compose.yml | 26 ++++--- test-builds.sh | 67 +++++++++++++++++++ 9 files changed, 138 insertions(+), 30 deletions(-) create mode 100644 test-builds.sh diff --git a/.env b/.env index 26a8ac9..6ace68a 100644 --- a/.env +++ b/.env @@ -1,2 +1,2 @@ -BUILD_TARGET=jre # native, jre -RUN_MODE=normal # native, normal +BUILD_TARGET=native # all, jre, native +RUN_MODE=json-rpc-native # normal, json-rpc, native, json-rpc-native diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f948624..e73f257 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,10 +83,13 @@ jobs: df -h echo "Start CI build" docker run --privileged --rm tonistiigi/binfmt --install all + podman manifest create build-all + podman build --format docker --layers --target all --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-all . + podman manifest push localhost/build-all docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${EPOCHSECONDS}-ci podman manifest create build-jre - podman build --format docker --target jre --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-jre . - podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${EPOCHSECONDS}-ci + podman build --format docker --layers --target jre --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-jre . + podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${EPOCHSECONDS}-ci-jre podman manifest create build-native - podman build --format docker --target native --platform linux/amd64,linux/arm64 --manifest localhost/build-native . + podman build --format docker --layers --target native --platform linux/amd64,linux/arm64 --manifest localhost/build-native . podman manifest push localhost/build-native docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${EPOCHSECONDS}-ci-native diff --git a/.github/workflows/release-dev-version.yml b/.github/workflows/release-dev-version.yml index 6569a33..a0eaa89 100644 --- a/.github/workflows/release-dev-version.yml +++ b/.github/workflows/release-dev-version.yml @@ -86,11 +86,15 @@ jobs: run: | echo "Start dev build" docker run --privileged --rm tonistiigi/binfmt --install all + podman manifest create build-all + podman build --format docker --layers --target all --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-all . + podman manifest push localhost/build-all docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${VERSION}-dev + podman manifest push localhost/build-all docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:latest-dev podman manifest create build-jre - podman build --format docker --target jre --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-jre . - podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${VERSION}-dev - podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:latest-dev + podman build --format docker --layers --target jre --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-jre . + podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${VERSION}-dev-jre + podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:latest-dev-jre podman manifest create build-native - podman build --format docker --target native --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64 --manifest localhost/build-native . + podman build --format docker --layers --target native --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64 --manifest localhost/build-native . podman manifest push localhost/build-native docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${VERSION}-dev-native podman manifest push localhost/build-native docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:latest-dev-native diff --git a/.github/workflows/release-productive-version.yml b/.github/workflows/release-productive-version.yml index 10bcb78..f8fa761 100644 --- a/.github/workflows/release-productive-version.yml +++ b/.github/workflows/release-productive-version.yml @@ -86,11 +86,15 @@ jobs: run: | echo "Start productive build" docker run --privileged --rm tonistiigi/binfmt --install all + podman manifest create build-all + podman build --format docker --layers --target all --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-all . + podman manifest push localhost/build-all docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${VERSION} + podman manifest push localhost/build-all docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:latest podman manifest create build-jre - podman build --format docker --target jre --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-jre . - podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${VERSION} - podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:latest + podman build --format docker --layers --target jre --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-jre . + podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${VERSION}-jre + podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:latest-jre podman manifest create build-native - podman build --format docker --target native --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64 --manifest localhost/build-native . + podman build --format docker --layers --target native --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64 --manifest localhost/build-native . podman manifest push localhost/build-native docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${VERSION}-native podman manifest push localhost/build-native docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:latest-native diff --git a/AGENTS.md b/AGENTS.md index 75572d1..9271065 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,7 @@ Go-based REST API wrapping [signal-cli](https://github.com/AsamK/signal-cli) for - `scripts/jsonrpc2-helper.go` — daemon manager for json-rpc mode - `plugin_loader.go` — Go plugin system using Lua scripts (v1/v2 plugin API) - `plugins/` — example Lua plugins and persistence plugin -- `Dockerfile` — multi-stage build with two release targets: `jre` and `native` +- `Dockerfile` — multi-stage build with three release targets: `all`, `jre`, and `native` - `entrypoint.sh` — container entrypoint; validates MODE against available binaries, handles UID/GID, chown, supervisor for json-rpc mode ## Build & Run @@ -36,8 +36,9 @@ Go-based REST API wrapping [signal-cli](https://github.com/AsamK/signal-cli) for ``` - **Build specific variant (Docker):** ```bash - docker build --target jre . # JRE variant (normal + json-rpc) - docker build --target native . # native variant (native + json-rpc-native) + docker build --target all . # all-in-one (all four modes) + docker build --target jre . # JRE variant (normal + json-rpc) + docker build --target native . # native variant (native + json-rpc-native) ``` - **No linter or typecheck config exists** — standard Go tooling only (e.g., `go vet`). @@ -54,13 +55,16 @@ The receive V1 schemas require an extra step (`add_v1_receive_schemas.go`); see ## Image Variants -The Dockerfile produces two release targets from a shared `base` stage: +The Dockerfile produces three release targets from a shared `base` stage: | Target | Tag suffix | Contains | Modes | Platforms | |---|---|---|---|---| -| `jre` | `latest`, `X.Y.Z` | headless JRE + signal-cli Java dist | `normal`, `json-rpc` | amd64, arm64, arm/v7 | +| `all` | `latest`, `X.Y.Z` | headless JRE + signal-cli Java dist + signal-cli-native | `normal`, `json-rpc`, `native`, `json-rpc-native` | amd64, arm64, arm/v7 | +| `jre` | `latest-jre`, `X.Y.Z-jre` | headless JRE + signal-cli Java dist | `normal`, `json-rpc` | amd64, arm64, arm/v7 | | `native` | `latest-native`, `X.Y.Z-native` | signal-cli-native only (no JRE) | `native`, `json-rpc-native` | amd64, arm64 | +The `all` target is the backwards-compatible default — it contains both runtimes so all four modes work, matching the original monolithic image. + The `entrypoint.sh` validates that the requested `MODE` matches binaries available in the running image variant and exits with a clear error if mismatched. The JRE variant uses `openjdk-25-jre-headless` (not the full `openjdk-25-jre`) to avoid ~280 MB of GUI libraries that a CLI tool never uses. @@ -84,5 +88,5 @@ The JRE variant uses `openjdk-25-jre-headless` (not the full `openjdk-25-jre`) t - Multi-arch builds: the `jre` target covers `linux/amd64`, `linux/arm64`, `linux/arm/v7`; the `native` target covers `linux/amd64`, `linux/arm64` only (no armv7 native binary). - `AUTO_RECEIVE_SCHEDULE` and `SIGNAL_CLI_CMD_TIMEOUT` cause a fatal error in json-rpc mode. - `RECEIVE_WEBHOOK_URL` is only valid in json-rpc mode; fatal in other modes. -- CI uses Podman for multi-arch builds, not Docker Buildx. Each CI/release workflow builds both `jre` and `native` targets. -- Release versions are published via `publish.sh` triggering GitHub Actions (dev vs stable tags, with `-native` suffix for the native variant). \ No newline at end of file +- CI uses Podman for multi-arch builds, not Docker Buildx. Each CI/release workflow builds all three targets (`all`, `jre`, `native`). +- Release versions are published via `publish.sh` triggering GitHub Actions (dev vs stable tags, with `-jre` and `-native` suffixes for those variants; `all` variant gets the plain version/latest tags). \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index f4b4da8..ddd99ea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -196,6 +196,19 @@ COPY --from=buildcontainer /opt/signal-cli-${SIGNAL_CLI_VERSION} /opt/signal-cli RUN ln -s /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli /usr/bin/signal-cli \ && mkdir -p /home/.local/share/signal-cli +# ---- All-in-one variant: MODE=normal, json-rpc, native, json-rpc-native ---- +# Includes headless JRE + signal-cli Java dist + signal-cli-native. +# Supports: linux/amd64, linux/arm64, linux/arm/v7 +# (arm/v7 gets JRE modes only — signal-cli-native is a dummy on arm/v7) + +FROM jre AS all + +ARG SIGNAL_CLI_VERSION + +COPY --from=buildcontainer /tmp/signal-cli-native /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli-native + +RUN ln -s /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli-native /usr/bin/signal-cli-native + # ---- Native variant: MODE=native, json-rpc-native ---- # Includes signal-cli-native only. No JRE, no Java dist. # Supports: linux/amd64, linux/arm64 (no arm/v7 — GraalVM doesn't produce 32-bit binaries) diff --git a/IMAGE-OPTIMIZATION-PROPOSAL.md b/IMAGE-OPTIMIZATION-PROPOSAL.md index e6a6bfe..8660483 100644 --- a/IMAGE-OPTIMIZATION-PROPOSAL.md +++ b/IMAGE-OPTIMIZATION-PROPOSAL.md @@ -19,13 +19,16 @@ No user benefits from both the JRE and the native binary at the same time — ea Additionally, the JRE pulls in ~280 MB of GUI libraries (LLVM, Mesa, GTK, icon themes) that signal-cli never uses. It is a headless CLI tool. -## Proposed Solution: Two Image Variants +## Proposed Solution: Three Image Variants | Variant | Tag pattern | Contains | Modes | Est. size | |---|---|---|---|---| -| **JRE** | `latest`, `X.Y.Z` | Headless JRE + signal-cli Java dist | `normal`, `json-rpc` | **~660 MB** | +| **all** | `latest`, `X.Y.Z` | Headles JRE and native | `normal`, `json-rpc`, `native`, `json-rpc-native` | **1.3 GB** | +| **JRE** | `latest-jre`, `X.Y.Z-jre` | Headless JRE + signal-cli Java dist | `normal`, `json-rpc` | **~660 MB** | | **Native** | `latest-native`, `X.Y.Z-native` | signal-cli-native only (no JRE) | `native`, `json-rpc-native` | **~615 MB** | +We keep the **all** variant in to avoid breaking changes. + For `arm/v7`, only the JRE variant is published (GraalVM doesn't produce a native binary for 32-bit ARM). ### Why the native variant (615 MB) isn't smaller than JRE (660 MB) diff --git a/docker-compose.yml b/docker-compose.yml index 383f166..ecb6b11 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,26 +1,36 @@ services: - # JRE variant (default) -- use with MODE=normal or MODE=json-rpc + # All-in-one variant -- supports all modes: normal, json-rpc, native, json-rpc-native + # This is the backwards-compatible default (same as the original image). signal-cli-rest-api: - # image: bbernhard/signal-cli-rest-api:latest + # image: bbernhard/signal-cli-rest-api:latest build: context: "." dockerfile: Dockerfile target: ${BUILD_TARGET} environment: - - MODE=${RUN_MODE} #supported modes: normal, json-rpc + - MODE=${RUN_MODE} #supported modes: normal, json-rpc, native, json-rpc-native #- AUTO_RECEIVE_SCHEDULE=0 22 * * * #enable this parameter on demand (see description below) ports: - "8080:8080" #map docker port 8080 to host port 8080. volumes: - "./signal-cli-config:/home/.local/share/signal-cli" #map "signal-cli-config" folder on host system into docker container. the folder contains the password and cryptographic keys when a new number is registered - # Native variant -- use with MODE=native or MODE=json-rpc-native - # Uncomment to use the native variant (smaller image, no Java runtime) - # signal-cli-rest-api-native: - # image: bbernhard/signal-cli-rest-api:latest-native + # JRE-only variant -- lighter, use with MODE=normal or MODE=json-rpc + # signal-cli-rest-api-jre: + # image: bbernhard/signal-cli-rest-api:latest-jre # environment: - # - MODE=native #supported modes: native, json-rpc-native + # - MODE=normal # ports: # - "8080:8080" # volumes: # - "./signal-cli-config:/home/.local/share/signal-cli" + + # Native-only variant -- lightest, use with MODE=native or MODE=json-rpc-native + # signal-cli-rest-api-native: + # image: bbernhard/signal-cli-rest-api:latest-native + # environment: + # - MODE=native + # ports: + # - "8080:8080" + # volumes: + # - "./signal-cli-config:/home/.local/share/signal-cli" \ No newline at end of file diff --git a/test-builds.sh b/test-builds.sh new file mode 100644 index 0000000..8190fa1 --- /dev/null +++ b/test-builds.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +WDIR=$(dirname $(readlink -f $0)) + + +function test_run() { + local target=$1 + local run_mode=$2 + + # Create the .env file + # Build and run all + # Create the .env file + cat < $WDIR/.env +BUILD_TARGET=${target} # all, jre, native +RUN_MODE=${run_mode} # normal, json-rpc, native, json-rpc-native +EOF + + docker compose \ + --project-directory $WDIR \ + up \ + --build --abort-on-container-exit --exit-code-from signal-cli-rest-api & + compose_pid=$! +} + +function test_wait() { + local target=$1 + local run_mode=$2 + local timeout=$3 + local start_time=$(date +%s) + + for i in $(seq 1 30); do + if curl -sf http://localhost:8080/v1/health >/dev/null 2>&1; then + echo "Health check passed" + echo Killing $compose_pid + kill -9 $compose_pid 2>/dev/null + wait $compose_pid 2>/dev/null + docker compose down + if [ $? -ne 0 ]; then + echo "ERROR: docker compose down failed for ${target} ${run_mode}" + exit 1 + fi + return 0 + fi + sleep 2 + done + echo "ERROR: docker compose build failed for ${target} ${run_mode}" +} + +function test_run_and_wait() { + local target=$1 + local run_mode=$2 + test_run $target $run_mode + test_wait $target $run_mode +} + +# Try to run all three build targets and wait for them to finish +# normal, json-rpc, native, json-rpc-native +test_run_and_wait "all" "normal" +test_run_and_wait "all" "json-rpc" +test_run_and_wait "all" "native" +test_run_and_wait "all" "json-rpc-native" + +test_run_and_wait "jre" "normal" +test_run_and_wait "jre" "json-rpc" + +test_run_and_wait "native" "native" +test_run_and_wait "native" "json-rpc-native" From febd6bb85a46279ba7baf3422283d8ef555f571e Mon Sep 17 00:00:00 2001 From: Peter Zandbergen Date: Sun, 21 Jun 2026 13:23:05 +0200 Subject: [PATCH 8/8] Document build optimization --- AGENTS.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 9271065..60b1796 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,11 +82,19 @@ The JRE variant uses `openjdk-25-jre-headless` (not the full `openjdk-25-jre`) t | `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error` | | `DEFAULT_SIGNAL_TEXT_MODE` | `normal` | `normal` or `styled` | +## CI Build Optimization + +All three targets (`all`, `jre`, `native`) share the `buildcontainer` and `base` stages in the Dockerfile. Without caching, Podman rebuilds these from scratch for each `--target` invocation — tripling the build time. + +The `--layers` flag on every `podman build` call tells Podman to cache intermediate layers in the local store. Since `all` is built first, its `base` and `buildcontainer` layers are already cached when `jre` and `native` are built next, so only the target-specific layers are rebuilt. + +Build order matters: `all` → `jre` → `native`. All three share `base` + `buildcontainer`; `jre` additionally shares its layers with `all` (since `all` extends `jre`). + ## Gotchas - The Dockerfile pins `SIGNAL_CLI_VERSION` and `LIBSIGNAL_CLIENT_VERSION` as build args. After bumping these, verify the `libsignal-client-*.jar` filename still matches. - Multi-arch builds: the `jre` target covers `linux/amd64`, `linux/arm64`, `linux/arm/v7`; the `native` target covers `linux/amd64`, `linux/arm64` only (no armv7 native binary). - `AUTO_RECEIVE_SCHEDULE` and `SIGNAL_CLI_CMD_TIMEOUT` cause a fatal error in json-rpc mode. - `RECEIVE_WEBHOOK_URL` is only valid in json-rpc mode; fatal in other modes. -- CI uses Podman for multi-arch builds, not Docker Buildx. Each CI/release workflow builds all three targets (`all`, `jre`, `native`). +- CI uses Podman for multi-arch builds, not Docker Buildx. Each CI/release workflow builds all three targets (`all`, `jre`, `native`) with `--layers` for cache reuse between targets. - Release versions are published via `publish.sh` triggering GitHub Actions (dev vs stable tags, with `-jre` and `-native` suffixes for those variants; `all` variant gets the plain version/latest tags). \ No newline at end of file