fix: Docker hardening, security, and deployment readiness for V1 (#82)

Phase 1 — Docker Artifact Optimization:
- Replace broad `COPY . .` with targeted frontend source copies (API/Python
  changes no longer bust the frontend build cache)
- Replace build-essential with gcc/g++ (leaner runtime)
- Fix LOG_LEVEL=debug → info for production
- Harden .dockerignore (exclude worktrees, IDE, CI, test artifacts)

Phase 2 — State & Persistence:
- Add PUID/PGID support in entrypoint.sh for bind mount compatibility
- Guard against PUID=0/PGID=0 to prevent accidental root execution
- Evict conflicting system users (e.g. node:1000) before UID remap

Phase 3 — Security:
- Always register @fastify/rate-limit so login brute-force protection
  works even when global rate limit is disabled (RATE_LIMIT_PER_MIN=0)
- Add trustProxy support (TRUST_PROXY env var, default true) so rate
  limiting and audit logs use real client IPs behind reverse proxies
- Strip stack traces from 500 error responses in production
- Fix FSTDEP022 deprecation: maxParamLength → routerOptions
- Add multi-file guard on single-file tool endpoint with clear error
  message pointing to the /batch endpoint

Phase 4 — Graceful Degradation:
- Add consolidated hardware detection startup banner (GPU, rate limit,
  upload limit, proxy status)
- Add ConnectionMonitor component with health polling and reconnecting
  overlay that auto-dismisses when the server comes back

Phase 5 — Deployment Docs:
- Rewrite deployment.md with copy-paste CPU and GPU compose templates
- Add hardware requirements table (minimum, recommended, heavy workloads)
- Add PUID/PGID bind mount documentation
- Add complete env var reference table
- Add reverse proxy guides for Nginx, Nginx Proxy Manager, Traefik,
  and Cloudflare Tunnels
This commit is contained in:
Ashim
2026-04-21 10:19:08 +08:00
committed by GitHub
parent fa35f57813
commit 4c9dc6e38e
11 changed files with 427 additions and 184 deletions
+25 -10
View File
@@ -42,7 +42,8 @@ recoverInterruptedInstalls();
const app = Fastify({
logger: { level: env.LOG_LEVEL },
bodyLimit: env.MAX_UPLOAD_SIZE_MB > 0 ? env.MAX_UPLOAD_SIZE_MB * 1024 * 1024 : 1073741824,
maxParamLength: 500,
trustProxy: env.TRUST_PROXY,
routerOptions: { maxParamLength: 500 },
});
app.setErrorHandler((error: Error & { statusCode?: number }, request, reply) => {
@@ -51,9 +52,11 @@ app.setErrorHandler((error: Error & { statusCode?: number }, request, reply) =>
{ err: error, url: request.url, method: request.method },
"Unhandled request error",
);
const isProduction = process.env.NODE_ENV === "production";
reply.status(statusCode).send({
error: statusCode >= 500 ? "Internal server error" : error.message,
details: error.stack ?? error.message,
...(statusCode < 500 && { details: error.message }),
...(!isProduction && statusCode >= 500 && { details: error.stack ?? error.message }),
});
});
@@ -80,13 +83,14 @@ app.addHook("onSend", async (_request, reply) => {
}
});
if (env.RATE_LIMIT_PER_MIN > 0) {
await app.register(rateLimit, {
max: env.RATE_LIMIT_PER_MIN,
timeWindow: "1 minute",
allowList: (request) => !request.url.startsWith("/api/"),
});
}
// Always register rate-limit plugin so per-route limits (login brute-force protection) work.
// When RATE_LIMIT_PER_MIN=0, the global limit is set high enough to be effectively unlimited
// while still enabling per-route overrides like the login endpoint.
await app.register(rateLimit, {
max: env.RATE_LIMIT_PER_MIN > 0 ? env.RATE_LIMIT_PER_MIN : 50000,
timeWindow: "1 minute",
allowList: (request) => !request.url.startsWith("/api/"),
});
// Multipart upload support
await registerUpload(app);
@@ -190,7 +194,18 @@ const cleanupCron = startCleanupCron();
// Start
try {
await app.listen({ port: env.PORT, host: "0.0.0.0" });
console.log(`ashim API running on port ${env.PORT}`);
const gpu = isGpuAvailable();
console.log(
[
`ashim v${APP_VERSION} running on port ${env.PORT}`,
gpu
? "[INFO] GPU detected — AI tools will use CUDA acceleration"
: "[WARN] No GPU detected — AI tools will use CPU (slower)",
`[INFO] Rate limit: ${env.RATE_LIMIT_PER_MIN > 0 ? `${env.RATE_LIMIT_PER_MIN}/min` : "disabled"}`,
`[INFO] Upload limit: ${env.MAX_UPLOAD_SIZE_MB > 0 ? `${env.MAX_UPLOAD_SIZE_MB} MB` : "unlimited"}`,
`[INFO] Trust proxy: ${env.TRUST_PROXY}`,
].join("\n"),
);
} catch (err) {
app.log.error(err);
process.exit(1);