Files
shopdb-flask/Dockerfile
benjamin.baker ed2a47e7f9 install frontend devDependencies explicitly in the build stage
vite and the build toolchain live under devDependencies; proxied/base-image
npm defaults to omit=dev, leaving 'vite: not found' at build time. Force
--include=dev so the build stage always has its toolchain.
2026-09-02 08:13:21 -04:00

130 lines
5.4 KiB
Docker

# shopdb-flask single-site container.
#
# One image, one site. Per ADR-004, each adopting facility runs its own
# stack with its own DB, secrets, and enabled-plugin list. This image
# bundles all 13 catalog plugins (computers, employees, geenforce,
# knowledgebase, machines, measuringtools, network, notifications,
# printedparts, printers, slides, usb, warranty); a site installs + enables
# the ones it wants with `flask plugin install <name>` (or, declaratively,
# `flask plugin apply-profile <profile.json>`). Per ADR-013 a future lean
# build stages only the chosen plugin directories into this image.
#
# The frontend is built in a first stage and its dist output is copied into
# the final image so Flask can serve the SPA (register_frontend_routes in
# shopdb/__init__.py resolves <repo>/frontend/dist).
#
# Build:
# docker build -t shopdb-flask .
# Run (with .env):
# docker run --env-file .env -p 5001:5001 shopdb-flask
# ---- Stage 1: build the Vue frontend ----
FROM node:20-slim AS frontendbuild
WORKDIR /build
# Optionally trust the site's corporate root CA so npm's TLS to the registry
# validates behind an inspecting proxy. The .pem is gitignored and absent from a
# clean checkout, so the copy is made optional by pairing it with a file that
# always exists; the cert is installed only when actually provided. Without it
# npm 10.x behind an inspecting proxy fails silently and leaves an incomplete
# node_modules; a clean build with no proxy is unaffected.
COPY corp-root-ca.pem* frontend/package.json /tmp/corpca/
# node:20-slim ships without the ca-certificates package (no update-ca-certificates
# and no /usr/local/share/ca-certificates), so install it before trusting the cert.
RUN if [ -f /tmp/corpca/corp-root-ca.pem ]; then \
apt-get update && apt-get install -y --no-install-recommends ca-certificates && \
rm -rf /var/lib/apt/lists/* && \
cp /tmp/corpca/corp-root-ca.pem /usr/local/share/ca-certificates/corp-root-ca.crt && \
update-ca-certificates; \
fi
# Copy only the manifests first so `npm ci` caches on dependency changes.
COPY frontend/package*.json ./frontend/
# Trust the corp CA for npm's TLS only when it was actually installed above. The
# guard keeps NODE_EXTRA_CA_CERTS unset on a clean checkout (no cert), so Node
# does not warn about a missing cert path; a proxy-inspected build still gets
# the cert because the file is present.
# Force devDependencies in: vite and the rest of the build toolchain live under
# devDependencies, and some base images / proxied environments default npm to
# omit=dev, which leaves `vite: not found` at build time. A build stage always
# needs them, so include them explicitly regardless of the ambient NODE_ENV.
RUN if [ -f /usr/local/share/ca-certificates/corp-root-ca.crt ]; then \
export NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/corp-root-ca.crt; \
fi; \
cd frontend && npm ci --include=dev
# The frontend keeps its repo-relative layout here, because npm prebuild runs
# `node ../scripts/stage-frontend.mjs`, which stages each plugin's frontend/ into
# the Vite tree and codegens routes.gen.js. That script resolves its repo root as
# its own directory's parent, so it needs scripts/ and plugins/ as SIBLINGS of
# frontend/. Building with frontend/ flattened to the stage root made `../scripts`
# resolve to /scripts and the build failed on a missing module.
COPY scripts/stage-frontend.mjs ./scripts/
COPY plugins/ ./plugins/
COPY frontend/ ./frontend/
RUN if [ -f /usr/local/share/ca-certificates/corp-root-ca.crt ]; then \
export NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/corp-root-ca.crt; \
fi; \
cd frontend && npm run build
# Output lands in /build/frontend/dist (Vite default), copied into the final
# stage below.
# ---- Stage 2: Python application image ----
FROM python:3.14-slim AS base
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential \
default-libmysqlclient-dev \
pkg-config \
curl \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt \
&& pip install --no-cache-dir gunicorn
COPY shopdb/ ./shopdb/
COPY plugins/ ./plugins/
COPY migrations/ ./migrations/
COPY scripts/ ./scripts/
COPY wsgi.py ./
# Built SPA from stage 1. Flask serves it via register_frontend_routes.
COPY --from=frontendbuild /build/frontend/dist ./frontend/dist
# Create instance/ IN THE IMAGE, owned by the app user, before the chown below.
# Docker seeds an empty named volume from the image's content at the mountpoint,
# ownership included. Without this the mountpoint is created root-owned 0755 and
# the container, which runs as shopdb, cannot write plugins.json or any upload
# into its own instance directory.
RUN mkdir -p /app/instance
RUN useradd --create-home --shell /bin/bash shopdb \
&& chown -R shopdb:shopdb /app
USER shopdb
EXPOSE 5001
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl --silent http://localhost:5001/api/auth/login -X POST \
-H "Content-Type: application/json" -d '{}' \
| grep -q "VALIDATION_ERROR" || exit 1
CMD ["gunicorn", \
"--bind", "0.0.0.0:5001", \
"--workers", "4", \
"--timeout", "60", \
"--access-logfile", "-", \
"--error-logfile", "-", \
"wsgi:app"]