Skip to content

feat(web): a Dockerfile to self-host the web app - #974

Merged
giswqs merged 3 commits into
opengeos:mainfrom
flight1973:feat/docker-self-host
Oct 26, 2026
Merged

giswqs merged 3 commits into
opengeos:mainfrom
flight1973:feat/docker-self-host

Conversation

@flight1973

@flight1973 flight1973 commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds a Dockerfile that builds the web app from the checkout and serves it with nginx, so anyone can host their own copy of web.rustgis.app. It lands the roadmap item A Dockerfile for self-hosting the web app (Self-contained, any time). No Rust code, crates, commands or project-format changes.

Build stage (rust:1.99.1-slim, on the build machine's own platform)

  • clang and the wasm32-unknown-unknown target; Trunk 0.21.14 pinned and checksum-verified as web-deploy.yml and docs-preview.yml pin it, plus the aarch64 checksum for arm64 build machines. The download steps are chained with &&, so a wrong checksum stops the build in any shell (older Podman ignores SHELL).
  • trunk build --release --locked --public-url ./, then cargo xtask web-pwa dist/web: the deploy's build without the analytics tag, with relative addresses as in the docs preview, so a copy also works under a path.
  • Cache mounts for the cargo registry, target/ and Trunk's tools.
  • FROM --platform=$BUILDPLATFORM: the output is WebAssembly, so an image for another platform (or for linux/amd64,linux/arm64 at once) compiles it once, natively.

Serve stage (nginxinc/nginx-unprivileged:1.31.7-alpine-slim)

  • Non-root (uid 101) on port 8080, a HEALTHCHECK on /healthz, and OCI labels (the base image's own labels are replaced or cleared).
  • packaging/docker/nginx.conf: the content-hashed .js and .wasm are cached immutable and everything else no-cache, so a new image reaches the app's new-version notice; gzip (also behind proxies), UTF-8 for text, application/manifest+json for the manifest, nosniff and relative redirects. No X-Frame-Options: the Python package's notebook and Qt widgets frame the app (?embed).
  • The slim variant leaves out nginx's optional modules and their libraries (21 packages instead of 71).

Both base images are pinned by tag and digest; a docker entry in .github/dependabot.yml keeps them current.

Docs: Self-host the web app with Docker in docs/installation.md (build and run, HTTPS through a reverse proxy, serving under a path, ArcGIS sign-in on a self-hosted copy, images for other platforms), linked from docs/python.md and packaging/README.md; the self-hosting rows of the comparison pages; ROADMAP.md (Done) and GEOLIBRE_PARITY.md (still partial: no published image or runtime config).

Testing

  • docker build on arm64 (macOS) and docker buildx build --platform linux/amd64 --load: both pass, and the amd64 build reuses the arm64 build stage (no emulated compile). A two-platform docker buildx build produced one index with linux/amd64 and linux/arm64, building the WebAssembly once.
  • Ran the images, also with --read-only --tmpfs /tmp and the amd64 one under emulation: healthy; per-file Content-Type, Cache-Control, gzip and redirects as described; 404 for unknown paths.
  • Chrome: the app loads from the container at / and under /rustgis/ behind a path-stripping proxy; the service worker registers and serves the app from its cache on reload; no console errors (screenshot in a comment below).
  • The Trunk step fails on a wrong checksum and installs nothing, also under plain /bin/sh.
  • docker build --check: no warnings. hadolint 2.12: only DL3008; apt versions are unpinned on purpose (the base is pinned by digest, Debian drops superseded versions, and the build stage isn't shipped).
  • Docker Scout: 0 critical, 1 high, in zlib from the nginx base image (fixed in Alpine; dependabot picks it up when nginx republishes the image).
  • cargo xtask ci: 2,437 passed, 0 failed, 16 ignored, layers ok; pre-commit run --all-files: passed; zensical build: no issues.
  • Not tested: a full Podman build.

Related issue

No issue: the roadmap item is marked (open). Lands A Dockerfile for self-hosting the web app (ROADMAP.md › Self-contained, any time) and is part of Docker self-hosting image with runtime config in GEOLIBRE_PARITY.md.

Possible follow-ups, not in this PR: a CI job that builds the image when the Dockerfile or the web build changes; publishing the image; runtime config.

Summary by CodeRabbit

  • New Features
    • Added a Docker option for self-hosting the RustGIS web app, serving the built app on port 8080.
    • The container runs as a non-root user and includes a health check.
  • Documentation
    • Added instructions for building and running the Docker image, including HTTPS, reverse-proxy, OAuth, and multi-platform build considerations.
    • Updated self-hosting guidance and platform comparisons to include Docker hosting.

Lands the roadmap item "A Dockerfile for self-hosting the web app" (Self-contained, any time).

The Dockerfile builds the web app as web-deploy.yml does (the same pinned, checksum-verified
Trunk release, `trunk build --release` and `cargo xtask web-pwa`), with relative addresses as
the docs preview's build, and serves it with the slim nginx-unprivileged image: non-root, on
port 8080, with a health check. The build stage runs on the build machine's own platform, so an
image for another platform, or for several, compiles the WebAssembly once, natively. The base
images are pinned by tag and digest; a docker entry in dependabot.yml keeps them current.

packaging/docker/nginx.conf caches the content-hashed .js and .wasm for good and revalidates
everything else, so a new image reaches the app's new-version notice. It serves text as UTF-8,
gzips (also behind proxies), gives the web app manifest its MIME type, sends nosniff and
relative redirects, and leaves out X-Frame-Options because the Python package's widgets frame
the app.

Docs: "Self-host the web app with Docker" in docs/installation.md (including signing in to
ArcGIS from a self-hosted copy), linked from docs/python.md and packaging/README.md; the
comparison pages; ROADMAP.md (Done) and GEOLIBRE_PARITY.md (still partial: no published image
or runtime config).
@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 676a6d20-9315-48fc-b23e-8d84f0a74c3d
Commits

Reviewing files that changed from the base of the PR and between cf17971 and 58c8853.

Files selected for processing: 1
  • Dockerfile

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.

Walkthrough

The repository adds a Docker build for the RustGIS web app and an nginx runtime image. It also adds Docker dependency update settings and updates installation, comparison, roadmap, parity, and Python documentation to describe Docker self-hosting.

Changes: Docker self-hosting

Layer / File(s) Summary
Build the web app image
.dockerignore, Dockerfile, .github/dependabot.yml
The Docker build installs the required tools, verifies and downloads Trunk for supported architectures, then builds the web app and PWA. Docker build exclusions and weekly Docker dependency updates are added.
Serve the built web app
Dockerfile, packaging/docker/nginx.conf
The final image serves the built assets with unprivileged nginx on port 8080. The configuration sets cache policies, compression, headers, file handling, manifest MIME type, and /healthz.
Document Docker self-hosting
docs/installation.md, packaging/README.md, GEOLIBRE_PARITY.md, ROADMAP.md, docs/comparison/index.md, docs/comparison/qgis.md, docs/python.md
The installation guide and related documentation describe Docker self-hosting and update the project’s hosting descriptions and parity status.

Priority: Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant DockerBuild
  participant Trunk
  participant CargoXtask
  participant Nginx
  participant Browser
  DockerBuild->>Trunk: Build web app
  DockerBuild->>CargoXtask: Build PWA from dist/web
  DockerBuild->>Nginx: Copy nginx config and built assets
  Browser->>Nginx: Request web app
  Nginx-->>Browser: Serve requested asset
Loading

Possibly related PRs

  • opengeos/RustGIS#747: PR #747 added the roadmap and parity inventory that included the Docker self-hosting item addressed here.

Suggested reviewers: giswqs

Merge Risk: Minimal · up to 58c88

The Docker self-hosting path and its documented deployment configuration are consistent, with no established merge-blocking risk.

Pre-merge checks: Passed 5
Check name Status Explanation
Title check Passed The title clearly and concisely identifies the main change: adding a Dockerfile to self-host the web app.
Description check Passed The description includes the required Summary, Testing, and Related issue sections. It explains the implementation, documents extensive validation, identifies the related roadmap item, and notes known…
Docstring Coverage Passed No eligible functions found in the changed files. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0 files. (1 skipped: 1 …
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description includes the required Summary, Testing, and Related issue sections. It explains the implementation, documents extensive validation, identifies the related roadmap item, and notes known limitations and follow-ups.

Full details: Docstring Coverage

Explanation

No eligible functions found in the changed files. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0 files. (1 skipped: 1 unsupported.)

Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit checks the Dockerfile’s plan,
Trunk builds the web app as it can.
Nginx serves each page with care,
A health check answers from its lair.
Hop, the docs now show the way!

Comment @coderabbitai help to get the list of available commands.

@giswqs giswqs added the claude-review Apply to run the Claude code review on this PR (remove and re-add to run again) label Oct 26, 2026
Comment thread Dockerfile Outdated
# own platform, also when the image is for another (`--platform linux/amd64,linux/arm64`).
FROM --platform=$BUILDPLATFORM rust:1.99.1-slim@sha256:1a939748a7cbab172de91a014e3804a97af1394a8c844faa67fce9401c4e40fb AS build

# clang compiles the Zstandard decoder in rustgis-data to WebAssembly; curl downloads Trunk and

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (comment accuracy, confidence high): the Dockerfile's curl does not fetch the Natural Earth data. crates/render/build.rs does that itself by running curl at build time (and panics without network unless RUSTGIS_DATA_DIR is set). The package is needed because build.rs shells out to it. Suggested wording:

Suggested change
# clang compiles the Zstandard decoder in rustgis-data to WebAssembly; curl downloads Trunk and
# clang compiles the Zstandard decoder in rustgis-data to WebAssembly; curl downloads Trunk, and
# crates/render/build.rs uses curl to fetch the Natural Earth sample data it embeds.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 58c8853: took your wording.

Comment thread Dockerfile Outdated
RUN case "$BUILDARCH" in \
amd64) arch=x86_64; sum=f2b4680cd239693a646a2795e4633c625328d7b2a044fbe749fa3a2fe9e7036b ;; \
arm64) arch=aarch64; sum=b1d8e60e454f7fc182d9a4d95d1506ffbae947d8ba90f8f6f02da93b60f980f9 ;; \
*) echo "no Trunk release for $BUILDARCH" >&2; exit 1 ;; \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (error message, confidence medium): if BUILDARCH is empty (a builder that doesn't predefine it, such as the legacy builder or some Podman/Buildah setups), this prints no Trunk release for with nothing after it. Naming the variable makes the failure diagnosable:

Suggested change
*) echo "no Trunk release for $BUILDARCH" >&2; exit 1 ;; \
*) echo "no Trunk release for '${BUILDARCH:-an unset platform}' (BUILDARCH is set by BuildKit)" >&2; exit 1 ;; \

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 58c8853: the message shows BUILDARCH's value, empty or not, and says BuildKit sets it.

Comment thread Dockerfile
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/src/target \
--mount=type=cache,target=/root/.cache/trunk \
trunk build --release --locked --public-url ./

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note (consistency, confidence medium): the header says the image builds "as web-deploy.yml does", but the deploy runs trunk build --release --public-url / without --locked. The --public-url ./ change is intentional. --locked is a reasonable addition, but the header should say it differs, or the deploy workflow should adopt it too. The PR also adds no CI job that builds this Dockerfile, so a broken Dockerfile won't be caught before a release (the PR lists this as a follow-up).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 58c8853: the header now lists --locked with the other two differences from web-deploy.yml. A CI job that builds the image is offered as a follow-up in the description.

@github-actions

Copy link
Copy Markdown
Contributor

Code review

Reviewed the Dockerfile, .dockerignore, nginx config, the Dependabot entry and the docs against the repository. I found no correctness or security bugs. The Dockerfile's Trunk checksums match web-deploy.yml, the dist path matches apps/rustgis-web/Trunk.toml, .dockerignore keeps .cargo (needed for cargo xtask), and the docs' anchors and links resolve. The image is non-root, has no secrets and copies only dist/web.

Bugs: none found.

Security: none found. The Trunk tarball is verified against a pinned SHA-256 before it is run, and nginx sets server_tokens off. The lack of X-Frame-Options is deliberate, as the PR explains.

Performance: none found. Cache mounts and the $BUILDPLATFORM stage avoid recompiling the WebAssembly per target.

Quality (posted inline, confidence in brackets):

  • The comment at Dockerfile:17 says curl downloads the Natural Earth data. crates/render/build.rs does that itself; the Dockerfile only installs curl for it [high].
  • Dockerfile:31: an empty BUILDARCH prints "no Trunk release for " with nothing after it. Naming the variable would help [medium].
  • Dockerfile:48: the header says the build is "as web-deploy.yml does", but the deploy omits --locked. No CI job builds this Dockerfile, so breakage won't be caught before a release (the PR lists this as a follow-up) [medium].
  • Not posted: build.rs downloads from assets.rustgis.app during docker build unless RUSTGIS_DATA_DIR is set, and the Dockerfile doesn't expose that option. Self-hosters on restricted networks need network access or a data-mount workaround, and docs/installation.md doesn't say so [medium].

AGENTS.md: The licence label (Apache-2.0) matches Cargo.toml. No assets, ribbon, i18n, layering or wasm-build rules are affected; the Dockerfile builds the wasm target in the same way CI does. No dependency changes, so cargo xtask notices isn't needed.

Change size

Compared with: the whole PR against main (merge base 6f4533e to PR head cf17971).

Measure Value
Files changed 11 (added 3, modified 8, removed 0, renamed 0)
Size before 105.4 KB (107,946 B)
Size after 113.7 KB (116,425 B)
Total size change +8.3 KB (+8,479 B)
Path Before After Change
Dockerfile 0 B 3.7 KB +3.7 KB
packaging/docker/nginx.conf 0 B 1.8 KB +1.8 KB
docs/installation.md 15.1 KB 16.8 KB +1.7 KB
.dockerignore 0 B 482 B +482 B
.github/dependabot.yml 1.9 KB 2.2 KB +344 B
packaging/README.md 3.4 KB 3.6 KB +221 B
docs/python.md 13.3 KB 13.4 KB +92 B
GEOLIBRE_PARITY.md 23.8 KB 23.8 KB +80 B
docs/comparison/qgis.md 15.3 KB 15.3 KB +14 B
ROADMAP.md 16.4 KB 16.4 KB −8 B
docs/comparison/index.md 16.3 KB 16.3 KB +6 B

@github-actions

Copy link
Copy Markdown
Contributor

📖 Docs preview

Item Value
Preview https://preview.rustgis.app/pr-974/
Web app https://preview.rustgis.app/pr-974/app/
Commit cf17971

GitHub Pages takes a minute or two to publish a new commit. Newer commits from the fork aren't published until a maintainer adds the preview label (once per commit).

The header lists --locked among the differences from web-deploy.yml, the apt comment says build.rs runs curl itself, and an unsupported or empty BUILDARCH is named in the error.
@giswqs
giswqs merged commit 67b2fe3 into opengeos:main Oct 26, 2026
10 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude-review Apply to run the Claude code review on this PR (remove and re-add to run again)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants