Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 27 additions & 30 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,69 +1,66 @@
# Multi-stage Dockerfile for QCX - Optimized for Google Cloud Build
# Stage 1: Dependencies installation
FROM oven/bun:1.1.3-alpine AS deps
# Multi-stage Dockerfile for QCX - Optimized for Google Cloud Build and Production
# Uses pinned Bun v1.3.5-alpine for reproducible builds (January 2026)
Comment on lines +1 to +2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clarify version pinning strategy.

The PR summary states the goal is to use oven/bun:alpine (latest stable), but the implementation pins 1.3.5-alpine. Pinning provides reproducibility, but conflicts with the stated objective of using "latest stable."

Please clarify:

  • If pinning is intentional, the PR description should be updated to reflect this decision.
  • If the goal is truly to track latest, consider using oven/bun:alpine without a version pin (though pinning is generally recommended for production).
🤖 Prompt for AI Agents
In @Dockerfile around lines 1 - 2, Decide and make the version strategy
consistent: either update the PR description to state that the Dockerfile
intentionally pins Bun to "1.3.5-alpine" for reproducible builds, or change the
FROM line to use the floating tag "oven/bun:alpine" if you want to track the
latest stable; mention the exact tag text ("1.3.5-alpine" or "oven/bun:alpine")
in your update so reviewers know which approach you chose.


# Stage 1: Install dependencies
FROM oven/bun:1.3.5-alpine AS deps

WORKDIR /app

# Copy package files
COPY package.json bun.lock* ./

# Install dependencies
# Install dependencies (frozen lockfile for reproducibility)
RUN bun install --frozen-lockfile

# Stage 2: Build stage
FROM oven/bun:1.1.3-alpine AS builder
# Stage 2: Build the Next.js application
FROM oven/bun:1.3.5-alpine AS builder

WORKDIR /app

# Copy dependencies from deps stage
# Copy node_modules from deps stage
COPY --from=deps /app/node_modules ./node_modules

# Copy application source
# Copy source code
COPY . .

# Set build-time environment variables
# Disable telemetry during build
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production

# Build the Next.js application
# Build the application (standalone output)
RUN bun run build

# Stage 3: Production runtime
FROM oven/bun:1.1.3-alpine AS runner
# Stage 3: Production runtime (minimal image running with Bun)
FROM oven/bun:1.3.5-alpine AS runner

WORKDIR /app

# Install only necessary runtime dependencies
RUN apk add --no-cache \
nodejs \
tini \
curl \
&& addgroup -g 1001 -S nodejs \
&& adduser -S nextjs -u 1001
# Create non-root group and user, with nextjs belonging to nodejs group
RUN addgroup -g 1001 -S nodejs \
&& adduser -S -u 1001 -G nodejs nextjs

# Set production environment
# Environment variables
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

# Copy built application from builder
# Copy standalone build output (optimized for minimal size)
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
# Copy static assets
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Copy public folder
COPY --from=builder --chown=nextjs:nodejs /app/public ./public

# Switch to non-root user for security
# Switch to non-root user
USER nextjs

# Expose the application port
# Expose port
EXPOSE 3000

# Health check for container orchestration
# Health check (uses Bun to fetch; adjust /api/health if your endpoint differs)
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:3000/api/health || exit 1

# Use tini as init system to handle signals properly
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["bun", "--eval", "fetch('http://localhost:3000/api/health').then(r => r.ok ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))"]

# Start the application
CMD ["node", "server.js"]
# Run the standalone server with Bun (fully compatible in Bun 1.3+)
CMD ["bun", "server.js"]
Comment on lines +33 to +66

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The runtime stage removed tini and the ENTRYPOINT that handled signal forwarding/reaping. Without an init process, PID 1 signal handling and zombie reaping can be problematic in Kubernetes/Cloud Run, leading to slow shutdowns or leaked processes. Bun/Node apps can still benefit from an init wrapper.

Suggestion

Reintroduce an init process. Simplest is Docker's built-in init at runtime (docker run --init), but for portability bake it in:

RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["bun", "server.js"]

Alternatively, if you deliberately don’t want tini, document why and ensure the app handles SIGTERM/SIGINT correctly.

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

Comment on lines +1 to +66

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Operational: Test Bun compatibility with Next.js standalone.

This migration from Node.js to Bun as the runtime for Next.js standalone is significant. While Bun 1.3+ advertises Node.js compatibility, thoroughly test:

  1. All Next.js features used in your app (API routes, middleware, ISR, etc.)
  2. Any native Node.js modules or dependencies that might have compatibility issues
  3. Performance characteristics (memory usage, request throughput, cold start times)
  4. Error handling and stack traces for debugging

Consider running parallel deployments initially to compare Node.js vs. Bun behavior before fully switching.

#!/bin/bash
# Description: Check for potential Node.js-specific dependencies that might have compatibility issues

# Look for dependencies that use native Node modules or might have Bun compatibility concerns
echo "Checking package.json for potentially problematic dependencies..."

# Check for native modules
jq -r '.dependencies // {} | to_entries[] | select(.key | test("^(node-|@node/)")) | .key' package.json 2>/dev/null || echo "No package.json found"

# Check for C++ addons or native dependencies
jq -r '.dependencies // {} | to_entries[] | select(.key | test("(bindings|nan|node-gyp|ffi)")) | .key' package.json 2>/dev/null

echo ""
echo "Review these dependencies for Bun compatibility at: https://bun.sh/docs/runtime/nodejs-apis"

Comment on lines +65 to +66

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find and inspect the Dockerfile to understand the full context
git ls-files | grep -i dockerfile

Repository: QueueLab/QCX

Length of output: 66


🏁 Script executed:

# Check the full Dockerfile to understand setup around lines 65-66
fd -t f -e Dockerfile -e dockerfile | head -20

Repository: QueueLab/QCX

Length of output: 38


🌐 Web query:

Bun signal handling SIGTERM SIGINT PID 1 docker

💡 Result:

Short answer: When Bun is PID 1 inside a container it will not be terminated by SIGINT/SIGTERM unless you explicitly handle those signals in your Bun app or run an init that forwards signals (tini/dumb-init / Docker --init). See Bun docs for registering handlers and use --init or an init binary if you don’t want to add handlers in-process. [1][2][3]

What to do (options):

  • Add explicit handlers in Bun (recommended if you want graceful shutdown):

    process.on("SIGTERM", async () => {
    // cleanup, close servers/connections
    process.exit(0);
    });
    process.on("SIGINT", () => process.exit(0));

    (Bun docs demonstrate listening for SIGINT—use the same for SIGTERM). [1]

  • Run an init as PID 1 so Bun becomes a child and receives forwarded signals:

    • docker run --init ... (uses tini)
    • Or install/use tini/dumb-init as your ENTRYPOINT to forward/reap signals. [2][3]
  • If you use a shell entrypoint, use exec to replace the shell with Bun so Bun runs as the main process (but you still must register handlers if it is PID 1). [3]

References:

  • Bun docs — listening for SIGINT / process.on. [1]
  • tini (init for containers) — usage and signal forwarding. [2]
  • Notes on PID 1 behaviour and options (exec, init, dumb-init). [3]

🌐 Web query:

Bun 1.3+ process signals graceful shutdown documentation

💡 Result:

Short summary — Bun 1.3+ supports process signals the same way Node does (process.on('SIGINT' / 'SIGTERM')), and Bun.Server exposes server.stop() which by default performs a graceful shutdown (waits for in‑flight requests & websockets). Typical pattern: listen for SIGINT/SIGTERM, perform cleanup (close DB, etc.), await server.stop(), then exit. [1][2][3]

Minimal example:

const server = Bun.serve({ fetch(req) { return new Response("ok"); } });

process.on("SIGTERM", async () => {
// cleanup (e.g. await db.close())
await server.stop(); // graceful: waits for in‑flight requests
process.exit(0);
});
process.on("SIGINT", () => process.kill(process.pid, "SIGTERM"));

Key docs:

  • Listening for Ctrl+C / SIGINT in Bun. [1]
  • Server.stop() (graceful by default; pass true to force-close). [2]
  • Bun.serve / Server API (server.stop, server.unref/ref, reload, etc.). [3]

Sources:
[1] Bun guide — Listen for CTRL+C.
[2] Bun API — Server.stop.
[3] Bun API — Bun.serve / Server lifecycle.


🏁 Script executed:

cat -n Dockerfile

Repository: QueueLab/QCX

Length of output: 2418


🌐 Web query:

Node.js standalone server Next.js signal handling SIGTERM graceful shutdown

💡 Result:

Short answer

  • For Next.js standalone (node .next/standalone/server.js) you should run a wrapper Node process that listens for SIGTERM/SIGINT, stops accepting new requests, waits for in-flight work to finish, closes DB/queues, then exits. Next.js also exposes a manual signal hook (NEXT_MANUAL_SIG_HANDLE) for adding shutdown handlers inside the Next process. [1][2]

What to do (practical options)

  1. Recommended: run your own wrapper that starts the standalone server and implements graceful shutdown. Example pattern:

    • spawn the standalone server or require your server file
    • on SIGTERM/SIGINT: stop accepting new requests, await pending requests/tasks, close DB pools/connections, then exit(0)
    • set a hard timeout and exit(1) if shutdown times out

    Minimal sketch (wrapper approach):

    • If you start server via node .next/standalone/server.js you can instead create a small server.js that requires/starts Next’s server and adds:
      process.on('SIGTERM', async () => { await shutdown(); process.exit(0); });

    This is the robust approach for containers / Kubernetes so you can control stop-timeout and resource cleanup. (General Node graceful‑shutdown guidance). [4][6]

  2. Next.js built-in manual handling (Pages router / some runtime versions):

    • Set NEXT_MANUAL_SIG_HANDLE=true in the environment used to run Next (e.g. in your start script). Then register process.on('SIGTERM') / process.on('SIGINT') handlers inside your app (example historically placed in pages/_document.js or root instrumentation for App Router). This lets Next’s runtime defer exit so your handlers run. Note: this is not available in next dev and behavior depends on Next version/runtime. [2][5][10]

Concrete references

  • Next.js self-hosting / manual shutdown guidance (mentions sending SIGINT/SIGTERM and NEXT_MANUAL_SIG_HANDLE). [2]
  • Next.js output: 'standalone' details (creates .next/standalone and minimal server.js to run with node). Use wrapper or copy public/static if needed. [1]
  • Node.js/Community guides on graceful shutdown patterns and using SIGTERM with stop-timeout in Docker/K8s. Use to implement your shutdown logic (stop accepting connections, close DBs, set hard timeout). [4][6]

If you want, I can:

  • produce a complete, ready-to-run wrapper server.js that starts .next/standalone/server.js and implements graceful shutdown (including waiting for pending requests and closing a sample DB connection), or
  • tailor advice for Docker/Kubernetes (liveness/readiness probes and stopTimeout settings). Which would you like?

Add signal handlers for graceful shutdown—Bun as PID 1 requires explicit handling.

Next.js standalone servers (like server.js) do not have built-in signal handlers for SIGTERM/SIGINT. Without them, docker stop will forcefully kill the container after 10 seconds instead of gracefully shutting down in-flight requests.

Choose one approach:

  1. Add signal handlers to server.js (recommended if you control the startup code):

    process.on("SIGTERM", async () => {
      // Close DB connections, etc.
      await server.close();
      process.exit(0);
    });
    
  2. Add tini as init system (simpler if you can't modify server.js):

    RUN apk add --no-cache tini
    ENTRYPOINT ["/sbin/tini", "--"]
    CMD ["bun", "server.js"]

Without one of these, the container will not shut down gracefully and may lose data on redeploy or restart.

🤖 Prompt for AI Agents
In @Dockerfile around lines 65 - 66, The container runs Bun as PID 1 (CMD
["bun","server.js"]) but lacks graceful shutdown handling; either add
SIGTERM/SIGINT handlers inside server.js to close resources and call
server.close() then process.exit(0) (reference server.js, server.close,
process.on("SIGTERM")/process.on("SIGINT")), or modify the Dockerfile to install
and use tini as init (add RUN to install tini and set ENTRYPOINT
["/sbin/tini","--"] before CMD ["bun","server.js"]) so signals are forwarded and
shutdowns are graceful.