Repository navigation
Expand file tree
/
Copy pathDockerfile
More file actions
195 lines (173 loc) · 10.6 KB
/
Copy pathDockerfile
File metadata and controls
195 lines (173 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# Run the build stage on the builder's native platform: the output is
# arch-independent static files, so emulating arm64 with QEMU here only
# slows down multi-arch builds without changing the result.
# ($BUILDPLATFORM is a Docker-provided automatic ARG, set by BuildKit.)
FROM --platform=$BUILDPLATFORM node:22-bookworm-slim AS build
# Debian rather than Alpine because this stage now also runs the JupyterLite
# build, whose Python dependency tree (jupyterlab, notebook, jupyterlite-core)
# resolves to prebuilt manylinux wheels here instead of compiling against musl.
# Only apps/geolibre-desktop/dist is copied into the runtime image, so the larger
# builder costs nothing in the shipped image.
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 python3-venv \
&& rm -rf /var/lib/apt/lists/*
# A venv keeps pip off the Debian-managed interpreter, which refuses installs
# under PEP 668.
ENV VIRTUAL_ENV=/opt/jupyterlite
RUN python3 -m venv "$VIRTUAL_ENV"
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
WORKDIR /app
# Copy every workspace member's package.json before npm ci so the install
# layer is cached. Adding a new package under apps/ or packages/ requires
# adding its package.json here, or npm ci fails with a missing workspace.
COPY package.json package-lock.json ./
COPY patches patches
COPY scripts/apply-dependency-patches.mjs scripts/apply-dependency-patches.mjs
COPY apps/geolibre-desktop/package.json apps/geolibre-desktop/package.json
COPY packages/core/package.json packages/core/package.json
COPY packages/collab-core/package.json packages/collab-core/package.json
COPY packages/map/package.json packages/map/package.json
COPY packages/plugins/package.json packages/plugins/package.json
COPY packages/processing/package.json packages/processing/package.json
COPY packages/ui/package.json packages/ui/package.json
RUN npm ci
# Its own layer, keyed only on the requirements file, so touching app source does
# not reinstall the Python tree.
COPY apps/geolibre-desktop/jupyterlite/requirements.txt apps/geolibre-desktop/jupyterlite/requirements.txt
RUN pip install --no-cache-dir -r apps/geolibre-desktop/jupyterlite/requirements.txt
COPY . .
ARG GEOLIBRE_APP_BASE=/
ARG VITE_GEE_OAUTH_CLIENT_ID=
ARG VITE_MAPILLARY_ACCESS_TOKEN=
# Set to 1 (or true) to disable the first-launch welcome wizard for the whole
# deployment; visitors land straight on the map.
ARG VITE_WELCOME_DISABLED=
# Comma-separated origins allowed to drive a framed app over the embed
# postMessage API. Usually set at RUN time instead (-e GEOLIBRE_EMBED_ORIGINS=…),
# which the entrypoint writes into the runtime config without a rebuild.
ARG VITE_GEOLIBRE_EMBED_ORIGINS=
# Self-hosted project sharing server (https://…, or "off" to remove Share and the
# Project Gallery). Unset uses the public hosted service. Like the embed origins,
# normally set at RUN time instead (-e GEOLIBRE_SHARE_URL=…) so a prebuilt image
# can be repointed without a rebuild.
ARG VITE_GEOLIBRE_SHARE_URL=
# Self-hosted collaboration relay (wss://…). Unset leaves collaboration dark.
# Also settable at RUN time (-e GEOLIBRE_COLLAB_URL=…).
ARG VITE_GEOLIBRE_COLLAB_URL=
# Name shown at the start of the toolbar and in the browser tab, replacing
# "GeoLibre". Also settable at RUN time (-e GEOLIBRE_APP_NAME=…).
ARG VITE_GEOLIBRE_APP_NAME=
# GeoLens catalog default. Also settable at RUN time
# (-e GEOLIBRE_GEOLENS_URL=...).
ARG VITE_GEOLENS_DEFAULT_URL=same-origin
# Set to 1 to strip every external CDN reference (unpkg.com, cdn.jsdelivr.net,
# …) from the build output, for deployments that may not load third-party
# hosts. Features that depend on CDN-hosted assets are disabled or degraded —
# see docs/self-hosting.md. Build-time only: the flag is baked into the bundle,
# so it cannot be flipped at RUN time the way the embed/share/collab URLs can.
ARG GEOLIBRE_NO_EXTERNAL_CDN=
# Comma-separated list of the deployment capabilities the interface may offer
# (project:edit, data:add, processing:run, export:data, plugins:install,
# settings:manage), or "none" to grant nothing — for a kiosk or classroom
# instance. Unset grants everything, so an existing build is unchanged. See
# docs/deployment-capabilities.md. Build-time default; at run time
# -e GEOLIBRE_CAPABILITIES=... overrides it on a prebuilt image through the
# deployment.json and runtime config the entrypoint writes (issue #2783).
ARG VITE_GEOLIBRE_CAPABILITIES=
ENV GEOLIBRE_APP_BASE=${GEOLIBRE_APP_BASE}
ENV VITE_GEE_OAUTH_CLIENT_ID=${VITE_GEE_OAUTH_CLIENT_ID}
ENV VITE_MAPILLARY_ACCESS_TOKEN=${VITE_MAPILLARY_ACCESS_TOKEN}
ENV VITE_WELCOME_DISABLED=${VITE_WELCOME_DISABLED}
ENV VITE_GEOLIBRE_EMBED_ORIGINS=${VITE_GEOLIBRE_EMBED_ORIGINS}
ENV VITE_GEOLIBRE_SHARE_URL=${VITE_GEOLIBRE_SHARE_URL}
ENV VITE_GEOLIBRE_COLLAB_URL=${VITE_GEOLIBRE_COLLAB_URL}
ENV VITE_GEOLIBRE_APP_NAME=${VITE_GEOLIBRE_APP_NAME}
ENV VITE_GEOLENS_DEFAULT_URL=${VITE_GEOLENS_DEFAULT_URL}
ENV GEOLIBRE_NO_EXTERNAL_CDN=${GEOLIBRE_NO_EXTERNAL_CDN}
ENV VITE_GEOLIBRE_CAPABILITIES=${VITE_GEOLIBRE_CAPABILITIES}
# The `prebuild` hook of apps/geolibre-desktop runs scripts/build-jupyterlite.mjs,
# which generates the site the Notebook panel embeds. That script is best-effort
# by default: without the `jupyter lite` CLI it warns and exits 0, and the build
# still succeeds. nginx then answers the panel's iframe URL with index.html
# through its SPA fallback, so the panel renders a second copy of GeoLibre
# instead of a notebook, which is how this image shipped for so long
# (GeoLibre#1851). Make the absence fatal here rather than silent.
ENV GEOLIBRE_JUPYTERLITE_REQUIRED=1
RUN npm run build
# The site is generated, not committed (public/jupyterlite/ is git-ignored), so
# assert it reached the output rather than trusting the step above.
RUN test -f apps/geolibre-desktop/dist/jupyterlite/lab/index.html \
|| (echo "ERROR: dist/jupyterlite is missing; the Notebook panel would render a second copy of the app." && exit 1)
# Runtime image bundles the static web app (served by nginx) and the optional
# Python conversion/Whitebox sidecar (uvicorn), reverse-proxied at /sidecar.
# A glibc base (not alpine/musl) is required for the prebuilt geo wheels
# (duckdb, rasterio/rio-cogeo, freestiler, whitebox-workflows).
FROM python:3.12-slim-bookworm AS runtime
# TARGETARCH is provided by BuildKit (amd64 / arm64).
ARG TARGETARCH
# libexpat1 is a runtime dependency of rasterio (pulled in by rio-cogeo) that
# the slim base image does not ship. openssl provides `openssl passwd` used by
# entrypoint.sh to hash the optional Basic Auth password.
RUN apt-get update \
&& apt-get install -y --no-install-recommends nginx libexpat1 openssl \
&& rm -rf /var/lib/apt/lists/* \
&& rm -f /etc/nginx/sites-enabled/default
# Install the sidecar package plus the core conversion stack. duckdb and
# rio-cogeo (rasterio) publish linux/arm64 wheels, so Vector->GeoParquet,
# CSV->GeoParquet and Raster->COG work on both architectures.
COPY backend/geolibre_server /opt/geolibre_server
RUN pip install --no-cache-dir /opt/geolibre_server \
&& pip install --no-cache-dir "duckdb>=1.1.0" "rio-cogeo>=5.0.0"
# freestiler (PMTiles) and whitebox-workflows publish no linux/arm64 wheels, so
# they are installed on amd64 only. On arm64 those tools report unavailable
# while the other conversions keep working.
RUN if [ "$TARGETARCH" = "amd64" ]; then \
pip install --no-cache-dir "freestiler>=0.1.0" "whitebox-workflows>=2.0.2"; \
else \
echo "Skipping freestiler + whitebox-workflows on $TARGETARCH (no wheels)"; \
fi
# Point the sidecar at this interpreter so it skips the managed-runtime
# bootstrap and uses the prebaked packages. Confine conversion reads/writes to
# /data by default: the sidecar is reachable same-origin through the nginx
# proxy, so without this an arbitrary same-origin caller could read or
# overwrite container paths. Mount input files at /data (read-write for
# outputs); override GEOLIBRE_CONVERSION_ROOTS to widen or disable.
ENV GEOLIBRE_CONVERSION_PYTHON=/usr/local/bin/python \
WBW_EXTERNAL_PYTHON=/usr/local/bin/python \
GEOLIBRE_CONVERSION_ROOTS=/data
RUN mkdir -p /data
# For the same reason, the sidecar's PostGIS endpoints refuse every destination
# until GEOLIBRE_POSTGIS_HOSTS names the allowed databases (comma-separated
# `host` or `host:port`) — otherwise a same-origin caller could aim them at
# hosts only this container can reach. Deliberately left unset: set it at
# `docker run` time to enable PostGIS, or `*` to accept any connection string.
# SQL Server uses the same destination allowlist via GEOLIBRE_MSSQL_HOSTS; it is
# intentionally unset in this image. This image also does not install pyodbc or
# Microsoft ODBC Driver 18: operators must add both in a derived image.
# WARNING: docker/nginx.conf's CSP allows http://localhost:* / http://127.0.0.1:*
# (and ws:// equivalents) in connect-src for local-dev data sources (PMTiles/COGs
# from a dev server on another port). This image is intended for local/single-user
# use; on a public host those allowances let the served JS probe each visitor's
# loopback. Drop them from the CSP before publishing publicly.
# Ship nginx.conf as an immutable template (not loaded directly). entrypoint.sh
# renders it to /etc/nginx/conf.d/default.conf on every boot, substituting the
# per-launch sidecar token, so a container restart never keeps a stale token.
COPY docker/nginx.conf /etc/nginx/nginx.conf.template
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
COPY docker/deployment_policy.py /usr/local/lib/geolibre/deployment_policy.py
COPY docker/sidecar_policy.py /usr/local/lib/geolibre/sidecar_policy.py
RUN chmod +x /usr/local/bin/entrypoint.sh \
# Default auth snippet (disabled). entrypoint.sh rewrites it at start based
# on GEOLIBRE_AUTH_USER/GEOLIBRE_AUTH_PASSWORD; baking a valid default keeps
# `nginx -t` and non-entrypoint invocations working.
&& printf '# Basic Auth disabled (GEOLIBRE_AUTH_USER/GEOLIBRE_AUTH_PASSWORD not set).\n' > /etc/nginx/geolibre-auth.conf \
&& printf '# AI proxy disabled (GEOLIBRE_AI_URL not set).\n' > /etc/nginx/geolibre-ai-proxy.conf \
&& printf '# Sidecar guards generated at boot.\n' > /etc/nginx/geolibre-sidecar-guards.conf \
&& printf '# Sidecar off switch generated at boot.\n' > /etc/nginx/geolibre-sidecar-off.conf
COPY --from=build /app/apps/geolibre-desktop/dist /usr/share/nginx/html
EXPOSE 80
# /healthz is exempt from the optional Basic Auth, so the check keeps passing
# when GEOLIBRE_AUTH_USER/GEOLIBRE_AUTH_PASSWORD are set.
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1/healthz', timeout=4).status==200 else 1)" || exit 1
CMD ["/usr/local/bin/entrypoint.sh"]