Docker: multi-stage backend, npm ci, healthcheck fix, same-origin API
- backend/Dockerfile: split builder/runner, runtime libs only, non-root app user, curl for healthcheck, --frozen via new uv.lock - frontend/Dockerfile: npm ci instead of npm install (deterministic), USER node - docker-compose.prod.yml: working backend healthcheck (curl now in image), redis healthcheck, drop dead NEXT_PUBLIC_API_BASE_URL env (Next.js bakes NEXT_PUBLIC_* at build time, runtime override is no-op) - docker-compose.yml: same redis healthcheck, BACKEND_URL for next.config rewrites, drop uv from CMD (not in new image) - Caddyfile: handle (not handle_path) so /api prefix is preserved into FastAPI router - next.config.ts: rewrite /api/* and /health to BACKEND_URL in dev (no Caddy locally) - frontend/src/lib/api.ts: empty default = same-origin relative URLs - Makefile: drop uv run from migrate target - Add backend/uv.lock and frontend/package-lock.json for reproducible builds Verified: docker build succeeds for both, backend container starts, /health responds, curl healthcheck works inside image, container runs as non-root. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c8b2cc8152
commit
e22aeb9b5a
12 changed files with 14236 additions and 46 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -17,3 +17,6 @@ node_modules/
|
|||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
|
||||
# Claude Code local state
|
||||
.claude/
|
||||
|
|
|
|||
|
|
@ -7,12 +7,14 @@
|
|||
# Activates only after DNS A-record is set on this domain.
|
||||
#
|
||||
# Replace gendesign.example.ru with your actual domain when bought.
|
||||
#
|
||||
# `handle /api/*` (NOT `handle_path`) — we keep the /api prefix because
|
||||
# the FastAPI router is mounted at /api/v1/*.
|
||||
|
||||
:80 {
|
||||
encode zstd gzip
|
||||
|
||||
# Backend API
|
||||
handle_path /api/* {
|
||||
handle /api/* {
|
||||
reverse_proxy backend:8000
|
||||
}
|
||||
|
||||
|
|
@ -29,7 +31,7 @@
|
|||
gendesign.example.ru {
|
||||
encode zstd gzip
|
||||
|
||||
handle_path /api/* {
|
||||
handle /api/* {
|
||||
reverse_proxy backend:8000
|
||||
}
|
||||
|
||||
|
|
|
|||
2
Makefile
2
Makefile
|
|
@ -24,7 +24,7 @@ backend-shell:
|
|||
docker compose exec backend bash
|
||||
|
||||
migrate:
|
||||
docker compose exec backend uv run alembic upgrade head
|
||||
docker compose exec backend alembic upgrade head
|
||||
|
||||
test:
|
||||
cd backend && uv run pytest -q
|
||||
|
|
|
|||
|
|
@ -1,37 +1,59 @@
|
|||
FROM python:3.12-slim
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
# ---- builder ----
|
||||
FROM python:3.12-slim AS builder
|
||||
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PIP_NO_CACHE_DIR=1
|
||||
UV_COMPILE_BYTECODE=1 \
|
||||
UV_LINK_MODE=copy
|
||||
|
||||
# Geo + WeasyPrint system deps
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
libpq-dev \
|
||||
libgdal-dev \
|
||||
libproj-dev \
|
||||
libgeos-dev \
|
||||
libcairo2 \
|
||||
libpango-1.0-0 \
|
||||
libpangoft2-1.0-0 \
|
||||
fonts-dejavu-core \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN pip install --no-cache-dir uv
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY pyproject.toml ./
|
||||
COPY uv.lock* ./
|
||||
RUN uv sync --no-dev
|
||||
COPY pyproject.toml uv.lock* ./
|
||||
RUN if [ -f uv.lock ]; then uv sync --frozen --no-dev; else uv sync --no-dev; fi
|
||||
|
||||
COPY app ./app
|
||||
|
||||
# Alembic migrations are added in Stage 2a (DB models). Until then these COPY are skipped.
|
||||
# When alembic/ exists, uncomment:
|
||||
# COPY alembic.ini ./
|
||||
# COPY alembic ./alembic
|
||||
|
||||
# ---- runner ----
|
||||
FROM python:3.12-slim AS runner
|
||||
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
# Runtime libs only — no -dev / no build-essential.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libpq5 \
|
||||
libgdal36 \
|
||||
libproj25 \
|
||||
libgeos-c1t64 \
|
||||
libcairo2 \
|
||||
libpango-1.0-0 \
|
||||
libpangoft2-1.0-0 \
|
||||
fonts-dejavu-core \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& useradd --create-home --uid 1000 app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder --chown=app:app /app/.venv /app/.venv
|
||||
COPY --from=builder --chown=app:app /app/app /app/app
|
||||
|
||||
USER app
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
|
|
|
|||
2165
backend/uv.lock
generated
Normal file
2165
backend/uv.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -5,7 +5,10 @@
|
|||
# Required env in /opt/gendesign/.env:
|
||||
# POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD
|
||||
# IMAGE_TAG (defaults to "latest")
|
||||
# NEXT_PUBLIC_API_BASE_URL (e.g. https://your-domain or empty for same-origin)
|
||||
#
|
||||
# In production the browser talks to Caddy on :80/:443.
|
||||
# Caddy proxies /api/* and /health straight to backend, the rest to frontend.
|
||||
# So the frontend uses same-origin relative URLs — no NEXT_PUBLIC_API_BASE_URL needed.
|
||||
#
|
||||
# Postgres + Redis run alongside the app on the same VM (Discovery mode).
|
||||
# Volumes are shared with docker-compose.yml so switching between files preserves data.
|
||||
|
|
@ -35,6 +38,11 @@ services:
|
|||
restart: unless-stopped
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
|
||||
backend:
|
||||
image: ghcr.io/lekss361/gendesign-backend:${IMAGE_TAG:-latest}
|
||||
|
|
@ -48,16 +56,15 @@ services:
|
|||
ports:
|
||||
- "127.0.0.1:8000:8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||
test: ["CMD", "curl", "-fsS", "http://localhost:8000/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
|
||||
frontend:
|
||||
image: ghcr.io/lekss361/gendesign-frontend:${IMAGE_TAG:-latest}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL:-}
|
||||
ports:
|
||||
- "127.0.0.1:3000:3000"
|
||||
depends_on:
|
||||
|
|
|
|||
|
|
@ -21,6 +21,11 @@ services:
|
|||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
|
||||
backend:
|
||||
build: ./backend
|
||||
|
|
@ -35,22 +40,14 @@ services:
|
|||
volumes:
|
||||
- ./backend/app:/app/app
|
||||
command:
|
||||
[
|
||||
"uv",
|
||||
"run",
|
||||
"uvicorn",
|
||||
"app.main:app",
|
||||
"--host",
|
||||
"0.0.0.0",
|
||||
"--port",
|
||||
"8000",
|
||||
"--reload",
|
||||
]
|
||||
["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
||||
|
||||
frontend:
|
||||
build: ./frontend
|
||||
# BACKEND_URL is read by next.config.ts rewrites (/api/* -> backend).
|
||||
# The browser hits Next.js on :3000 with relative paths, Next.js proxies to backend.
|
||||
environment:
|
||||
NEXT_PUBLIC_API_BASE_URL: http://localhost:8000
|
||||
BACKEND_URL: http://backend:8000
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
|
|
|
|||
|
|
@ -1,27 +1,35 @@
|
|||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
# ---- deps ----
|
||||
FROM node:20-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* ./
|
||||
# --legacy-peer-deps bypasses peer dep mismatches (Tailwind 4 alpha + React 19).
|
||||
# We use `npm install` instead of `npm ci` because we don't commit package-lock.json yet.
|
||||
# Once `npm install` runs locally and a lockfile is committed, switch back to `npm ci`.
|
||||
RUN npm install --legacy-peer-deps --no-audit --no-fund
|
||||
COPY package.json package-lock.json ./
|
||||
# --legacy-peer-deps: Tailwind 4 alpha + React 19 peer-dep mismatches.
|
||||
RUN npm ci --legacy-peer-deps --no-audit --no-fund
|
||||
|
||||
|
||||
# ---- builder ----
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
RUN npm run build
|
||||
|
||||
|
||||
# ---- runner ----
|
||||
FROM node:20-alpine AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production \
|
||||
NEXT_TELEMETRY_DISABLED=1 \
|
||||
PORT=3000
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
# `node` user (uid 1000) ships with the alpine image.
|
||||
COPY --from=builder --chown=node:node /app/public ./public
|
||||
COPY --from=builder --chown=node:node /app/.next/standalone ./
|
||||
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
|
||||
|
||||
USER node
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["node", "server.js"]
|
||||
|
|
|
|||
|
|
@ -3,6 +3,17 @@ import type { NextConfig } from "next";
|
|||
const nextConfig: NextConfig = {
|
||||
reactStrictMode: true,
|
||||
output: "standalone",
|
||||
// In dev (no Caddy) Next.js itself proxies /api/* and /health to the backend
|
||||
// so the browser can use same-origin relative URLs.
|
||||
// In prod Caddy intercepts these paths before they reach Next.js,
|
||||
// so these rewrites are effectively a no-op there.
|
||||
async rewrites() {
|
||||
const backend = process.env.BACKEND_URL ?? "http://localhost:8000";
|
||||
return [
|
||||
{ source: "/api/:path*", destination: `${backend}/api/:path*` },
|
||||
{ source: "/health", destination: `${backend}/health` },
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
|
|||
11969
frontend/package-lock.json
generated
Normal file
11969
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,6 @@
|
|||
export const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8000";
|
||||
// Empty default = same-origin relative URLs.
|
||||
// In prod Caddy proxies /api/* to backend; in dev Next.js rewrites do the same.
|
||||
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
|
||||
|
||||
export async function apiFetch<T>(
|
||||
path: string,
|
||||
|
|
|
|||
|
|
@ -46,3 +46,8 @@ pg pass 2J2SBPMKuS998fiwhtQqDhMI
|
|||
|
||||
ssh -i $HOME\.ssh\gendesign_deploy gendesign@46.173.16.127
|
||||
|
||||
pat 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
|
||||
|
||||
echo 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| docker login ghcr.io -u lekss361 --password-stdin
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue