From b9cde0486f6e8fc2039fe5bf474000fb7ac60396 Mon Sep 17 00:00:00 2001 From: giswqs Date: Mon, 26 Oct 2026 19:02:46 -0400 Subject: [PATCH 1/5] ci(docker): build the web app's image and publish it to ghcr.io A Docker image workflow builds the Dockerfile and publishes ghcr.io/opengeos/rustgis-web for linux/amd64 and linux/arm64: edge and sha- on every update to main, the version, major.minor and latest on every published release (a pre-release gets only its version). The build stage runs once, on the runner's platform, so the arm64 image needs no emulation. Every run first builds the amd64 image, loads it and checks that it serves the app (health check, index.html with the WebAssembly, the service worker and the manifest). A pull request that changes the image (Dockerfile, .dockerignore, packaging/docker, the workflow) runs only that check and publishes nothing. The labels and index annotations keep the Dockerfile's title, description and URL over the ones metadata-action takes from the repository. Docs: installation.md now leads with the published image; GEOLIBRE_PARITY.md notes it. --- .github/workflows/docker.yml | 127 +++++++++++++++++++++++++++++++++++ GEOLIBRE_PARITY.md | 5 +- docs/installation.md | 14 +++- 3 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/docker.yml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 000000000..804ee3872 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,127 @@ +# Builds the web app's Docker image (the Dockerfile at the root, see docs/installation.md › +# Self-host the web app with Docker) and publishes it to the GitHub Container Registry as +# ghcr.io/opengeos/rustgis-web, for linux/amd64 and linux/arm64: +# +# edge, sha- every update to main +# , ., every published release (a pre-release gets only its version) +# latest +# +# A pull request that changes the image builds and smoke-tests it without publishing. The app is +# WebAssembly, so the build stage runs once, on the runner's platform, for both platforms. +name: Docker image + +on: + push: + branches: [main] + release: + types: [published] + pull_request: + paths: + - Dockerfile + - .dockerignore + - packaging/docker/** + - .github/workflows/docker.yml + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: docker-${{ github.event_name }}-${{ github.ref }} + # A newer push to main supersedes the image being built; a release always finishes. + cancel-in-progress: ${{ github.event_name != 'release' }} + +env: + IMAGE: ghcr.io/opengeos/rustgis-web + +jobs: + image: + name: Build and publish + if: github.repository == 'opengeos/RustGIS' + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + persist-credentials: false + + - uses: docker/setup-buildx-action@v4 + + - name: Image tags and labels + id: meta + uses: docker/metadata-action@v6 + env: + # The registry shows the index's annotations for a multi-platform image. + DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index + with: + images: ${{ env.IMAGE }} + # `latest` follows releases, not main. + flavor: latest=false + tags: | + type=edge,branch=main + type=sha,enable=${{ github.event_name != 'release' }} + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }} + # The Dockerfile's own, ahead of the ones metadata-action takes from the repository. + labels: | + org.opencontainers.image.title=RustGIS web app + org.opencontainers.image.description=The RustGIS web app (WebAssembly), served by nginx + org.opencontainers.image.url=https://rustgis.app + annotations: | + org.opencontainers.image.title=RustGIS web app + org.opencontainers.image.description=The RustGIS web app (WebAssembly), served by nginx + org.opencontainers.image.url=https://rustgis.app + + # The image for the runner's platform, loaded into Docker to check that it serves the app. + - name: Build for the smoke test + uses: docker/build-push-action@v7 + with: + context: . + platforms: linux/amd64 + load: true + tags: rustgis-web:test + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Smoke test + run: | + set -euo pipefail + docker run -d --name rustgis-web -p 8080:8080 rustgis-web:test + for _ in $(seq 30); do + curl -sSf http://127.0.0.1:8080/healthz >/dev/null && break + sleep 1 + done + curl -sSf http://127.0.0.1:8080/ | grep -q '_bg.wasm' + curl -sSf -o /dev/null http://127.0.0.1:8080/sw.js + curl -sSf -o /dev/null http://127.0.0.1:8080/manifest.webmanifest + echo "The image serves the web app." + + - name: Smoke test logs + if: failure() + run: docker logs rustgis-web || true + + - name: Sign in to the GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Both platforms; the build stage is the smoke test's, from the builder's cache. + - name: Publish + if: github.event_name != 'pull_request' + uses: docker/build-push-action@v7 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + annotations: ${{ steps.meta.outputs.annotations }} + cache-from: type=gha diff --git a/GEOLIBRE_PARITY.md b/GEOLIBRE_PARITY.md index 9e9720238..69856f93a 100644 --- a/GEOLIBRE_PARITY.md +++ b/GEOLIBRE_PARITY.md @@ -267,8 +267,9 @@ deployment-capabilities.md, ui-profiles.md_ GeoLibre, which covers field work on phones. - [ ] **PWA and offline basemap tile pre-caching**. Partial: the web app is installable with offline start; offline data areas with sync. Tile pre-caching unverified. -- [ ] **Docker self-hosting image with runtime config**. Partial: a Dockerfile for the web app; a - published image and runtime config (settings from the container's environment) are missing. +- [ ] **Docker self-hosting image with runtime config**. Partial: a Dockerfile for the web app and a + published image (ghcr.io/opengeos/rustgis-web); runtime config (settings from the container's + environment) is missing. - [ ] **Deployment capabilities, policies and UI profiles**: restrict edit, add, run and export per deployment. Missing. - [ ] **Package-manager and store channels**: winget, a Homebrew Cask, the Mac App Store. Partial: diff --git a/docs/installation.md b/docs/installation.md index f05e77526..e62cffed8 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -208,12 +208,20 @@ stale cache. ### Self-host the web app with Docker -The `Dockerfile` at the root of the repository builds the web app the way the deploy does and -serves it with nginx, so you need only [Docker](https://docs.docker.com/get-docker/): +A ready-made image of the web app, served by nginx, is published to the GitHub Container +Registry for x86-64 and arm64, so you need only [Docker](https://docs.docker.com/get-docker/): + +```sh +docker run --rm -p 8080:8080 ghcr.io/opengeos/rustgis-web # http://localhost:8080 +``` + +`latest` is the newest release, `0.5.0` or `0.5` a given one (releases have images from 0.5.0 +on), and `edge` the newest build of `main`. To build the image yourself, the `Dockerfile` at the root of the repository builds the web +app the way the deploy does: ```sh docker build -t rustgis-web . -docker run --rm -p 8080:8080 rustgis-web # http://localhost:8080 +docker run --rm -p 8080:8080 rustgis-web ``` The image holds only the built files and runs nginx as a non-root user on port 8080. Its pages From a6f12ea14138d6897587a586567349ce404a8870 Mon Sep 17 00:00:00 2001 From: giswqs Date: Mon, 26 Oct 2026 19:04:18 -0400 Subject: [PATCH 2/5] ci(docker): name the image ghcr.io/opengeos/rustgis The registry's own name for it, without the -web suffix; the local build examples tag it rustgis too. --- .github/workflows/docker.yml | 10 +++++----- Dockerfile | 4 ++-- GEOLIBRE_PARITY.md | 2 +- docs/installation.md | 8 ++++---- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 804ee3872..ca4dafe80 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,6 +1,6 @@ # Builds the web app's Docker image (the Dockerfile at the root, see docs/installation.md › # Self-host the web app with Docker) and publishes it to the GitHub Container Registry as -# ghcr.io/opengeos/rustgis-web, for linux/amd64 and linux/arm64: +# ghcr.io/opengeos/rustgis, for linux/amd64 and linux/arm64: # # edge, sha- every update to main # , ., every published release (a pre-release gets only its version) @@ -32,7 +32,7 @@ concurrency: cancel-in-progress: ${{ github.event_name != 'release' }} env: - IMAGE: ghcr.io/opengeos/rustgis-web + IMAGE: ghcr.io/opengeos/rustgis jobs: image: @@ -84,14 +84,14 @@ jobs: context: . platforms: linux/amd64 load: true - tags: rustgis-web:test + tags: rustgis:test cache-from: type=gha cache-to: type=gha,mode=max - name: Smoke test run: | set -euo pipefail - docker run -d --name rustgis-web -p 8080:8080 rustgis-web:test + docker run -d --name rustgis -p 8080:8080 rustgis:test for _ in $(seq 30); do curl -sSf http://127.0.0.1:8080/healthz >/dev/null && break sleep 1 @@ -103,7 +103,7 @@ jobs: - name: Smoke test logs if: failure() - run: docker logs rustgis-web || true + run: docker logs rustgis || true - name: Sign in to the GitHub Container Registry if: github.event_name != 'pull_request' diff --git a/Dockerfile b/Dockerfile index 34bb87f32..0e11a76e7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,8 @@ # The RustGIS web app, built from this checkout and served by nginx (see docs/installation.md › # Self-host the web app with Docker): # -# docker build -t rustgis-web . -# docker run --rm -p 8080:8080 rustgis-web # http://localhost:8080 +# docker build -t rustgis . +# docker run --rm -p 8080:8080 rustgis # http://localhost:8080 # # It builds as .github/workflows/web-deploy.yml does, with three differences: no analytics tag # (only the official deploy adds one), relative addresses (as the docs preview's build) so it can diff --git a/GEOLIBRE_PARITY.md b/GEOLIBRE_PARITY.md index 69856f93a..5a4732f89 100644 --- a/GEOLIBRE_PARITY.md +++ b/GEOLIBRE_PARITY.md @@ -268,7 +268,7 @@ deployment-capabilities.md, ui-profiles.md_ - [ ] **PWA and offline basemap tile pre-caching**. Partial: the web app is installable with offline start; offline data areas with sync. Tile pre-caching unverified. - [ ] **Docker self-hosting image with runtime config**. Partial: a Dockerfile for the web app and a - published image (ghcr.io/opengeos/rustgis-web); runtime config (settings from the container's + published image (ghcr.io/opengeos/rustgis); runtime config (settings from the container's environment) is missing. - [ ] **Deployment capabilities, policies and UI profiles**: restrict edit, add, run and export per deployment. Missing. diff --git a/docs/installation.md b/docs/installation.md index e62cffed8..2d56d5d12 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -212,7 +212,7 @@ A ready-made image of the web app, served by nginx, is published to the GitHub C Registry for x86-64 and arm64, so you need only [Docker](https://docs.docker.com/get-docker/): ```sh -docker run --rm -p 8080:8080 ghcr.io/opengeos/rustgis-web # http://localhost:8080 +docker run --rm -p 8080:8080 ghcr.io/opengeos/rustgis # http://localhost:8080 ``` `latest` is the newest release, `0.5.0` or `0.5` a given one (releases have images from 0.5.0 @@ -220,8 +220,8 @@ on), and `edge` the newest build of `main`. To build the image yourself, the `Do app the way the deploy does: ```sh -docker build -t rustgis-web . -docker run --rm -p 8080:8080 rustgis-web +docker build -t rustgis . +docker run --rm -p 8080:8080 rustgis ``` The image holds only the built files and runs nginx as a non-root user on port 8080. Its pages @@ -238,7 +238,7 @@ The image is for the platform of the machine that builds it. To build it for ano on an Apple silicon Mac for an x86-64 server, add `--platform linux/amd64` to `docker build`. With a builder that can build several platforms (one made by `docker buildx create --use`, or Docker Desktop's containerd image store), -`docker buildx build --platform linux/amd64,linux/arm64 -t /rustgis-web --push .` +`docker buildx build --platform linux/amd64,linux/arm64 -t /rustgis --push .` publishes one image for both. The app itself is WebAssembly, so it is compiled once, on the machine that builds it, whichever platforms the image is for. From 4b9071199efa412063baf00843e60e745c4c4d02 Mon Sep 17 00:00:00 2001 From: giswqs Date: Mon, 26 Oct 2026 19:09:00 -0400 Subject: [PATCH 3/5] ci(docker): a command line image, ghcr.io/opengeos/rustgis-cli packaging/docker/cli.Dockerfile builds rustgis-cli on the rust image and copies it onto the matching slim Debian (13), as a non-root user (ID 1000) working in /data, with rustgis-cli as the entrypoint. It links only glibc. Unlike the web app, the command line is native code, so the workflow builds each platform on a runner of its own (ubuntu-latest and ubuntu-24.04-arm), smoke-tests it (tools, logo, rendering the sample), pushes it by digest, and a manifest job tags the two together with the same tags as the web app's image. The tag list is shared by both images. Dependabot watches packaging/docker as well. Docs: "The command line in Docker" in installation.md, linked from cli.md; packaging/README.md and GEOLIBRE_PARITY.md mention the image. --- .github/dependabot.yml | 7 +- .github/workflows/docker.yml | 201 ++++++++++++++++++++++++++++---- GEOLIBRE_PARITY.md | 4 +- docs/cli.md | 3 +- docs/installation.md | 16 +++ packaging/README.md | 4 +- packaging/docker/cli.Dockerfile | 52 +++++++++ 7 files changed, 260 insertions(+), 27 deletions(-) create mode 100644 packaging/docker/cli.Dockerfile diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fa289bfc7..ac7edae7e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -49,9 +49,12 @@ updates: - minor - patch - # The base images of the Dockerfile that self-hosts the web app, pinned by tag and digest. + # The base images of the Docker images (the web app's Dockerfile at the root, the command line's + # in packaging/docker), pinned by tag and digest. - package-ecosystem: docker - directory: "/" + directories: + - "/" + - "/packaging/docker" schedule: interval: weekly open-pull-requests-limit: 2 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ca4dafe80..a1b36fdae 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,14 +1,20 @@ -# Builds the web app's Docker image (the Dockerfile at the root, see docs/installation.md › -# Self-host the web app with Docker) and publishes it to the GitHub Container Registry as -# ghcr.io/opengeos/rustgis, for linux/amd64 and linux/arm64: +# Builds RustGIS's Docker images and publishes them to the GitHub Container Registry, for +# linux/amd64 and linux/arm64 (see docs/installation.md › Docker): +# +# ghcr.io/opengeos/rustgis the web app, served by nginx (the Dockerfile at the root) +# ghcr.io/opengeos/rustgis-cli the command line (packaging/docker/cli.Dockerfile) +# +# with the tags # # edge, sha- every update to main # , ., every published release (a pre-release gets only its version) # latest # -# A pull request that changes the image builds and smoke-tests it without publishing. The app is -# WebAssembly, so the build stage runs once, on the runner's platform, for both platforms. -name: Docker image +# A pull request that changes an image builds and smoke-tests both without publishing. The web app +# is WebAssembly, so its build stage runs once, on the runner's platform, for both platforms; the +# command line is native code, so each platform is built on a runner of its own and the two are +# then published under one multi-platform tag. +name: Docker images on: push: @@ -28,15 +34,23 @@ permissions: concurrency: group: docker-${{ github.event_name }}-${{ github.ref }} - # A newer push to main supersedes the image being built; a release always finishes. + # A newer push to main supersedes the images being built; a release always finishes. cancel-in-progress: ${{ github.event_name != 'release' }} env: - IMAGE: ghcr.io/opengeos/rustgis + WEB_IMAGE: ghcr.io/opengeos/rustgis + CLI_IMAGE: ghcr.io/opengeos/rustgis-cli + # The tags of both images (docker/metadata-action); `latest` follows releases, not main. + TAGS: | + type=edge,branch=main + type=sha,enable=${{ github.event_name != 'release' }} + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }} jobs: - image: - name: Build and publish + web: + name: Web app image if: github.repository == 'opengeos/RustGIS' runs-on: ubuntu-latest timeout-minutes: 60 @@ -58,15 +72,9 @@ jobs: # The registry shows the index's annotations for a multi-platform image. DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index with: - images: ${{ env.IMAGE }} - # `latest` follows releases, not main. + images: ${{ env.WEB_IMAGE }} flavor: latest=false - tags: | - type=edge,branch=main - type=sha,enable=${{ github.event_name != 'release' }} - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }} + tags: ${{ env.TAGS }} # The Dockerfile's own, ahead of the ones metadata-action takes from the repository. labels: | org.opencontainers.image.title=RustGIS web app @@ -85,8 +93,8 @@ jobs: platforms: linux/amd64 load: true tags: rustgis:test - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: type=gha,scope=web + cache-to: type=gha,scope=web,mode=max - name: Smoke test run: | @@ -124,4 +132,155 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} annotations: ${{ steps.meta.outputs.annotations }} - cache-from: type=gha + cache-from: type=gha,scope=web + + cli: + name: Command line image (${{ matrix.arch }}) + if: github.repository == 'opengeos/RustGIS' + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + runner: ubuntu-latest + - arch: arm64 + runner: ubuntu-24.04-arm + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + persist-credentials: false + + - uses: docker/setup-buildx-action@v4 + + - name: Image labels + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.CLI_IMAGE }} + flavor: latest=false + tags: ${{ env.TAGS }} + # The Dockerfile's own, ahead of the ones metadata-action takes from the repository. + labels: | + org.opencontainers.image.title=RustGIS command line + org.opencontainers.image.description=The RustGIS command line: run commands and geoprocessing, render maps, inspect data + org.opencontainers.image.url=https://rustgis.app + + - name: Build for the smoke test + uses: docker/build-push-action@v7 + with: + context: . + file: packaging/docker/cli.Dockerfile + platforms: linux/${{ matrix.arch }} + load: true + tags: rustgis-cli:test + cache-from: type=gha,scope=cli-${{ matrix.arch }} + cache-to: type=gha,scope=cli-${{ matrix.arch }},mode=max + + # Runs the binary as the image runs it; drawing the logo and rendering the built-in sample + # exercise the renderer, the fonts and the embedded data. + - name: Smoke test + run: | + set -euo pipefail + docker run --rm rustgis-cli:test tools > "$RUNNER_TEMP/tools.txt" + grep -q buffer "$RUNNER_TEMP/tools.txt" + docker run --rm rustgis-cli:test logo /tmp/logo.png --size 64 + docker run --rm --entrypoint sh rustgis-cli:test -c \ + 'rustgis-cli run --sample --cmd "share.exportMap={\"path\":\"/tmp/map.png\"}" && test -s /tmp/map.png' + echo "The image runs the command line." + + - name: Sign in to the GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Pushed by digest only; the manifest job tags the two platforms' images together. + - name: Publish by digest + id: push + if: github.event_name != 'pull_request' + uses: docker/build-push-action@v7 + with: + context: . + file: packaging/docker/cli.Dockerfile + platforms: linux/${{ matrix.arch }} + labels: ${{ steps.meta.outputs.labels }} + outputs: type=image,name=${{ env.CLI_IMAGE }},push-by-digest=true,name-canonical=true,push=true + cache-from: type=gha,scope=cli-${{ matrix.arch }} + + - name: Record the digest + if: github.event_name != 'pull_request' + env: + DIGEST: ${{ steps.push.outputs.digest }} + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/digests" + touch "$RUNNER_TEMP/digests/${DIGEST#sha256:}" + + - name: Upload the digest + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@v7 + with: + name: cli-digest-${{ matrix.arch }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + cli-manifest: + name: Command line image (publish) + needs: cli + if: github.repository == 'opengeos/RustGIS' && github.event_name != 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + packages: write + steps: + - name: Download the digests + uses: actions/download-artifact@v8 + with: + path: ${{ runner.temp }}/digests + pattern: cli-digest-* + merge-multiple: true + + - uses: docker/setup-buildx-action@v4 + + - name: Image tags + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.CLI_IMAGE }} + flavor: latest=false + tags: ${{ env.TAGS }} + + - name: Sign in to the GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish the multi-platform tags + working-directory: ${{ runner.temp }}/digests + run: | + set -euo pipefail + tags=() + while IFS= read -r tag; do + [ -n "$tag" ] && tags+=(-t "$tag") + done <<< "$DOCKER_METADATA_OUTPUT_TAGS" + sources=() + for digest in *; do sources+=("$CLI_IMAGE@sha256:$digest"); done + docker buildx imagetools create "${tags[@]}" \ + --annotation "index:org.opencontainers.image.title=RustGIS command line" \ + --annotation "index:org.opencontainers.image.description=The RustGIS command line: run commands and geoprocessing, render maps, inspect data" \ + --annotation "index:org.opencontainers.image.source=https://github.com/opengeos/RustGIS" \ + --annotation "index:org.opencontainers.image.licenses=Apache-2.0" \ + "${sources[@]}" + docker buildx imagetools inspect "$CLI_IMAGE:$DOCKER_METADATA_OUTPUT_VERSION" diff --git a/GEOLIBRE_PARITY.md b/GEOLIBRE_PARITY.md index 5a4732f89..1b6bb81d2 100644 --- a/GEOLIBRE_PARITY.md +++ b/GEOLIBRE_PARITY.md @@ -267,8 +267,8 @@ deployment-capabilities.md, ui-profiles.md_ GeoLibre, which covers field work on phones. - [ ] **PWA and offline basemap tile pre-caching**. Partial: the web app is installable with offline start; offline data areas with sync. Tile pre-caching unverified. -- [ ] **Docker self-hosting image with runtime config**. Partial: a Dockerfile for the web app and a - published image (ghcr.io/opengeos/rustgis); runtime config (settings from the container's +- [ ] **Docker self-hosting image with runtime config**. Partial: a Dockerfile for the web app and + published images (ghcr.io/opengeos/rustgis, and rustgis-cli for the command line); runtime config (settings from the container's environment) is missing. - [ ] **Deployment capabilities, policies and UI profiles**: restrict edit, add, run and export per deployment. Missing. diff --git a/docs/cli.md b/docs/cli.md index e1a422f5d..4f3795cf3 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -15,7 +15,8 @@ rustgis-cli skills # the AI Assistant's skills (also: add, re ``` `commands` lists the engine commands and their JSON parameters; `tools` lists geoprocessing -tools and their parameters. See [Installation](installation.md) for the CLI packages. +tools and their parameters. See [Installation](installation.md) for the CLI packages, and +[The command line in Docker](installation.md#the-command-line-in-docker) for its image. ## Rendering diff --git a/docs/installation.md b/docs/installation.md index 2d56d5d12..428d6abcb 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -242,6 +242,22 @@ Docker Desktop's containerd image store), publishes one image for both. The app itself is WebAssembly, so it is compiled once, on the machine that builds it, whichever platforms the image is for. +### The command line in Docker + +`ghcr.io/opengeos/rustgis-cli` holds the [command line](cli.md), `rustgis-cli`, on a slim Debian, +for x86-64 and arm64, with the same tags. It runs in `/data`, so mount the folder to work on +there: + +```sh +docker run --rm -v "$PWD:/data" ghcr.io/opengeos/rustgis-cli info cities.geojson +docker run --rm -v "$PWD:/data" ghcr.io/opengeos/rustgis-cli render cities.geojson map.png --basemap ocean +``` + +It runs as a user with ID 1000, the first user on most Linux systems, so the files it writes are +yours; elsewhere, add `--user "$(id -u):$(id -g)" -e HOME=/tmp`. The optional GDAL, DuckDB and +GeoLibre programs aren't in the image. To build it yourself: +`docker build -f packaging/docker/cli.Dockerfile -t rustgis-cli .` + ## Fonts RustGIS uses an installed sans-serif when it finds one (Segoe UI, San Francisco, Arial, Inter, diff --git a/packaging/README.md b/packaging/README.md index 0a468ac9c..6cd3416d7 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -4,7 +4,9 @@ runs it on every platform and attaches the files to the GitHub Release. `downloads/` is the [downloads.rustgis.app](https://downloads.rustgis.app) site. `docker/` holds the nginx configuration of the `Dockerfile` at the repository root, which builds the web app into an image -to host yourself (see [Installation](../docs/installation.md#self-host-the-web-app-with-docker)). +to host yourself (see [Installation](../docs/installation.md#self-host-the-web-app-with-docker)), +and `cli.Dockerfile`, the command line's image. `.github/workflows/docker.yml` publishes both to +ghcr.io. ## Package managers diff --git a/packaging/docker/cli.Dockerfile b/packaging/docker/cli.Dockerfile new file mode 100644 index 000000000..0f79f883c --- /dev/null +++ b/packaging/docker/cli.Dockerfile @@ -0,0 +1,52 @@ +# syntax=docker/dockerfile:1 +# +# The RustGIS command line (rustgis-cli), built from this checkout, on a slim Debian (see +# docs/installation.md › The command line in Docker). Build it from the repository root: +# +# docker build -f packaging/docker/cli.Dockerfile -t rustgis-cli . +# docker run --rm -v "$PWD:/data" rustgis-cli info data.geojson +# +# Unlike the web app's image, this is native code, so it is built on the platform the image is +# for. .github/workflows/docker.yml builds each platform on a runner of its own. +# Dependabot keeps the base images current. + +FROM rust:1.99.1-slim@sha256:1a939748a7cbab172de91a014e3804a97af1394a8c844faa67fce9401c4e40fb AS build + +# crates/render/build.rs uses curl to fetch the Natural Earth sample data it embeds. +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates curl \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /src +COPY . . + +# The cargo registry and the target directory are cache mounts, so a rebuild after a change redoes +# only what changed. The binary is copied out of them. +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/src/target \ + cargo build --release --locked -p rustgis-cli \ + && cp target/release/rustgis-cli /usr/local/bin/rustgis-cli + +# The Debian release the rust image is built on, so the binary finds the C library it was linked +# against. +FROM debian:13.7-slim@sha256:62bd423aeee912639ca8ee681c2b762cb3e25137e0a92b20be455898a9f034ad + +# ca-certificates for HTTPS (web tiles, remote data, the AI Assistant's providers). +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates \ + && rm -rf /var/lib/apt/lists/* \ + && useradd --create-home --uid 1000 rustgis + +LABEL org.opencontainers.image.title="RustGIS command line" \ + org.opencontainers.image.description="The RustGIS command line: run commands and geoprocessing, render maps, inspect data" \ + org.opencontainers.image.url="https://rustgis.app" \ + org.opencontainers.image.source="https://github.com/opengeos/RustGIS" \ + org.opencontainers.image.licenses="Apache-2.0" + +COPY --from=build /usr/local/bin/rustgis-cli /usr/local/bin/rustgis-cli + +# Non-root; its settings, plugins and caches go to /home/rustgis. Mount the data to work on at +# /data, the working directory, so relative paths resolve there. +USER rustgis +WORKDIR /data +ENTRYPOINT ["rustgis-cli"] From eb39d23a15505de0ee13327f6f6dd36db25abd9c Mon Sep 17 00:00:00 2001 From: giswqs Date: Mon, 26 Oct 2026 19:24:58 -0400 Subject: [PATCH 4/5] Address review feedback - Run the image workflow on pull requests that change how the images build: Cargo.toml, Cargo.lock, .cargo, xtask and the two apps, not only the Docker files (crates' code is built by ci.yml already). - Ship LICENSE and THIRD-PARTY-LICENSES.txt in the CLI image at /usr/share/doc/rustgis, as the .deb does, and smoke-test that it's there. - Smoke-test index.html from a file instead of curl | grep -q, which can SIGPIPE under pipefail. - Rewrap the installation docs' paragraph on building the image. --- .github/workflows/docker.yml | 15 +++++++++++++-- docs/installation.md | 6 ++++-- packaging/docker/cli.Dockerfile | 2 ++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a1b36fdae..6826d63be 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -21,12 +21,21 @@ on: branches: [main] release: types: [published] + # The images' own files and what changes how they build: the dependencies, the build tasks and + # the two apps. A change to the crates' code is built natively and for wasm by + # ci.yml already; a new build dependency of theirs arrives through Cargo.lock. pull_request: paths: - Dockerfile - .dockerignore - packaging/docker/** - .github/workflows/docker.yml + - Cargo.toml + - Cargo.lock + - .cargo/** + - xtask/** + - apps/rustgis-web/** + - apps/rustgis-cli/** workflow_dispatch: permissions: @@ -104,7 +113,8 @@ jobs: curl -sSf http://127.0.0.1:8080/healthz >/dev/null && break sleep 1 done - curl -sSf http://127.0.0.1:8080/ | grep -q '_bg.wasm' + curl -sSf -o "$RUNNER_TEMP/index.html" http://127.0.0.1:8080/ + grep -q '_bg.wasm' "$RUNNER_TEMP/index.html" curl -sSf -o /dev/null http://127.0.0.1:8080/sw.js curl -sSf -o /dev/null http://127.0.0.1:8080/manifest.webmanifest echo "The image serves the web app." @@ -191,7 +201,8 @@ jobs: grep -q buffer "$RUNNER_TEMP/tools.txt" docker run --rm rustgis-cli:test logo /tmp/logo.png --size 64 docker run --rm --entrypoint sh rustgis-cli:test -c \ - 'rustgis-cli run --sample --cmd "share.exportMap={\"path\":\"/tmp/map.png\"}" && test -s /tmp/map.png' + 'rustgis-cli run --sample --cmd "share.exportMap={\"path\":\"/tmp/map.png\"}" && test -s /tmp/map.png \ + && test -s /usr/share/doc/rustgis/THIRD-PARTY-LICENSES.txt' echo "The image runs the command line." - name: Sign in to the GitHub Container Registry diff --git a/docs/installation.md b/docs/installation.md index 428d6abcb..9d2cc6a11 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -216,8 +216,10 @@ docker run --rm -p 8080:8080 ghcr.io/opengeos/rustgis # http://localhost:8080 ``` `latest` is the newest release, `0.5.0` or `0.5` a given one (releases have images from 0.5.0 -on), and `edge` the newest build of `main`. To build the image yourself, the `Dockerfile` at the root of the repository builds the web -app the way the deploy does: +on), and `edge` the newest build of `main`. + +To build the image yourself, the `Dockerfile` at the root of the repository builds the web app +the way the deploy does: ```sh docker build -t rustgis . diff --git a/packaging/docker/cli.Dockerfile b/packaging/docker/cli.Dockerfile index 0f79f883c..048bd7318 100644 --- a/packaging/docker/cli.Dockerfile +++ b/packaging/docker/cli.Dockerfile @@ -44,6 +44,8 @@ LABEL org.opencontainers.image.title="RustGIS command line" \ org.opencontainers.image.licenses="Apache-2.0" COPY --from=build /usr/local/bin/rustgis-cli /usr/local/bin/rustgis-cli +# The licence and the notices of the dependencies compiled into it, where the .deb puts them. +COPY LICENSE THIRD-PARTY-LICENSES.txt /usr/share/doc/rustgis/ # Non-root; its settings, plugins and caches go to /home/rustgis. Mount the data to work on at # /data, the working directory, so relative paths resolve there. From 33290661db2cf91a873030e39f7d04a92229d0c1 Mon Sep 17 00:00:00 2001 From: giswqs Date: Mon, 26 Oct 2026 19:41:16 -0400 Subject: [PATCH 5/5] Address review feedback - Point the workflow's header at the two installation sections by their headings. - One concurrency group per ref, cancelling only pull request runs: a publishing run always finishes (no half-updated CLI tags), and pushes and manual runs on main publish edge in order. - Create /data owned by the rustgis user, so the CLI image can write there without a mount; the smoke test now writes the logo there. - Run the image workflow when LICENSE or THIRD-PARTY-LICENSES.txt change, now that the CLI image copies them. --- .github/workflows/docker.yml | 15 ++++++++++----- packaging/docker/cli.Dockerfile | 3 ++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 6826d63be..cf139b1fc 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,5 +1,6 @@ # Builds RustGIS's Docker images and publishes them to the GitHub Container Registry, for -# linux/amd64 and linux/arm64 (see docs/installation.md › Docker): +# linux/amd64 and linux/arm64 (see docs/installation.md › Self-host the web app with Docker, and +# The command line in Docker): # # ghcr.io/opengeos/rustgis the web app, served by nginx (the Dockerfile at the root) # ghcr.io/opengeos/rustgis-cli the command line (packaging/docker/cli.Dockerfile) @@ -36,15 +37,19 @@ on: - xtask/** - apps/rustgis-web/** - apps/rustgis-cli/** + - LICENSE + - THIRD-PARTY-LICENSES.txt workflow_dispatch: permissions: contents: read +# One run at a time per ref, so pushes and manual runs on main publish `edge` in order. A publishing +# run always finishes (a cancelled one could leave the CLI's tags half updated); a newer push only +# replaces a run that is still waiting. A pull request's newer push cancels its older run. concurrency: - group: docker-${{ github.event_name }}-${{ github.ref }} - # A newer push to main supersedes the images being built; a release always finishes. - cancel-in-progress: ${{ github.event_name != 'release' }} + group: docker-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: WEB_IMAGE: ghcr.io/opengeos/rustgis @@ -199,7 +204,7 @@ jobs: set -euo pipefail docker run --rm rustgis-cli:test tools > "$RUNNER_TEMP/tools.txt" grep -q buffer "$RUNNER_TEMP/tools.txt" - docker run --rm rustgis-cli:test logo /tmp/logo.png --size 64 + docker run --rm rustgis-cli:test logo logo.png --size 64 # into /data, unmounted docker run --rm --entrypoint sh rustgis-cli:test -c \ 'rustgis-cli run --sample --cmd "share.exportMap={\"path\":\"/tmp/map.png\"}" && test -s /tmp/map.png \ && test -s /usr/share/doc/rustgis/THIRD-PARTY-LICENSES.txt' diff --git a/packaging/docker/cli.Dockerfile b/packaging/docker/cli.Dockerfile index 048bd7318..43be95823 100644 --- a/packaging/docker/cli.Dockerfile +++ b/packaging/docker/cli.Dockerfile @@ -35,7 +35,8 @@ FROM debian:13.7-slim@sha256:62bd423aeee912639ca8ee681c2b762cb3e25137e0a92b20be4 RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates \ && rm -rf /var/lib/apt/lists/* \ - && useradd --create-home --uid 1000 rustgis + && useradd --create-home --uid 1000 rustgis \ + && install -d -o rustgis -g rustgis /data LABEL org.opencontainers.image.title="RustGIS command line" \ org.opencontainers.image.description="The RustGIS command line: run commands and geoprocessing, render maps, inspect data" \