Switch the user Docker build to the Montify Node serve style.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+5
-4
@@ -1,10 +1,11 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
.git
|
.git
|
||||||
.gitignore
|
.env.development
|
||||||
.env
|
.env.production
|
||||||
.env.local
|
.env.local
|
||||||
*.log
|
*.log
|
||||||
.vscode
|
.DS_Store
|
||||||
.idea
|
.idea
|
||||||
coverage
|
.fleet
|
||||||
|
.cursor
|
||||||
|
|||||||
+33
-11
@@ -1,17 +1,39 @@
|
|||||||
# داکپلوی: Build Type = Dockerfile ، پورت ۸۰
|
FROM docker.iranserver.com/node:20
|
||||||
FROM node:22-alpine AS build
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
RUN npm config set registry https://mirror2.chabokan.net/npm/
|
COPY scripts ./scripts
|
||||||
RUN npm ci
|
|
||||||
|
RUN npm config set registry https://mirror2.chabokan.net/npm/ \
|
||||||
|
&& npm config set replace-registry-host npmjs \
|
||||||
|
&& npm config set fetch-retry-mintimeout 10000 \
|
||||||
|
&& npm config set fetch-retry-maxtimeout 120000 \
|
||||||
|
&& npm config set fetch-timeout 120000 \
|
||||||
|
&& npm config set fetch-retries 5
|
||||||
|
|
||||||
|
ENV NODE_ENV=development
|
||||||
|
RUN npm ci --include=dev --foreground-scripts --registry=https://mirror2.chabokan.net/npm/
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
ARG VITE_API_BASE_URL=https://parche.montest.ir/server/api
|
|
||||||
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
|
|
||||||
RUN npm run build-only
|
|
||||||
|
|
||||||
FROM nginx:1.27-alpine
|
# نامها باید با Environment و Build-time Arguments در Dokploy یکی باشند
|
||||||
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
|
ARG VITE_API_BASE_URL
|
||||||
COPY --from=build /app/dist /usr/share/nginx/html
|
ARG VITE_USE_MOCK_AUTH
|
||||||
EXPOSE 80
|
ARG VITE_BASE_PATH
|
||||||
|
|
||||||
|
RUN node scripts/write-docker-vite-env.mjs \
|
||||||
|
&& unset VITE_API_BASE_URL VITE_USE_MOCK_AUTH VITE_BASE_PATH \
|
||||||
|
&& npm run build
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV VITE_BASE_PATH=/
|
||||||
|
ENV PORT=7676
|
||||||
|
|
||||||
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
EXPOSE 7676
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
CMD ["node", "scripts/serve-user.mjs"]
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
load_env_file() {
|
||||||
|
file="$1"
|
||||||
|
[ -f "$file" ] || return 0
|
||||||
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
|
case "$line" in
|
||||||
|
''|\#*) continue ;;
|
||||||
|
esac
|
||||||
|
key="${line%%=*}"
|
||||||
|
val="${line#*=}"
|
||||||
|
eval "current=\${$key-}"
|
||||||
|
if [ -z "$current" ]; then
|
||||||
|
export "$key=$val"
|
||||||
|
fi
|
||||||
|
done < "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
load_env_file /app/.env.production
|
||||||
|
load_env_file /app/.env
|
||||||
|
|
||||||
|
escape_sed() {
|
||||||
|
printf '%s\n' "$1" | sed -e 's/[\/&]/\\&/g' -e 's/\./\\\./g'
|
||||||
|
}
|
||||||
|
|
||||||
|
ESCAPED_API_BASE_URL=$(escape_sed "$VITE_API_BASE_URL")
|
||||||
|
|
||||||
|
if [ -d /app/dist ]; then
|
||||||
|
find /app/dist -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i \
|
||||||
|
-e "s/__VITE_API_BASE_URL__/${ESCAPED_API_BASE_URL}/g" \
|
||||||
|
{} \;
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import http from 'node:http'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const port = Number(process.env.PORT || 7676)
|
||||||
|
const distDir = path.resolve(process.env.DIST_DIR || path.join(__dirname, '..', 'dist'))
|
||||||
|
const rawBase = String(process.env.VITE_BASE_PATH ?? '/').trim() || '/'
|
||||||
|
const publicBase = rawBase === '/' ? '' : rawBase.replace(/\/+$/, '')
|
||||||
|
|
||||||
|
const MIME = {
|
||||||
|
'.html': 'text/html; charset=utf-8',
|
||||||
|
'.js': 'text/javascript; charset=utf-8',
|
||||||
|
'.mjs': 'text/javascript; charset=utf-8',
|
||||||
|
'.css': 'text/css; charset=utf-8',
|
||||||
|
'.json': 'application/json; charset=utf-8',
|
||||||
|
'.map': 'application/json; charset=utf-8',
|
||||||
|
'.svg': 'image/svg+xml',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.jpg': 'image/jpeg',
|
||||||
|
'.jpeg': 'image/jpeg',
|
||||||
|
'.gif': 'image/gif',
|
||||||
|
'.webp': 'image/webp',
|
||||||
|
'.ico': 'image/x-icon',
|
||||||
|
'.woff': 'font/woff',
|
||||||
|
'.woff2': 'font/woff2',
|
||||||
|
'.ttf': 'font/ttf',
|
||||||
|
'.txt': 'text/plain; charset=utf-8',
|
||||||
|
'.webmanifest': 'application/manifest+json',
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendFile(res, filePath) {
|
||||||
|
const type = MIME[path.extname(filePath).toLowerCase()] || 'application/octet-stream'
|
||||||
|
res.writeHead(200, { 'Content-Type': type, 'Cache-Control': 'public, max-age=0' })
|
||||||
|
fs.createReadStream(filePath).pipe(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendIndex(res) {
|
||||||
|
sendFile(res, path.join(distDir, 'index.html'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveSafe(urlPath) {
|
||||||
|
const decoded = decodeURIComponent(urlPath.split('?')[0] || '/')
|
||||||
|
const relative = publicBase && decoded.startsWith(`${publicBase}/`)
|
||||||
|
? decoded.slice(publicBase.length)
|
||||||
|
: publicBase && decoded === publicBase
|
||||||
|
? '/'
|
||||||
|
: decoded
|
||||||
|
const target = path.resolve(distDir, `.${relative}`)
|
||||||
|
if (!target.startsWith(distDir)) return null
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
const url = req.url || '/'
|
||||||
|
const pathname = decodeURIComponent(url.split('?')[0] || '/')
|
||||||
|
|
||||||
|
if (pathname === '/health' || (publicBase && pathname === `${publicBase}/health`)) {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/plain' })
|
||||||
|
return res.end('ok')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (publicBase && pathname === publicBase) {
|
||||||
|
res.writeHead(302, { Location: `${publicBase}/` })
|
||||||
|
return res.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (publicBase && pathname === '/') {
|
||||||
|
res.writeHead(302, { Location: `${publicBase}/` })
|
||||||
|
return res.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = resolveSafe(url)
|
||||||
|
if (target && fs.existsSync(target) && fs.statSync(target).isFile()) {
|
||||||
|
return sendFile(res, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendIndex(res)
|
||||||
|
})
|
||||||
|
|
||||||
|
server.listen(port, '0.0.0.0', () => {
|
||||||
|
console.log(`fabric-trade user listening on ${port} → ${publicBase || ''}/`)
|
||||||
|
})
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
||||||
|
|
||||||
|
const keys = ['VITE_API_BASE_URL', 'VITE_USE_MOCK_AUTH', 'VITE_BASE_PATH']
|
||||||
|
|
||||||
|
function parseEnv(text) {
|
||||||
|
const out = {}
|
||||||
|
for (const line of text.split(/\r?\n/)) {
|
||||||
|
const trimmed = line.trim()
|
||||||
|
if (!trimmed || trimmed.startsWith('#')) continue
|
||||||
|
const index = trimmed.indexOf('=')
|
||||||
|
if (index <= 0) continue
|
||||||
|
const key = trimmed.slice(0, index).trim()
|
||||||
|
let value = trimmed.slice(index + 1)
|
||||||
|
if (
|
||||||
|
(value.startsWith('"') && value.endsWith('"')) ||
|
||||||
|
(value.startsWith("'") && value.endsWith("'"))
|
||||||
|
) {
|
||||||
|
value = value.slice(1, -1)
|
||||||
|
}
|
||||||
|
out[key] = value
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
const merged = {}
|
||||||
|
|
||||||
|
if (existsSync('.env')) {
|
||||||
|
Object.assign(merged, parseEnv(readFileSync('.env', 'utf8')))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = process.env[key]
|
||||||
|
if (value) merged[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!merged.VITE_API_BASE_URL) {
|
||||||
|
merged.VITE_API_BASE_URL = 'https://parche.montest.ir/server/api'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!merged.VITE_USE_MOCK_AUTH) {
|
||||||
|
merged.VITE_USE_MOCK_AUTH = 'false'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!merged.VITE_BASE_PATH || merged.VITE_BASE_PATH === '/') {
|
||||||
|
merged.VITE_BASE_PATH = '/'
|
||||||
|
} else if (!merged.VITE_BASE_PATH.endsWith('/')) {
|
||||||
|
merged.VITE_BASE_PATH = `${merged.VITE_BASE_PATH}/`
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = Object.entries(merged)
|
||||||
|
.filter(([key]) => key.startsWith('VITE_'))
|
||||||
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
|
|
||||||
|
writeFileSync('.env.production', `${lines.join('\n')}\n`)
|
||||||
|
console.log('Vite build env:')
|
||||||
|
for (const line of lines) console.log(` ${line}`)
|
||||||
Reference in New Issue
Block a user