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 new file mode 100644 index 000000000..cf139b1fc --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,302 @@ +# Builds RustGIS's Docker images and publishes them to the GitHub Container Registry, for +# 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) +# +# 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 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: + 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/** + - 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.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + 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: + web: + name: Web app image + 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.WEB_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 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:test + cache-from: type=gha,scope=web + cache-to: type=gha,scope=web,mode=max + + - name: Smoke test + run: | + set -euo pipefail + 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 + done + 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." + + - name: Smoke test logs + if: failure() + run: docker logs rustgis || 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,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 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' + 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/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 9e9720238..1b6bb81d2 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 + 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. - [ ] **Package-manager and store channels**: winget, a Homebrew Cask, the Mac App Store. Partial: 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 f05e77526..9d2cc6a11 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -208,12 +208,22 @@ 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 build -t rustgis-web . -docker run --rm -p 8080:8080 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 +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 . +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 @@ -230,10 +240,26 @@ 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. +### 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..43be95823 --- /dev/null +++ b/packaging/docker/cli.Dockerfile @@ -0,0 +1,55 @@ +# 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 \ + && 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" \ + 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 +# 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. +USER rustgis +WORKDIR /data +ENTRYPOINT ["rustgis-cli"]