Dockerfile
Rama main · réplica de lectura del repositorio original.
# Two programs in one image: the web server, which `CMD` starts, and the map
# renderer, started instead as `node map-renderer/index.mjs` - the way the API
# image runs its worker with `arq`.
#
# Ubuntu 24.04 rather than Alpine, for the renderer: MapLibre Native ships Linux
# binaries for Ubuntu 24.04 and nothing else, and Debian's and Alpine's library
# names differ from the ones it links. Node is the official build, copied out of
# the official image rather than installed from a third-party apt repository,
# so one floating tag decides it. Both build stages share the base, so what
# `npm ci` installs is what runs.
FROM node:24-bookworm-slim AS node
FROM ubuntu:24.04 AS base
COPY --from=node /usr/local/bin/node /usr/local/bin/node
WORKDIR /app
# Build the application
#
# No separate production-deps stage: `output: "standalone"` (next.config.js)
# makes the build emit its own pruned `node_modules` into `.next/standalone`,
# which is what the runner copies, and the map renderer's build gathers the two
# native packages it loads into its own. A `npm ci --only=production` stage
# would be built on every image and then never used by anything.
FROM base AS builder
ARG DEBIAN_FRONTEND=noninteractive
# The certificates are for `npm ci`, and for the binary MapLibre Native's install
# script downloads from its GitHub release.
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=node /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/npm
RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
&& ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
COPY package*.json ./
RUN npm ci
COPY . .
# Set environment variables for build
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# An override, not a requirement. Left unset - which is how the published image is
# built - the bundle calls the relative `/api/v1` and this app's own route handler
# forwards it to `API_INTERNAL_URL`, a variable read at *runtime*. That is what makes one
# image work on any domain.
#
# Supply it (`--build-arg NEXT_PUBLIC_API_URL=https://api.example.com/api/v1` - the full
# base, `/api/v1` prefix included) only for a split-origin deployment where the browser
# should reach the API directly. `NEXT_PUBLIC_*` values are inlined into the client bundle
# at build time, so a rebuild is the only way to change it afterwards - the whole reason
# it is no longer the default path.
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
# Build the Next.js application, and the map renderer beside it
RUN npm run build
# Production image
FROM base AS runner
ARG DEBIAN_FRONTEND=noninteractive
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# `upgrade` as well as `install`, because a rebuild has to collect the Ubuntu
# security updates published since `ubuntu:24.04` was last cut, so this stage
# must never come from a build cache. Publish Image uses none at all.
#
# What MapLibre Native links against, and the X server it draws through: its
# Linux binary renders with GLX, so even a renderer with no screen needs one,
# and Mesa's software rasteriser is the GPU. The web server uses none of it.
RUN apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y --no-install-recommends \
libcurl4t64 libicu74 libjpeg-turbo8 libpng16-16t64 libuv1t64 libwebp7 \
libopengl0 libglx0 libglx-mesa0 libgl1-mesa-dri libx11-6 libxext6 \
xvfb \
&& rm -rf /var/lib/apt/lists/* \
# Where Xvfb puts its socket, made once by root so the renderer, which is not
# root, finds it already there.
&& mkdir -m 1777 /tmp/.X11-unix
# Create a non-root user
RUN groupadd --system --gid 1001 nodejs \
&& useradd --system --uid 1001 --gid nodejs nextjs
# The licences travel with the image, not just with the repository. AGPL-3.0 asks
# that the notice reach every copy conveyed, and NOTICE.md carries the third-party
# terms for the artwork and vendored builds baked into this one - an operator who
# only ever pulls the image would otherwise receive neither.
COPY --from=builder /app/LICENSE /app/NOTICE.md ./
# Copy built application
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/build/map-renderer ./map-renderer
# Switch to non-root user
USER nextjs
# Expose port
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
# Node's own `fetch` rather than curl or wget: this image carries neither, and
# installing one to ask a question the runtime can already ask is a package and a CVE
# surface for nothing. Written in exec form, so no shell is involved and the builder
# performs no substitution on it - `process.env.PORT` is read by node at run time, which
# keeps the check right for an instance that moved the port. The map renderer answers
# `/healthz` on the same `PORT`, so the one check serves either command.
#
# `--start-period` is what stops a slow first boot from counting as failures: within it a
# failing check delays `healthy` rather than counting toward `--retries`.
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD ["node", "-e", "fetch('http://127.0.0.1:' + (process.env.PORT || 3000) + '/healthz').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
# Start the application
CMD ["node", "server.js"]