Ctrl K
ComunidadGuías…

Dockerfile

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

Archivo original ↗Preguntar a KiBot
# Bun only installs packages (from package-lock.json); Node runs the app.
FROM oven/bun:1.4.4-alpine@sha256:4cf82f5686b196504fe119db3fcd9185b191f82ece82c40680e597228ec6070a AS bun

# Build stage
FROM node:25-alpine@sha256:bdf2cca6fe3dabd014ea60163eca3f0f7015fbd5c7ee1b0e9ccb4ced6eb02ef4 AS builder

WORKDIR /app
COPY --from=bun /usr/local/bin/bun /usr/local/bin/bun

# Copy dependency manifests first for better caching
COPY backend/package*.json ./backend/
COPY frontend/package*.json ./frontend/
# postinstall runs it after intlayer build
COPY frontend/scripts/build-locale-packs.mjs ./frontend/scripts/

# Install dependencies for both apps
WORKDIR /app/backend
RUN --mount=type=cache,target=/root/.bun/install/cache bun install --frozen-lockfile --network-concurrency=16
WORKDIR /app/frontend
RUN --mount=type=cache,target=/root/.bun/install/cache bun install --frozen-lockfile --network-concurrency=16
WORKDIR /app

# Copy source code
COPY backend ./backend
COPY frontend ./frontend

# Build frontend with API proxy baked in
ARG API_PORT=4000
ENV API_PORT=${API_PORT}
ARG API_PROXY_TARGET=http://127.0.0.1:${API_PORT}
ENV API_PROXY_TARGET=${API_PROXY_TARGET}
ENV NEXT_PUBLIC_API_URL=/api/v1
# Product name; the frontend bakes it in, the backend reads APP_NAME at runtime
ARG APP_NAME="Stoic Budget"
ENV NEXT_PUBLIC_APP_NAME="${APP_NAME}"
RUN npm run build --prefix frontend

# Build backend
RUN npm run build --prefix backend

# Reinstall backend with production deps only for the runtime image
WORKDIR /app/backend
RUN --mount=type=cache,target=/root/.bun/install/cache \
  rm -rf node_modules && bun install --frozen-lockfile --network-concurrency=16 --production
WORKDIR /app

# Runtime image
FROM node:25-alpine@sha256:bdf2cca6fe3dabd014ea60163eca3f0f7015fbd5c7ee1b0e9ccb4ced6eb02ef4 AS runner

WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV API_PORT=4000
ENV HOST=0.0.0.0
ENV PIP_BREAK_SYSTEM_PACKAGES=1

# Install Python + pdfplumber for PDF parsing + pdf2image for thumbnails.
# Pillow comes from requirements.txt rather than Alpine's py3-pillow so the
# pinned version is the one that actually ships.
# `apk upgrade` takes OS security fixes newer than the base image (e.g. OpenSSL).
COPY backend/requirements.txt ./backend/requirements.txt
RUN apk upgrade --no-cache && \
    apk add --no-cache python3 py3-pip poppler-utils && \
    pip3 install --no-cache-dir --break-system-packages --require-hashes -r ./backend/requirements.txt

# The runtime only runs `node`. npm and corepack, with npm's bundled tar,
# sigstore and pacote, would only add image-scan findings.
RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/corepack \
    /usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack

# Non-root runtime user. Files are copied in already owned by it: a `chown -R`
# after the copies would store every file a second time in its own layer.
RUN addgroup -S app && adduser -S app -G app && chown -R app:app /app

# Backend runtime dependencies (already pruned)
COPY --chown=app:app --from=builder /app/backend/package*.json ./backend/
COPY --chown=app:app --from=builder /app/backend/node_modules ./backend/node_modules

# Backend runtime files
COPY --chown=app:app --from=builder /app/backend/dist ./backend/dist
COPY --chown=app:app --from=builder /app/backend/scripts ./backend/scripts

# Frontend standalone output (vinext); static files, public/ included, live in dist/client
COPY --chown=app:app --from=builder /app/frontend/dist/standalone/server.js /app/frontend/dist/standalone/package.json ./frontend/
COPY --chown=app:app --from=builder /app/frontend/dist/standalone/dist ./frontend/dist
COPY --chown=app:app --from=builder /app/frontend/dist/standalone/node_modules ./frontend/node_modules

# Startup script to run both servers
COPY --chown=app:app --chmod=755 scripts/start.sh ./scripts/start.sh

USER app

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
  CMD ["node", "-e", "const http=require('http'); const port=process.env.API_PORT||4000; http.get('http://127.0.0.1:'+port+'/api/v1/health/ready', (r)=>process.exit(r.statusCode===200?0:1)).on('error', ()=>process.exit(1));"]

CMD ["./scripts/start.sh"]