260211:1703 First Deploy (Not Complete)
Some checks failed
Spec Validation / validate-markdown (push) Has been cancelled
Spec Validation / validate-diagrams (push) Has been cancelled
Spec Validation / check-todos (push) Has been cancelled

This commit is contained in:
admin
2026-02-11 17:03:38 +07:00
parent d9df4e66b4
commit 912f379016
11 changed files with 98 additions and 91 deletions

View File

@@ -43,8 +43,8 @@ COPY backend/ ./backend/
# Build NestJS → backend/dist # Build NestJS → backend/dist
RUN cd backend && pnpm run build RUN cd backend && pnpm run build
# Prune dev dependencies # Deploy with production deps only (pnpm workspace isolation)
RUN pnpm prune --prod --filter backend... RUN pnpm --filter backend deploy --prod --legacy /app/backend-prod
# ========================= # =========================
# Stage 3: Production Runtime # Stage 3: Production Runtime
@@ -65,8 +65,8 @@ RUN addgroup -g 1001 -S nestjs && \
# Copy production artifacts only # Copy production artifacts only
COPY --from=build --chown=nestjs:nestjs /app/backend/dist ./dist COPY --from=build --chown=nestjs:nestjs /app/backend/dist ./dist
COPY --from=build --chown=nestjs:nestjs /app/backend/node_modules ./node_modules COPY --from=build --chown=nestjs:nestjs /app/backend-prod/node_modules ./node_modules
COPY --from=build --chown=nestjs:nestjs /app/backend/package.json ./ COPY --from=build --chown=nestjs:nestjs /app/backend-prod/package.json ./
# Create uploads directory (Two-Phase Storage) # Create uploads directory (Two-Phase Storage)
RUN mkdir -p /app/uploads/temp /app/uploads/permanent && \ RUN mkdir -p /app/uploads/temp /app/uploads/permanent && \

View File

@@ -56,6 +56,7 @@
"cache-manager-redis-yet": "^5.1.5", "cache-manager-redis-yet": "^5.1.5",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.2", "class-validator": "^0.14.2",
"express": "^5.1.0",
"fs-extra": "^11.3.2", "fs-extra": "^11.3.2",
"helmet": "^8.1.0", "helmet": "^8.1.0",
"ioredis": "^5.8.2", "ioredis": "^5.8.2",

View File

@@ -18,21 +18,26 @@ import { ForbiddenException } from '@nestjs/common'; // ✅ Import เพิ่
@Injectable() @Injectable()
export class FileStorageService { export class FileStorageService {
private readonly logger = new Logger(FileStorageService.name); private readonly logger = new Logger(FileStorageService.name);
private readonly uploadRoot: string; private readonly tempDir: string;
private readonly permanentDir: string;
constructor( constructor(
@InjectRepository(Attachment) @InjectRepository(Attachment)
private attachmentRepository: Repository<Attachment>, private attachmentRepository: Repository<Attachment>,
private configService: ConfigService, private configService: ConfigService
) { ) {
// ใช้ Path จริงถ้าอยู่บน Server (Production) หรือใช้ ./uploads ถ้าอยู่ Local // ใช้ env vars จาก docker-compose สำหรับ Production
this.uploadRoot = // ถ้าไม่ได้กำหนดจะ fallback เป็น ./uploads/temp และ ./uploads/permanent
this.configService.get('NODE_ENV') === 'production' this.tempDir =
? '/share/dms-data' this.configService.get<string>('UPLOAD_TEMP_DIR') ||
: path.join(process.cwd(), 'uploads'); path.join(process.cwd(), 'uploads', 'temp');
this.permanentDir =
this.configService.get<string>('UPLOAD_PERMANENT_DIR') ||
path.join(process.cwd(), 'uploads', 'permanent');
// สร้างโฟลเดอร์ temp รอไว้เลยถ้ายังไม่มี // สร้างโฟลเดอร์ temp และ permanent รอไว้เลยถ้ายังไม่มี
fs.ensureDirSync(path.join(this.uploadRoot, 'temp')); fs.ensureDirSync(this.tempDir);
fs.ensureDirSync(this.permanentDir);
} }
/** /**
@@ -42,7 +47,7 @@ export class FileStorageService {
const tempId = uuidv4(); const tempId = uuidv4();
const fileExt = path.extname(file.originalname); const fileExt = path.extname(file.originalname);
const storedFilename = `${uuidv4()}${fileExt}`; const storedFilename = `${uuidv4()}${fileExt}`;
const tempPath = path.join(this.uploadRoot, 'temp', storedFilename); const tempPath = path.join(this.tempDir, storedFilename);
// 1. คำนวณ Checksum (SHA-256) เพื่อความปลอดภัยและความถูกต้องของไฟล์ // 1. คำนวณ Checksum (SHA-256) เพื่อความปลอดภัยและความถูกต้องของไฟล์
const checksum = this.calculateChecksum(file.buffer); const checksum = this.calculateChecksum(file.buffer);
@@ -89,7 +94,7 @@ export class FileStorageService {
// แจ้งเตือนแต่อาจจะไม่ throw ถ้าต้องการให้ process ต่อไปได้บางส่วน (ขึ้นอยู่กับ business logic) // แจ้งเตือนแต่อาจจะไม่ throw ถ้าต้องการให้ process ต่อไปได้บางส่วน (ขึ้นอยู่กับ business logic)
// แต่เพื่อความปลอดภัยควรแจ้งว่าไฟล์ไม่ครบ // แต่เพื่อความปลอดภัยควรแจ้งว่าไฟล์ไม่ครบ
this.logger.warn( this.logger.warn(
`Expected ${tempIds.length} files to commit, but found ${attachments.length}`, `Expected ${tempIds.length} files to commit, but found ${attachments.length}`
); );
throw new NotFoundException('Some files not found or already committed'); throw new NotFoundException('Some files not found or already committed');
} }
@@ -100,7 +105,7 @@ export class FileStorageService {
const month = (today.getMonth() + 1).toString().padStart(2, '0'); const month = (today.getMonth() + 1).toString().padStart(2, '0');
// โฟลเดอร์ถาวรแยกตาม ปี/เดือน // โฟลเดอร์ถาวรแยกตาม ปี/เดือน
const permanentDir = path.join(this.uploadRoot, 'permanent', year, month); const permanentDir = path.join(this.permanentDir, year, month);
await fs.ensureDir(permanentDir); await fs.ensureDir(permanentDir);
for (const att of attachments) { for (const att of attachments) {
@@ -122,16 +127,16 @@ export class FileStorageService {
} else { } else {
this.logger.error(`File missing during commit: ${oldPath}`); this.logger.error(`File missing during commit: ${oldPath}`);
throw new NotFoundException( throw new NotFoundException(
`File not found on disk: ${att.originalFilename}`, `File not found on disk: ${att.originalFilename}`
); );
} }
} catch (error) { } catch (error) {
this.logger.error( this.logger.error(
`Failed to move file from ${oldPath} to ${newPath}`, `Failed to move file from ${oldPath} to ${newPath}`,
error, error
); );
throw new BadRequestException( throw new BadRequestException(
`Failed to commit file: ${att.originalFilename}`, `Failed to commit file: ${att.originalFilename}`
); );
} }
} }
@@ -144,7 +149,7 @@ export class FileStorageService {
* ดึงไฟล์มาเป็น Stream เพื่อส่งกลับไปให้ Controller * ดึงไฟล์มาเป็น Stream เพื่อส่งกลับไปให้ Controller
*/ */
async download( async download(
id: number, id: number
): Promise<{ stream: fs.ReadStream; attachment: Attachment }> { ): Promise<{ stream: fs.ReadStream; attachment: Attachment }> {
// 1. ค้นหาข้อมูลไฟล์จาก DB // 1. ค้นหาข้อมูลไฟล์จาก DB
const attachment = await this.attachmentRepository.findOne({ const attachment = await this.attachmentRepository.findOne({
@@ -191,7 +196,7 @@ export class FileStorageService {
// (ในอนาคตอาจเพิ่มเงื่อนไข OR User เป็น Admin/Document Control) // (ในอนาคตอาจเพิ่มเงื่อนไข OR User เป็น Admin/Document Control)
if (attachment.uploadedByUserId !== userId) { if (attachment.uploadedByUserId !== userId) {
this.logger.warn( this.logger.warn(
`User ${userId} tried to delete file ${id} owned by ${attachment.uploadedByUserId}`, `User ${userId} tried to delete file ${id} owned by ${attachment.uploadedByUserId}`
); );
throw new ForbiddenException('You are not allowed to delete this file'); throw new ForbiddenException('You are not allowed to delete this file');
} }
@@ -202,13 +207,13 @@ export class FileStorageService {
await fs.remove(attachment.filePath); await fs.remove(attachment.filePath);
} else { } else {
this.logger.warn( this.logger.warn(
`File not found on disk during deletion: ${attachment.filePath}`, `File not found on disk during deletion: ${attachment.filePath}`
); );
} }
} catch (error) { } catch (error) {
this.logger.error( this.logger.error(
`Failed to delete file from disk: ${attachment.filePath}`, `Failed to delete file from disk: ${attachment.filePath}`,
error, error
); );
throw new BadRequestException('Failed to delete file from storage'); throw new BadRequestException('Failed to delete file from storage');
} }

View File

@@ -1,7 +1,4 @@
import { TypeOrmModuleOptions } from '@nestjs/typeorm'; import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { config } from 'dotenv';
config();
export const databaseConfig: TypeOrmModuleOptions = { export const databaseConfig: TypeOrmModuleOptions = {
type: 'mysql', type: 'mysql',

View File

@@ -48,8 +48,10 @@ async function bootstrap() {
app.use(json({ limit: '50mb' })); app.use(json({ limit: '50mb' }));
app.use(urlencoded({ extended: true, limit: '50mb' })); app.use(urlencoded({ extended: true, limit: '50mb' }));
// 🌐 4. Global Prefix // 🌐 4. Global Prefix (ยกเว้น /health, /metrics สำหรับ monitoring)
app.setGlobalPrefix('api'); app.setGlobalPrefix('api', {
exclude: ['health', 'metrics'],
});
// ⚙️ 5. Global Pipes & Interceptors & Filters // ⚙️ 5. Global Pipes & Interceptors & Filters
app.useGlobalPipes( app.useGlobalPipes(

View File

@@ -1,4 +1,11 @@
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts", "documentation"] "exclude": [
"node_modules",
"test",
"dist",
"scripts",
"**/*spec.ts",
"documentation"
]
} }

BIN
lcbp3-backend.tar Normal file

Binary file not shown.

BIN
lcbp3-frontend.tar Normal file

Binary file not shown.

54
pnpm-lock.yaml generated
View File

@@ -109,6 +109,9 @@ importers:
class-validator: class-validator:
specifier: ^0.14.2 specifier: ^0.14.2
version: 0.14.3 version: 0.14.3
express:
specifier: ^5.1.0
version: 5.1.0
fs-extra: fs-extra:
specifier: ^11.3.2 specifier: ^11.3.2
version: 11.3.2 version: 11.3.2
@@ -4708,6 +4711,10 @@ packages:
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
cors@2.8.6:
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
engines: {node: '>= 0.10'}
cosmiconfig@8.3.6: cosmiconfig@8.3.6:
resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -7312,10 +7319,6 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
send@1.2.0:
resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==}
engines: {node: '>= 18'}
send@1.2.1: send@1.2.1:
resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
@@ -9790,7 +9793,7 @@ snapshots:
chokidar: 3.6.0 chokidar: 3.6.0
colors: 1.4.0 colors: 1.4.0
connect: 3.7.0 connect: 3.7.0
cors: 2.8.5 cors: 2.8.6
event-stream: 4.0.1 event-stream: 4.0.1
faye-websocket: 0.11.4 faye-websocket: 0.11.4
http-auth: 4.1.9 http-auth: 4.1.9
@@ -13476,6 +13479,11 @@ snapshots:
object-assign: 4.1.1 object-assign: 4.1.1
vary: 1.1.2 vary: 1.1.2
cors@2.8.6:
dependencies:
object-assign: 4.1.1
vary: 1.1.2
cosmiconfig@8.3.6(typescript@5.9.3): cosmiconfig@8.3.6(typescript@5.9.3):
dependencies: dependencies:
import-fresh: 3.3.1 import-fresh: 3.3.1
@@ -13949,8 +13957,8 @@ snapshots:
'@typescript-eslint/parser': 8.48.0(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/parser': 8.48.0(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1 eslint: 8.57.1
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1)
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1) eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1)
@@ -13973,7 +13981,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1): eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1):
dependencies: dependencies:
'@nolyfill/is-core-module': 1.0.39 '@nolyfill/is-core-module': 1.0.39
debug: 4.4.3 debug: 4.4.3
@@ -13984,22 +13992,22 @@ snapshots:
tinyglobby: 0.2.15 tinyglobby: 0.2.15
unrs-resolver: 1.11.1 unrs-resolver: 1.11.1
optionalDependencies: optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
dependencies: dependencies:
debug: 3.2.7 debug: 3.2.7
optionalDependencies: optionalDependencies:
'@typescript-eslint/parser': 8.48.0(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/parser': 8.48.0(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1 eslint: 8.57.1
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
dependencies: dependencies:
'@rtsao/scc': 1.1.0 '@rtsao/scc': 1.1.0
array-includes: 3.1.9 array-includes: 3.1.9
@@ -14010,7 +14018,7 @@ snapshots:
doctrine: 2.1.0 doctrine: 2.1.0
eslint: 8.57.1 eslint: 8.57.1
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
hasown: 2.0.2 hasown: 2.0.2
is-core-module: 2.16.1 is-core-module: 2.16.1
is-glob: 4.0.3 is-glob: 4.0.3
@@ -14295,7 +14303,7 @@ snapshots:
qs: 6.14.0 qs: 6.14.0
range-parser: 1.2.1 range-parser: 1.2.1
router: 2.2.0 router: 2.2.0
send: 1.2.0 send: 1.2.1
serve-static: 2.2.0 serve-static: 2.2.0
statuses: 2.0.2 statuses: 2.0.2
type-is: 2.0.1 type-is: 2.0.1
@@ -16492,22 +16500,6 @@ snapshots:
semver@7.7.3: {} semver@7.7.3: {}
send@1.2.0:
dependencies:
debug: 4.4.3
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
fresh: 2.0.0
http-errors: 2.0.1
mime-types: 3.0.2
ms: 2.1.3
on-finished: 2.4.1
range-parser: 1.2.1
statuses: 2.0.2
transitivePeerDependencies:
- supports-color
send@1.2.1: send@1.2.1:
dependencies: dependencies:
debug: 4.4.3 debug: 4.4.3
@@ -16547,7 +16539,7 @@ snapshots:
encodeurl: 2.0.0 encodeurl: 2.0.0
escape-html: 1.0.3 escape-html: 1.0.3
parseurl: 1.3.3 parseurl: 1.3.3
send: 1.2.0 send: 1.2.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color

View File

@@ -22,25 +22,25 @@
## 1. Build Docker Images ## 1. Build Docker Images
### Option A: Build บน Dev Machine แล้ว Transfer ### Option A: Build บน Dev Machine (Windows) แล้ว Transfer
```bash ```powershell
# อยู่ที่ workspace root (nap-dms.lcbp3/) # อยู่ที่ workspace root (nap-dms.lcbp3/)
# Build Backend # Build Backend
docker build -f backend/Dockerfile -t lcbp3-backend:latest . docker build -f backend/Dockerfile -t lcbp3-backend:latest .
# Build Frontend (NEXT_PUBLIC_API_URL bake เข้าไปตอน build) # Build Frontend (NEXT_PUBLIC_API_URL bake เข้าไปตอน build)
docker build -f frontend/Dockerfile \ docker build -f frontend/Dockerfile `
--build-arg NEXT_PUBLIC_API_URL=https://backend.np-dms.work/api \ --build-arg NEXT_PUBLIC_API_URL=https://backend.np-dms.work/api `
-t lcbp3-frontend:latest . -t lcbp3-frontend:latest .
# Export เป็น .tar เพื่อ Transfer # Export เป็น .tar เพื่อ Transfer
docker save lcbp3-backend:latest | gzip > lcbp3-backend.tar.gz docker save lcbp3-backend:latest -o lcbp3-backend.tar
docker save lcbp3-frontend:latest | gzip > lcbp3-frontend.tar.gz docker save lcbp3-frontend:latest -o lcbp3-frontend.tar
# Transfer ไปยัง QNAP (ผ่าน SCP หรือ Shared Folder) # Transfer ไปยัง QNAP (ผ่าน SMB Shared Folder)
scp lcbp3-*.tar.gz admin@192.168.10.8:/share/np-dms/app/ # Copy lcbp3-backend.tar และ lcbp3-frontend.tar ไปที่ \\192.168.10.8\np-dms\app\
``` ```
### Option B: Build บน QNAP โดยตรง (SSH) ### Option B: Build บน QNAP โดยตรง (SSH)
@@ -69,8 +69,8 @@ docker build -f frontend/Dockerfile \
ssh admin@192.168.10.8 ssh admin@192.168.10.8
# Load images # Load images
docker load < /share/np-dms/app/lcbp3-backend.tar.gz docker load < /share/np-dms/app/lcbp3-backend.tar
docker load < /share/np-dms/app/lcbp3-frontend.tar.gz docker load < /share/np-dms/app/lcbp3-frontend.tar
# ตรวจสอบ # ตรวจสอบ
docker images | grep lcbp3 docker images | grep lcbp3
@@ -82,15 +82,15 @@ docker images | grep lcbp3
```bash ```bash
# สร้าง directories สำหรับ volumes # สร้าง directories สำหรับ volumes
mkdir -p /share/dms-data/uploads/temp mkdir -p /share/np-dms/data/uploads/temp
mkdir -p /share/dms-data/uploads/permanent mkdir -p /share/np-dms/data/uploads/permanent
mkdir -p /share/dms-data/logs/backend mkdir -p /share/np-dms/data/logs/backend
mkdir -p /share/np-dms/app mkdir -p /share/np-dms/app
# กำหนดสิทธิ์ให้ non-root user ใน container (UID 1001) # กำหนดสิทธิ์ให้ non-root user ใน container (UID 1001)
chown -R 1001:1001 /share/dms-data/uploads chown -R 1001:1001 /share/np-dms/data/uploads
chown -R 1001:1001 /share/dms-data/logs/backend chown -R 1001:1001 /share/np-dms/data/logs/backend
chmod -R 750 /share/dms-data/uploads chmod -R 750 /share/np-dms/data/uploads
``` ```
--- ---
@@ -160,20 +160,22 @@ docker logs -f frontend
เมื่อต้องการ deploy version ใหม่: เมื่อต้องการ deploy version ใหม่:
```bash ```powershell
# 1. Build images ใหม่ (บน Dev Machine) # 1. Build images ใหม่ (บน Dev Machine - PowerShell)
docker build -f backend/Dockerfile -t lcbp3-backend:latest . docker build -f backend/Dockerfile -t lcbp3-backend:latest .
docker build -f frontend/Dockerfile -t lcbp3-frontend:latest . docker build -f frontend/Dockerfile `
--build-arg NEXT_PUBLIC_API_URL=https://backend.np-dms.work/api `
-t lcbp3-frontend:latest .
# 2. Export & Transfer # 2. Export & Transfer
docker save lcbp3-backend:latest | gzip > lcbp3-backend.tar.gz docker save lcbp3-backend:latest -o lcbp3-backend.tar
docker save lcbp3-frontend:latest | gzip > lcbp3-frontend.tar.gz docker save lcbp3-frontend:latest -o lcbp3-frontend.tar
scp lcbp3-*.tar.gz admin@192.168.10.8:/share/np-dms/app/ # Copy ไปที่ \\192.168.10.8\np-dms\app\ ผ่าน SMB Shared Folder
# 3. Load บน QNAP # 3. Load บน QNAP (SSH)
ssh admin@192.168.10.8 ssh admin@192.168.10.8
docker load < /share/np-dms/app/lcbp3-backend.tar.gz docker load < /share/np-dms/app/lcbp3-backend.tar
docker load < /share/np-dms/app/lcbp3-frontend.tar.gz docker load < /share/np-dms/app/lcbp3-frontend.tar
# 4. Restart ใน Container Station # 4. Restart ใน Container Station
# Applications → lcbp3-app → Restart # Applications → lcbp3-app → Restart

View File

@@ -48,8 +48,8 @@ services:
# --- Database --- # --- Database ---
DB_HOST: 'mariadb' DB_HOST: 'mariadb'
DB_PORT: '3306' DB_PORT: '3306'
DB_NAME: 'lcbp3' DB_DATABASE: 'lcbp3'
DB_USER: 'center' DB_USERNAME: 'center'
DB_PASSWORD: 'Center#2025' DB_PASSWORD: 'Center#2025'
# --- Redis --- # --- Redis ---
REDIS_HOST: 'cache' REDIS_HOST: 'cache'
@@ -60,7 +60,8 @@ services:
ELASTICSEARCH_PORT: '9200' ELASTICSEARCH_PORT: '9200'
# --- JWT --- # --- JWT ---
JWT_SECRET: 'eebc122aa65adde8c76c6a0847d9649b2b67a06db1504693e6c912e51499b76e' JWT_SECRET: 'eebc122aa65adde8c76c6a0847d9649b2b67a06db1504693e6c912e51499b76e'
JWT_EXPIRES_IN: '8h' JWT_EXPIRATION: '8h'
JWT_REFRESH_SECRET: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2'
# --- Numbering --- # --- Numbering ---
NUMBERING_LOCK_TIMEOUT: '5000' NUMBERING_LOCK_TIMEOUT: '5000'
NUMBERING_RESERVATION_TTL: '300' NUMBERING_RESERVATION_TTL: '300'
@@ -72,9 +73,9 @@ services:
- lcbp3 - lcbp3
volumes: volumes:
# Two-Phase Storage: จัดเก็บไฟล์นอก container # Two-Phase Storage: จัดเก็บไฟล์นอก container
- '/share/dms-data/uploads/temp:/app/uploads/temp' - '/share/np-dms/data/uploads/temp:/app/uploads/temp'
- '/share/dms-data/uploads/permanent:/app/uploads/permanent' - '/share/np-dms/data/uploads/permanent:/app/uploads/permanent'
- '/share/dms-data/logs/backend:/app/logs' - '/share/np-dms/data/logs/backend:/app/logs'
healthcheck: healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:3000/health'] test: ['CMD', 'curl', '-f', 'http://localhost:3000/health']
interval: 30s interval: 30s