Ctrl K
ComunidadGuías…

Dockerfile

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# TravStats - Combined Frontend + Backend Dockerfile
# This creates a single container with both the web UI and API

# Version argument (pass via: docker build --build-arg VERSION=$(cat backend/VERSION) .)
ARG VERSION=0.0.0-dev

# Stage 1: Build Frontend
FROM node:22-alpine AS frontend-builder

WORKDIR /app/frontend

COPY frontend/package*.json ./
RUN npm config set fetch-retries 5 \
    && npm config set fetch-retry-mintimeout 30000 \
    && npm config set fetch-retry-maxtimeout 300000 \
    && npm ci

COPY frontend/ ./
# Build without VITE_API_URL - frontend will use relative path /api/v1
ENV VITE_API_URL=
RUN npm run build

# Stage 2: Build Backend
FROM node:22-alpine AS backend-builder

WORKDIR /app/backend

COPY backend/package*.json ./
COPY backend/prisma ./prisma/
# bcrypt downloads a prebuilt binary from a GitHub release and falls back to
# compiling from source when that fails. The fallback needs python3 and a
# toolchain, which node:22-alpine does not carry — so a flaky CDN became a hard
# build failure: measured 2026-08-12, one request in three for
# bcrypt_lib-v5.1.1-napi-v3-linux-x64-musl.tar.gz ended in a socket hang up.
# 2.6.0-beta.7 lost that roll; 2.5.2-rc.4 an hour earlier had won it. npm's
# fetch-retries below do not cover node-pre-gyp's own download, so they never
# helped. These packages make the documented fallback actually work. They live
# in the builder stage and never reach the production image.
RUN apk add --no-cache python3 make g++
RUN npm config set fetch-retries 5 \
    && npm config set fetch-retry-mintimeout 30000 \
    && npm config set fetch-retry-maxtimeout 300000 \
    && npm ci

COPY backend/ ./
RUN rm -rf dist
RUN npx prisma generate
RUN npm run build

# Stage 2b: production dependencies, resolved on the SAME libc as the runtime.
#
# This exists for one reason: bcrypt has no pure-JS path. It downloads a
# prebuilt binary from a GitHub release and compiles from source when that
# fails, and the download is measurably flaky — one request in three ended in a
# socket hang up on 2026-08-12, which killed two consecutive 2.6.0-beta.7
# builds. The fallback needs python3 and a toolchain, and putting those in the
# shipped image would mean paying in size and attack surface for a build-time
# problem. Resolving the dependencies here and copying only `node_modules`
# forward keeps the compiler out of production while making the fallback real.
#
# bookworm-slim, not alpine: the native binary must match the runtime's libc,
# so this stage has to be the same base as the production stage below.
FROM node:22-bookworm-slim AS prod-deps

WORKDIR /app/backend

COPY backend/package*.json ./
COPY backend/prisma ./prisma/
COPY backend/prisma.config.ts ./
RUN apt-get update \
    && apt-get install -y --no-install-recommends python3 make g++ \
    && rm -rf /var/lib/apt/lists/*
RUN npm config set fetch-retries 5 \
    && npm config set fetch-retry-mintimeout 30000 \
    && npm config set fetch-retry-maxtimeout 300000 \
    && npm ci --only=production

# Stage 3: Production - Combined Container
# Pinned to bookworm-slim so apt sources are deterministic.
FROM node:22-bookworm-slim AS production

# Re-declare ARG so it's available in this stage
ARG VERSION=0.0.0-dev

# OCI image labels
LABEL org.opencontainers.image.title="TravStats"
LABEL org.opencontainers.image.description="Personal flight tracking and statistics"
LABEL org.opencontainers.image.version="${VERSION}"
LABEL org.opencontainers.image.source="https://github.com/Abrechen2/TravStats"
LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later"

WORKDIR /app

# Install nginx, supervisor, and runtime dependencies.
# Security: apt-get upgrade pulls the latest Debian security patches on top
# of the base image (closes CVEs that accumulate between base-image rebuilds).
RUN apt-get update && \
    apt-get upgrade -y --no-install-recommends && \
    apt-get install -y --no-install-recommends \
    nginx \
    supervisor \
    openssl \
    ca-certificates \
    wget \
    netcat-openbsd \
    postgresql-client \
    && rm -rf /var/lib/apt/lists/*

# Setup Backend
WORKDIR /app/backend
COPY backend/package*.json ./
COPY backend/prisma ./prisma/
# Prisma 7 dropped `--schema` and `--url` and stopped reading `.env`: the CLI
# takes the schema path and the datasource URL from this file, so `prisma
# migrate deploy` in the entrypoint has no datasource without it.
COPY backend/prisma.config.ts ./
# Dependencies come from the prod-deps stage above rather than a second
# `npm ci` here, so this image never needs a compiler. Same base image, so the
# bcrypt binary in there matches this runtime's libc.
COPY --from=prod-deps /app/backend/node_modules ./node_modules
COPY --from=backend-builder /app/backend/dist ./dist
# tsc compiles .ts files only — non-TS assets (CSV seed data) need an
# explicit copy so seedPortsFromCSV / seedShipsFromCSV find them at
# dist/seedData/*.csv. Without this, both seeders silently skip with
# `csv_missing` on first boot, leaving the cruise/ferry domain unable
# to resolve port + ship references.
COPY --from=backend-builder /app/backend/src/seedData ./dist/seedData
# The bundled v2 parser templates (plan 2026-10-09 P4a): every issuer reader
# — hotel chains, portals, airlines — is a JSON template now, not code, and
# this snapshot is what a fresh or offline instance reads with. tsc does not
# copy JSON, and the loader treats a missing directory as an empty snapshot,
# so without this line every such confirmation silently reads as nothing
# until the first successful template sync. Guarded by
# services/parsers/templates/v2/__tests__/snapshot.dockerfile.test.ts.
COPY --from=backend-builder /app/backend/src/services/parsers/templates/v2/snapshot ./dist/services/parsers/templates/v2/snapshot
# schematicRouter resolves the fine land-mask via __dirname relative to
# its compiled location (dist/services/...), landing at /app/backend/data/.
# Without this copy, /api/v1/cruises/geometry(/batch) returns 500 with
# ENOENT on every request, so cruise paths never render on the globe.
COPY backend/data/land-mask.bin ./data/land-mask.bin
# Vendored Eurostat marnet shipping-lane graph used by the marnet
# pathfinder (services/marnet/marnetGraph.ts). 1.6 MB GeoJSON, ~6 k
# nodes / ~7.6 k edges. Without this file the marnet router throws
# ENOENT on first call and every cruise leg falls back to the coarse
# 1° A*, which cuts across narrow Baltic and Adriatic straits.
COPY backend/data/marnet/marnet.geojson ./data/marnet/marnet.geojson
# River anchor polylines (data/rivers/river_<name>.json), read by
# services/cruiseDistance/riverCalculator.ts via __dirname from
# dist/services/cruiseDistance to /app/backend/data/rivers. The calculator
# treats a missing directory as "no rivers" with a single warn, so without
# this line every river leg is measured as the chord and drawn as one — which
# is how prod stored a whole Nile cruise as `haversine` (2026-10-16). Guarded
# by services/cruiseDistance/__tests__/inlandLeg.test.ts.
COPY backend/data/rivers ./data/rivers
# Vendored Natural Earth 1:10m country outlines (public domain), read by
# services/geo/countryBoundaries.ts via __dirname from dist/services/geo/ to
# /app/backend/data/countries. 10.2 MB, 245 polygonal features, 239 ISO codes.
# This is the ONLY way an instance can turn a GPS point into a country: §8.1 of
# the country-counting design refuses to ask Dawarich, whose reverse geocoder
# most self-hosters do not run, so a missing file is not a degraded answer but
# no answer at all — the loader throws ENOENT on first use, exactly like the
# marnet graph above. Guarded by services/geo/__tests__/countryBoundaries.test.ts,
# which reads this file and fails if the line disappears.
COPY backend/data/countries/countries-10m.geojson ./data/countries/countries-10m.geojson
# Vendored airline logos (soaring-symbols, MIT) — the KEYLESS DEFAULT tier of
# the logo chain (services/airlineLogo/vendoredLogos.ts), resolved via __dirname
# from dist/services/... to /app/backend/data/airline-logos. 1.4 MB, 93 airlines.
# Without this copy the tier silently disappears in production and every logo
# falls through to the external Daisycon hotlink — the exact regression this
# feature exists to remove. It fails soft (a warn, not a crash), so nothing but
# this comment will tell you.
COPY backend/data/airline-logos ./data/airline-logos
# Vendored OpenFlights airline + aircraft seed data (data/openflights/*.dat),
# consumed by the boot seeders (seedAirlinesFromData / seedAircraftFromData).
# Without these the airline/aircraft tables seed empty and the logo lookup
# degrades to placeholders.
COPY backend/data/openflights/airlines.dat ./data/openflights/airlines.dat
COPY backend/data/openflights/planes.dat ./data/openflights/planes.dat
# Vendored rail station catalogue (Trainline stations.csv, ODbL 1.0, filtered
# by scripts/build-rail-stations.mjs; 1.2 MB gzipped, ~53 k stations) plus its
# licence file, read by the boot seeder (seedRailStations.ts) via __dirname
# from dist/ to /app/backend/data/rail. Without it the seeder skips with
# `file_missing` and the station picker only ever finds geocoder results.
# The same directory carries station_codes.csv (DB short codes, CC0, built by
# scripts/build-rail-station-codes.mjs); without it every station answers
# shortCode null and the boot log warns `seed_rail_station_codes_missing`.
COPY backend/data/rail ./data/rail
# Rental-provider catalogue (forgejo#196): suggestions for the provider field
# and the website each provider's logo is looked up on. Read via __dirname from
# dist/services/rentalProviders; without it there are no suggestions and every
# rental row shows its monogram.
COPY backend/data/rental ./data/rental
# Developer scripts, copied as-is. NOTE: these are .ts files and the image
# has neither tsx nor the src/ tree they import from, so they do NOT run
# here — that was forgejo#108, where the documented backfill could not
# resolve `../src/services/co2Calculator`. Anything an operator is meant
# to run inside the container lives in backend/src/scripts/ instead, is
# compiled with the app, and runs as
# `node /app/backend/dist/scripts/<name>.js` (see package.json backfill:*).
COPY backend/scripts ./scripts
# No `prisma generate` here any more. Prisma 7's `prisma-client` generator
# emits plain TypeScript, which the backend-builder stage compiled into
# dist/generated/prisma along with everything else — the client ships as build
# output. Generating again would write an unused copy under a src/ tree this
# image does not have.

# Write VERSION file for runtime version reporting.
# Uses the build-arg (1.0.0, 1.0.0-rc.6, …) so RC / prerelease images
# surface their suffix in Admin → System-Info and the About page,
# rather than the "base" version stored in backend/VERSION.
RUN echo "${VERSION}" > ./VERSION

# Setup Frontend (nginx will serve these files)
WORKDIR /app/frontend
COPY --from=frontend-builder /app/frontend/dist ./dist

# Nginx configuration (Debian uses sites-available)
COPY nginx-combined.conf /etc/nginx/sites-available/default
RUN rm -f /etc/nginx/sites-enabled/default && \
    ln -s /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default

# Supervisor configuration (Debian uses conf.d)
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf

# Startup script
COPY docker-entrypoint.sh /docker-entrypoint.sh
# Fix line endings (CRLF -> LF) and make executable
RUN sed -i 's/\r$//' /docker-entrypoint.sh && \
    chmod +x /docker-entrypoint.sh

# Boot-race wrapper: nginx waits for backend /health before serving.
# See scripts/wait-then-nginx.sh for the why and the env knobs.
COPY scripts/wait-then-nginx.sh /app/scripts/wait-then-nginx.sh
RUN sed -i 's/\r$//' /app/scripts/wait-then-nginx.sh && \
    chmod +x /app/scripts/wait-then-nginx.sh

# Create data directory for persistent config (logs, secrets, backups, …)
# Secrets live at /app/data/secrets — one mounted volume covers everything.
# The subdirectory gets 0700 so it's not world-readable even if someone
# overrides the parent mode from the host.
RUN mkdir -p /app/data/logs /app/data/secrets && \
    mkdir -p /var/log/supervisor /var/log/nginx /var/lib/nginx && \
    chown -R www-data:www-data /var/log/nginx /var/lib/nginx && \
    chown -R node:node /app && \
    chmod -R 755 /app/data && \
    chmod 700 /app/data/secrets

# Single volume — data, logs, backups, secrets all live underneath /app/data
VOLUME ["/app/data"]

EXPOSE 80

# 180s start-period covers the longest observed boot path:
# Prisma generate (~5s) + migrate deploy (~10s) + closed-airport
# backfill (~30s) + airport-seed first install (~90s) + Express
# listen (~3s) + the wait-then-nginx poll loop (~2s).
HEALTHCHECK --interval=30s --timeout=3s --start-period=180s \
  CMD wget --no-verbose --tries=1 --spider http://localhost/health || exit 1

ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]