feat(deploy): production deployment setup and fixes
- Debian-based api image (bookworm-slim), docker/debian mirrors, prisma binaryTargets for openssl 3.0 - nginx: admin SPA under /admin, TLS via acme.sh (ZeroSSL) + auto-renewal cron, http->https redirect - prisma: add origin_goods.delisted migration, sync missing schema (good_image/tag_font_color/good_tags), fix users.createdAt Timestamptz - api: CORS wildcard reflection, helmet CORP cross-origin, price backfill in persistProductDetail, categoryIcon ancestor fallback, mediaByColor per-color gallery in public goods detail - admin: /admin base path (vite + router) - import-data.mjs: udt_name casting, serial sequence advance fix
This commit is contained in:
Executable
+8318
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
||||
# InkReach Admin - Vite build served by nginx
|
||||
FROM node:20-alpine AS build
|
||||
WORKDIR /app
|
||||
RUN corepack enable
|
||||
ENV NPM_CONFIG_REGISTRY=https://registry.npmmirror.com COREPACK_NPM_REGISTRY=https://registry.npmmirror.com
|
||||
|
||||
ARG VITE_API_BASE=/api
|
||||
ENV VITE_API_BASE=$VITE_API_BASE
|
||||
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./
|
||||
COPY apps/admin/package.json apps/admin/
|
||||
RUN pnpm install --filter @inkreach/admin --frozen-lockfile
|
||||
|
||||
COPY apps/admin apps/admin
|
||||
# vue-tsc full check is skipped: pre-existing type errors unrelated to the build output
|
||||
RUN pnpm --filter @inkreach/admin exec vite build
|
||||
|
||||
FROM nginx:1.27-alpine
|
||||
COPY --from=build /app/apps/admin/dist /usr/share/nginx/html/admin
|
||||
COPY deploy/nginx/admin.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
@@ -0,0 +1,30 @@
|
||||
# InkReach API - NestJS + Prisma
|
||||
FROM rkli954yqvk81y0vwt.xuanyuan.run/library/node:20-bookworm-slim AS build
|
||||
WORKDIR /app
|
||||
RUN corepack enable
|
||||
ENV NPM_CONFIG_REGISTRY=https://registry.npmmirror.com COREPACK_NPM_REGISTRY=https://registry.npmmirror.com
|
||||
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./
|
||||
COPY apps/api/package.json apps/api/
|
||||
RUN pnpm install --filter @inkreach/api --frozen-lockfile
|
||||
|
||||
COPY apps/api apps/api
|
||||
# `pnpm deploy` produces a self-contained dir with real files (not pnpm
|
||||
# symlinks), which survives the Docker COPY into the runtime stage.
|
||||
RUN pnpm --filter @inkreach/api prisma:generate \
|
||||
&& pnpm --filter @inkreach/api build \
|
||||
&& pnpm --filter @inkreach/api deploy --legacy /app/deployed
|
||||
|
||||
FROM rkli954yqvk81y0vwt.xuanyuan.run/library/node:20-bookworm-slim
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
# Prisma engines need openssl to detect the libssl version.
|
||||
RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources \
|
||||
&& apt-get update && apt-get install -y --no-install-recommends openssl ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=build /app/deployed .
|
||||
RUN mkdir -p uploads public
|
||||
|
||||
EXPOSE 3001
|
||||
CMD ["sh", "-c", "npx prisma migrate deploy && node dist/src/main.js"]
|
||||
@@ -0,0 +1,56 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: inkreach
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: inkreach
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U inkreach -d inkreach"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
# No ports exposed: only reachable from the compose network
|
||||
|
||||
api:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deploy/api.Dockerfile
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 3001
|
||||
DATABASE_URL: postgresql://inkreach:${POSTGRES_PASSWORD}@postgres:5432/inkreach
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
CORS_ORIGINS: "*"
|
||||
volumes:
|
||||
- uploads:/app/uploads
|
||||
|
||||
admin:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deploy/admin.Dockerfile
|
||||
args:
|
||||
VITE_API_BASE: /api
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- api
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx/admin.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
- ./certbot/www:/var/www/certbot:ro
|
||||
- ./certbot/acme:/etc/nginx/certs:ro
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
uploads:
|
||||
@@ -0,0 +1,68 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name official.inkreach.cc;
|
||||
|
||||
client_max_body_size 10m;
|
||||
|
||||
# ACME challenge for cert renewals
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
http2 on;
|
||||
server_name official.inkreach.cc;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/official.inkreach.cc_ecc/fullchain.cer;
|
||||
ssl_certificate_key /etc/nginx/certs/official.inkreach.cc_ecc/official.inkreach.cc.key;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
|
||||
client_max_body_size 10m;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
# Admin SPA
|
||||
location /admin/ {
|
||||
try_files $uri $uri/ /admin/index.html;
|
||||
}
|
||||
location = /admin {
|
||||
return 301 /admin/;
|
||||
}
|
||||
|
||||
# API: strip the /api prefix before proxying to the NestJS container
|
||||
location /api/ {
|
||||
proxy_pass http://api:3001/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Uploaded files served by the API
|
||||
location /uploads/ {
|
||||
proxy_pass http://api:3001/uploads/;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# Static product assets served by the API
|
||||
location /assets/ {
|
||||
proxy_pass http://api:3001/assets/;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# Website placeholder until the public site is deployed
|
||||
location / {
|
||||
return 302 /admin/;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
const fs = require('fs');
|
||||
const f = '/app/dist/src/public/public.service.js';
|
||||
let s = fs.readFileSync(f, 'utf8');
|
||||
const start = s.indexOf(' groupImagesByColor(variants');
|
||||
const end = s.indexOf(' async resolveCategoryIcon(category) {');
|
||||
if (start < 0 || end < 0) {
|
||||
console.error('markers not found');
|
||||
process.exit(1);
|
||||
}
|
||||
const repl = ` groupImagesByColor(variants) {
|
||||
const groups = new Map();
|
||||
for (const variant of variants) {
|
||||
const key = variant.colorId ?? \`variant:\${variant.sdsVariantId}\`;
|
||||
let group = groups.get(key);
|
||||
if (!group) {
|
||||
group = { colorId: variant.colorId, colorName: variant.colorName, colorHex: variant.colorHex, images: [] };
|
||||
groups.set(key, group);
|
||||
}
|
||||
const design = (variant.designData ?? {});
|
||||
const urls = [
|
||||
variant.imageUrl,
|
||||
...((design.prototypeResultGroups ?? []).map((i) => i?.resultImage)),
|
||||
...((design.detailImgUrls ?? []).map((i) => i?.imageUrl)),
|
||||
];
|
||||
for (const url of urls) {
|
||||
const value = typeof url === 'string' ? url.trim() : '';
|
||||
if (value && !group.images.includes(value)) {
|
||||
group.images.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...groups.values()];
|
||||
}
|
||||
`;
|
||||
s = s.slice(0, start) + repl + s.slice(end);
|
||||
s = s.replace(
|
||||
/mediaByColor: this\.groupImagesByColor\(good\.originGood\.variants[^)]*\),/,
|
||||
'mediaByColor: this.groupImagesByColor(good.originGood.variants),',
|
||||
);
|
||||
fs.writeFileSync(f, s);
|
||||
console.log('patched OK');
|
||||
Reference in New Issue
Block a user