430 lines
11 KiB
Markdown
430 lines
11 KiB
Markdown
# Infrastructure & Deployment
|
|
|
|
> **LEGACY (2026-08-02):** Dokumen plan ini ditulis saat infra masih Docker/Traefik. Produksi sekarang Caddy + Nix/systemd dengan port 4000-an. Gunakan hanya sebagai referensi historis.
|
|
|
|
## Docker Image Architecture
|
|
|
|
Project ini punya **satu Docker image** dengan multi-stage build. Backend Rust + Tesseract + ONNX model plus frontend Next.js.
|
|
|
|
### Dockerfile Structure
|
|
|
|
```dockerfile
|
|
# ============================================================
|
|
# Stage 1: Build Rust Backend
|
|
# ============================================================
|
|
FROM rust:1.85-slim-bookworm AS chef
|
|
RUN cargo install cargo-chef
|
|
WORKDIR /app
|
|
|
|
FROM chef AS planner
|
|
COPY backend/ .
|
|
RUN cargo chef prepare --recipe-path recipe.json
|
|
|
|
FROM chef AS builder
|
|
COPY --from=planner /app/recipe.json recipe.json
|
|
RUN cargo chef cook --release --recipe-path recipe.json
|
|
|
|
COPY backend/ .
|
|
RUN cargo build --release --bin gateway --bin workers
|
|
|
|
# ============================================================
|
|
# Stage 2: Build Next.js Frontend
|
|
# ============================================================
|
|
FROM oven/bun:1.3 AS frontend-builder
|
|
WORKDIR /app
|
|
COPY frontend/package.json frontend/bun.lock ./
|
|
RUN bun install --frozen-lockfile
|
|
COPY frontend/ .
|
|
RUN bun run build
|
|
|
|
# ============================================================
|
|
# Stage 3: Production Runtime
|
|
# ============================================================
|
|
FROM debian:bookworm-slim AS runtime
|
|
|
|
# Install runtime dependencies
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
tesseract-ocr \
|
|
tesseract-ocr-eng \
|
|
tesseract-ocr-ind \
|
|
ca-certificates \
|
|
fonts-dejavu-core \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
WORKDIR /app
|
|
|
|
# Copy Rust binaries
|
|
COPY --from=builder /app/target/release/gateway /app/gateway
|
|
COPY --from=builder /app/target/release/workers /app/workers
|
|
|
|
# Copy Next.js build
|
|
COPY --from=frontend-builder /app/.next /app/.next
|
|
COPY --from=frontend-builder /app/public /app/public
|
|
COPY --from=frontend-builder /app/package.json /app/package.json
|
|
COPY --from=frontend-builder /app/node_modules /app/node_modules
|
|
|
|
# Copy ONNX model (for background removal)
|
|
COPY models/ /app/models/
|
|
|
|
# Create temp storage directory
|
|
RUN mkdir -p /data/tools && chmod 1777 /data/tools
|
|
|
|
# Environment
|
|
ENV TESSDATA_PREFIX=/usr/share/tesseract-ocr/5/tessdata
|
|
ENV TOOLS_STORAGE_PATH=/data/tools
|
|
ENV TOOLS_GATEWAY_PORT=3001
|
|
ENV TOOLS_WORKER_CONCURRENCY=4
|
|
ENV RUST_LOG=info
|
|
|
|
# Expose port
|
|
EXPOSE 3001
|
|
|
|
# Run both gateway and workers via supervisor script
|
|
COPY scripts/entrypoint.sh /app/entrypoint.sh
|
|
RUN chmod +x /app/entrypoint.sh
|
|
|
|
CMD ["/app/entrypoint.sh"]
|
|
```
|
|
|
|
### Entrypoint Script
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
# Start Gateway (Axum HTTP server)
|
|
/app/gateway &
|
|
GATEWAY_PID=$!
|
|
|
|
# Start Worker(s)
|
|
/app/workers &
|
|
WORKER_PID=$!
|
|
|
|
# Handle graceful shutdown
|
|
trap "kill $GATEWAY_PID $WORKER_PID; exit 0" SIGINT SIGTERM
|
|
|
|
# Wait for either process to exit
|
|
wait -n $GATEWAY_PID $WORKER_PID
|
|
|
|
# If one exits, kill the other
|
|
kill $GATEWAY_PID $WORKER_PID 2>/dev/null
|
|
exit 1
|
|
```
|
|
|
|
### Image Size Estimates
|
|
|
|
| Component | Size |
|
|
|-----------|------|
|
|
| Rust binary (gateway) | ~8 MB |
|
|
| Rust binary (workers) | ~15 MB |
|
|
| Next.js build | ~10 MB |
|
|
| Tesseract + data | ~25 MB |
|
|
| ONNX model | ~50 MB |
|
|
| Base (Debian slim) | ~80 MB |
|
|
| **Total** | **~188 MB** |
|
|
|
|
> ONNX model opsional — bisa di-download runtime daripada di-include di image.
|
|
|
|
---
|
|
|
|
## Docker Compose
|
|
|
|
```yaml
|
|
# infra/compose/tools.yml
|
|
services:
|
|
tools:
|
|
container_name: tools
|
|
image: ghcr.io/asepharyana/asepharyana-hub/tools:sha-xxxxxxx
|
|
restart: always
|
|
networks:
|
|
app-shared-net:
|
|
aliases:
|
|
- tools
|
|
env_file:
|
|
- ../../.env
|
|
environment:
|
|
- REDIS_URL=redis://redis:6379
|
|
- NATS_URL=nats://nats:4222
|
|
- TOOLS_STORAGE_PATH=/data/tools
|
|
- TOOLS_GATEWAY_PORT=3001
|
|
- TOOLS_WORKER_CONCURRENCY=4
|
|
- RUST_LOG=info
|
|
volumes:
|
|
- tools_data:/data/tools
|
|
ports:
|
|
- "3001:3001"
|
|
depends_on:
|
|
redis:
|
|
condition: service_started
|
|
nats:
|
|
condition: service_started
|
|
|
|
volumes:
|
|
tools_data:
|
|
|
|
networks:
|
|
app-shared-net:
|
|
name: app-shared-net
|
|
external: true
|
|
```
|
|
|
|
### Environment Variables (`../../.env`)
|
|
|
|
```bash
|
|
# Tools
|
|
TOOLS_GATEWAY_PORT=3001
|
|
TOOLS_WORKER_CONCURRENCY=4
|
|
TOOLS_STORAGE_PATH=/data/tools
|
|
TOOLS_JOB_TTL_SECONDS=3600
|
|
TOOLS_RATE_LIMIT_PER_MINUTE=30
|
|
TOOLS_MAX_FILE_SIZE_MB=50
|
|
TOOLS_OCR_LANG=eng+ind
|
|
|
|
# Infra (reuse existing)
|
|
REDIS_URL=redis://redis:6379
|
|
NATS_URL=nats://nats:4222
|
|
```
|
|
|
|
---
|
|
|
|
## CI/CD Integration
|
|
|
|
### Docker Build Workflow
|
|
|
|
Tambah service `tools` di `.github/workflows/docker-build-push.yml`:
|
|
|
|
```yaml
|
|
# Di job "changes" step "Detect changed services"
|
|
changed() {
|
|
printf '%s\n' "$CHANGED_FILES" | grep -Eq "$1" && echo true || echo false
|
|
}
|
|
echo "tools=$(changed '^(apps/tools(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/tools\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
|
|
|
|
# Di job "build" step "Set matrix"
|
|
if [ "${{ steps.filter.outputs['tools'] == 'true' || steps.dispatch.outputs['tools'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then
|
|
add_service "tools" "docker-tools" "apps/tools"
|
|
fi
|
|
|
|
# Di job "build" step "Docker metadata"
|
|
case "$SVC_NAME" in
|
|
"tools") echo "dockerfile=infra/docker/tools.Dockerfile" >> $GITHUB_OUTPUT ;;
|
|
esac
|
|
|
|
# Di job "update-manifest"
|
|
SERVICES["tools"]="tools.yml"
|
|
PATHS["tools"]="apps/tools"
|
|
```
|
|
|
|
### Deploy Workflow
|
|
|
|
Tambah di `.github/workflows/deploy-docker.yml`:
|
|
```yaml
|
|
# Tidak perlu perubahan — deploy-docker.yml auto-detect compose file changes.
|
|
# Kalau compose/tools.yml berubah, service tools akan di-restart.
|
|
```
|
|
|
|
### Service Registration (update infra/traefik/dynamic/apps.yaml)
|
|
|
|
```yaml
|
|
tools:
|
|
rule: 'Host(`tools.asepharyana.my.id`) || Host(`tools.asepharyana.web.id`)'
|
|
entryPoints:
|
|
- websecure
|
|
tls: {}
|
|
middlewares:
|
|
- common-chain@file
|
|
service: tools-service
|
|
|
|
# ...di bagian services:
|
|
tools-service:
|
|
loadBalancer:
|
|
servers:
|
|
- url: 'http://tools:3001'
|
|
```
|
|
|
|
---
|
|
|
|
## Monitoring
|
|
|
|
### Prometheus Metrics
|
|
|
|
Tambahkan label Prometheus ke container tools:
|
|
|
|
```yaml
|
|
# Di compose tools.yml
|
|
labels:
|
|
- 'prometheus.io/scrape=true'
|
|
- 'prometheus.io/port=3001'
|
|
- 'prometheus.io/path=/metrics'
|
|
```
|
|
|
|
### Dashboard Integration
|
|
|
|
Tambah card di dashboard hub yang sudah ada:
|
|
|
|
```tsx
|
|
// Di dashboard hub — tambah section "Tools Usage"
|
|
// Data dari /api/dashboard → Prometheus query:
|
|
// rate(tools_jobs_total[24h]) — jobs per tool per hari
|
|
// sum(increase(tools_jobs_total[7d])) — total jobs minggu ini
|
|
// tools_jobs_in_flight — current processing
|
|
```
|
|
|
|
---
|
|
|
|
## Storage Architecture
|
|
|
|
### Temp Storage
|
|
|
|
```
|
|
/data/tools/
|
|
├── upload/ # Uploaded files
|
|
│ └── {job_id}.{ext}
|
|
├── processing/ # Intermediate files (stage-by-stage)
|
|
│ └── {job_id}/
|
|
│ ├── 00_original.png
|
|
│ ├── 01_grayscale.png
|
|
│ ├── 02_edges.png
|
|
│ ├── 03_warped.png
|
|
│ └── ...
|
|
└── output/ # Final output
|
|
└── {job_id}.pdf
|
|
```
|
|
|
|
### Cleanup Strategy
|
|
|
|
| Mekanisme | Timing |
|
|
|-----------|--------|
|
|
| NATS cron job | Setiap 10 menit |
|
|
| Scan files >1 jam | `find /data/tools -mmin +60 -delete` |
|
|
| Redis job keys >1 jam | `SCAN 0 MATCH job:*` → TTL check → DEL |
|
|
| Storage low warning | Alert via Notification Hub (future) |
|
|
|
|
---
|
|
|
|
## Resource Estimation (VPS orangevps)
|
|
|
|
### Current Usage
|
|
|
|
| Service | CPU | RAM | Disk |
|
|
|---------|-----|-----|------|
|
|
| Traefik | 0.1 | 50 MB | 10 MB |
|
|
| NATS | 0.05 | 30 MB | 10 MB |
|
|
| Redis | 0.05 | 10 MB | 5 MB |
|
|
| Dapr Placement | 0.02 | 20 MB | 5 MB |
|
|
| Scraper API | 0.1 | 30 MB | 50 MB |
|
|
| Hub | 0.05 | 120 MB | 200 MB |
|
|
| Jaeger | 0.1 | 200 MB | 500 MB |
|
|
| Prometheus | 0.1 | 150 MB | 1 GB |
|
|
| Node Exporter | 0.02 | 10 MB | 5 MB |
|
|
| OTel Collector | 0.05 | 50 MB | 10 MB |
|
|
| **Total Current** | **~0.64** | **~670 MB** | **~1.8 GB** |
|
|
|
|
### Tools Addition
|
|
|
|
| Resources | Estimate | Notes |
|
|
|-----------|----------|-------|
|
|
| CPU | +1.0 core (burst) | Pipeline processing berat di CPU. Scoring, warp, OCR semua CPU-bound. |
|
|
| RAM | +300 MB | Rust binary + image processing buffers + Tesseract + ONNX |
|
|
| Disk | +5 GB | Temp files, bisa lebih untuk batch processing. Butuh auto-cleanup ketat. |
|
|
| **Total After** | **~1.64 cores** | **~970 MB RAM** | **~6.8 GB disk** |
|
|
|
|
> **Catatan**: Kalau VPS cuma punya 1-2 cores, processing akan antri. NATS queue handle ini. Untuk production, pastikan CPU ada >2 cores.
|
|
|
|
### Scalability
|
|
|
|
```
|
|
VPS 1 core:
|
|
- Scanner: ~5-8 detik per page
|
|
- Concurrent: 1 job at a time
|
|
- Antrian: NATS queue buffer unlimited
|
|
|
|
VPS 4+ core:
|
|
- Scanner: ~2-3 detik per page
|
|
- Concurrent: 4 jobs parallel (1 per worker)
|
|
- Rayon: parallel per-page dalam batch
|
|
```
|
|
|
|
---
|
|
|
|
## Security Considerations
|
|
|
|
| Area | Mitigation |
|
|
|------|-----------|
|
|
| **Upload validation** | MIME type check (whitelist), magic bytes verification, max size 50MB |
|
|
| **Path traversal** | Job ID = UUID v4, no user-controlled filenames in storage |
|
|
| **Command injection** | No shell commands — semua processing via Rust crates, FFmpeg via crate binding |
|
|
| **Temporary files** | Auto-cleanup, random filenames, restricted permissions (0600) |
|
|
| **Rate limiting** | Redis sliding window: 30 requests/min/IP per tool, 429 response |
|
|
| **CORS** | Origin terbatas ke domain portfolio |
|
|
| **Resource exhaustion** | Max image dimension 8000px, max file count per batch 50, worker concurrency limit |
|
|
| **OCR data** | Tesseract data dari package manager, no user-trained models |
|
|
| **ONNX model** | Model dari source terpercaya, verify checksum |
|
|
|
|
---
|
|
|
|
## Rollback Strategy
|
|
|
|
1. **Image tag**: `tools:sha-<short>` immutable — tinggal update compose file ke tag sebelumnya
|
|
2. **Data**: Files auto-expire dalam 1 jam — no persistent data migration needed
|
|
3. **Traefik**: Cukup restart, TLS certs ga berubah
|
|
4. **Monitor**: Prometheus metrics akan langsung show error rate spike
|
|
|
|
---
|
|
|
|
## Development Setup (Local)
|
|
|
|
Untuk development tanpa Docker:
|
|
|
|
```bash
|
|
# Terminal 1: Redis + NATS
|
|
docker compose -f infra/compose/shared.yml -f infra/compose/nats.yml up -d
|
|
|
|
# Terminal 2: Rust workers
|
|
cd apps/tools/backend
|
|
REDIS_URL=redis://localhost:6379 NATS_URL=nats://localhost:4222 \
|
|
cargo run --bin workers
|
|
|
|
# Terminal 3: Rust gateway
|
|
REDIS_URL=redis://localhost:6379 NATS_URL=nats://localhost:4222 \
|
|
TOOLS_STORAGE_PATH=/tmp/tools \
|
|
cargo run --bin gateway
|
|
|
|
# Terminal 4: Next.js
|
|
cd apps/tools/frontend
|
|
bun dev --port 3002
|
|
```
|
|
|
|
### Test Pipeline Locally (tanpa NATS/Redis)
|
|
|
|
Untuk development pipeline image processing doang:
|
|
|
|
```rust
|
|
// Di workers/src/scanner/pipeline.rs — test function
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_full_pipeline() {
|
|
let pipeline = ScanPipeline::default();
|
|
let result = pipeline.process_sync(
|
|
"test_images/scan_miring.jpg",
|
|
ScanOptions { ocr: false, enhance: true }
|
|
);
|
|
assert!(result.is_ok());
|
|
assert!(result.unwrap().output_path.exists());
|
|
}
|
|
|
|
#[test]
|
|
fn test_edge_detection_variations() {
|
|
// Test dengan berbagai kondisi: kertas putih, background ramai, sudut ekstrim
|
|
for case in &["normal.jpg", "dark.jpg", "angle45.jpg", "shadow.jpg"] {
|
|
let img = image::open(format!("test_images/{}", case)).unwrap();
|
|
let corners = detect_corners_with_fallback(&img.grayscale().into_luma8());
|
|
assert!(corners.is_ok(), "Failed on: {}", case);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Test images kumpulin dari foto dokumen real di berbagai kondisi — ini penting buat tuning parameter.
|