Skip to content

feat: update Dockerfile and scripts for frontend, add Turbo integration and environment configuration#22

Merged
chakrihacker merged 10 commits into
mainfrom
feat/enhance-docker-setup
Sep 29, 2025
Merged

feat: update Dockerfile and scripts for frontend, add Turbo integration and environment configuration#22
chakrihacker merged 10 commits into
mainfrom
feat/enhance-docker-setup

Conversation

@chakrihacker
Copy link
Copy Markdown
Contributor

@chakrihacker chakrihacker commented Sep 28, 2025

Summary by Sourcery

Integrate Turborepo and Bun workflows across the monorepo, modernize package scripts, introduce a Bun-based production server for the frontend, and add a multi-stage Dockerfile for building and serving the frontend application.

New Features:

  • Integrate Turbo tasks with root scripts and add turbo.json config to unify dev and build workflows
  • Introduce a Bun-based server (server.ts) in the frontend to serve Expo web builds
  • Add a multi-stage Dockerfile for building and deploying the frontend with Bun and Turbo

Enhancements:

  • Consolidate development and build commands across root, API, frontend, and DB packages to use turbo run or Bun scripts
  • Update frontend package.json with export:web and export:server pipelines and include type checking in the API package
  • Specify Bun version in packageManager field for consistent environment

Build:

  • Add Turbo as a global Bun dependency in Docker build stages
  • Add a Prisma generate build script to the DB package

@korbit-ai
Copy link
Copy Markdown

korbit-ai Bot commented Sep 28, 2025

You've used up your 5 PR reviews for this month under the Korbit Starter Plan. You'll get 5 more reviews on October 6th, 2025 or you can upgrade to Pro for unlimited PR reviews and enhanced features in your Korbit Console.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Sep 28, 2025

Caution

Review failed

The pull request is closed.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Containerized frontend with a production web server (port 4000).
    • Added Docker Compose setups for production and staging (web, API, workers, Postgres, Redis).
  • Configuration

    • Frontend now targets the staging API URL.
    • Removed the public storage prefix setting.
    • Expanded CORS to allow coolify.fyndx.io subdomains.
  • Build/Refactor

    • Multi-stage Docker builds for API and workers.
    • Centralized monorepo builds via Turbo; added type-check script.
    • New build/export scripts for the frontend.
  • Chores

    • Broader .dockerignore coverage; minor .gitignore/editor setting tweaks.
    • Removed legacy dev Compose setup.

Walkthrough

Adds Turbo orchestration and type-check scripts, reintroduces Bun+Expo frontend server and multi-stage Dockerfiles, updates frontend env/schema (removes EXPO_PUBLIC_STORAGE_PREFIX, updates API URL), refactors API/workers Dockerfiles, and adds/updates docker-compose and ignore files.

Changes

Cohort / File(s) Summary
Workspace & tooling
package.json, turbo.json, ./.gitignore, .vscode/settings.json
Centralize dev/build scripts under Turbo (turbo.json), add packageManager and check-types wiring; minor .gitignore reorder (*.pem); add Dockerfile formatter override in VS Code settings.
Frontend build & server
apps/frontend/Dockerfile, apps/frontend/package.json, apps/frontend/server.ts, apps/frontend/.env, apps/frontend/.env.example, apps/frontend/.dockerignore, apps/frontend/src/config/env.ts
Reintroduce multi-stage Bun-based frontend Dockerfile and Bun/Expo server (server.ts); add build/export:web/export:server scripts and @expo/server dependency; update EXPO_PUBLIC_API_URL; remove EXPO_PUBLIC_STORAGE_PREFIX from env and schema; add .dockerignore.
API
apps/api/package.json, apps/api/.dockerignore, apps/api/Dockerfile, apps/api/src/cors.ts
Add check-types script, expand .dockerignore, refactor API Dockerfile into multi-stage prune/install/build and distroless runtime; update CORS allowed pattern to coolify.fyndx.io.
Workers
apps/workers/Dockerfile, apps/workers/package.json
Convert worker Dockerfile to multi-stage turbo-based build and distroless runtime; adjust Bun build outfile name.
Database package
packages/db/package.json
Add build script to run bun run db:generate.
Docker Compose & env
docker/docker-compose.prod.yml, docker-compose.yml, docker-compose.staging.yml, docker/docker-compose.dev.yml
Add web service (builds apps/frontend/Dockerfile, exposes 4000); expand api/workers env declarations; add staging compose with postgres and redis; remove old dev compose.
Repo Docker helpers
docker/Dockerfile.base
Remove shared base Dockerfile previously providing common Bun base and dependency caching steps.
Per-app dockerignore
apps/api/.dockerignore, apps/frontend/.dockerignore
Add/expand comprehensive ignore patterns to reduce build context and surface.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant BunServer as Bun HTTP Server (apps/frontend/server.ts)
  participant Static as ./dist/client
  participant Expo as Expo Request Handler (server build)

  Client->>BunServer: HTTP request
  alt static asset (/_expo/static/* or /assets/* or /canvaskit.wasm)
    BunServer->>Static: check & read file
    alt exists
      Static-->>BunServer: file (200)
      BunServer-->>Client: static response
    else
      BunServer-->>Client: 404
    end
  else dynamic route
    BunServer->>Expo: delegate to createRequestHandler({ build: SERVER_BUILD_DIR })
    Expo-->>BunServer: rendered response
    BunServer-->>Client: served response
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

"I hop through scripts and turbo trails,
A Bun-born breeze fills Expo’s sails.
Distroless clouds and staging light,
Type-checks nibble through the night.
With tidy ignores, we ship just right. 🐇"

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly references the key changes of updating the frontend Dockerfile and scripts, adding Turbo integration, and configuring environment settings, which align with the main modifications in the pull request around monorepo build tooling and frontend deployment.
Description Check ✅ Passed The description succinctly summarizes the pull request’s objectives, including Turbo integration, Bun-based server introduction, multi-stage Dockerfile for the frontend, and script modernizations, all of which directly reflect the changes in the diff.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 770b3f1 and cdaac61.

📒 Files selected for processing (1)
  • apps/frontend/server.ts (1 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Sep 28, 2025

Reviewer's Guide

This PR integrates Turborepo as the monorepo build orchestrator, standardizes dev/build scripts across projects, introduces a Bun-powered server for the frontend, and adds a multi-stage Dockerfile for containerizing the frontend app.

File-Level Changes

Change Details Files
Integrate Turborepo and update root configuration
  • Replaced Bun-based dev and web scripts with turbo run dev/build
  • Added turbo as a devDependency and specified bun@1.2.22 as the packageManager
  • Introduced a turbo.json configuration file for workspace settings
package.json
turbo.json
Standardize and enhance package scripts in sub-projects
  • apps/api: added a check-types script for type checking
  • apps/frontend: introduced build, export:web, and export:server scripts and consolidated start/dev commands
  • packages/db: added a build alias for Prisma generate
apps/api/package.json
apps/frontend/package.json
packages/db/package.json
Introduce Bun-based server entrypoint for frontend
  • Created server.ts to serve Expo routes via createRequestHandler
  • Implemented custom routes for static assets and canvaskit.wasm using Bun.serve
apps/frontend/server.ts
Add multi-stage Dockerfile for frontend deployment
  • Set up builder and installer stages with Bun and Turbo prune
  • Ran turbo run build to produce the frontend artifacts
  • Configured a distroless web stage to run the Bun-based server on port 4000
apps/frontend/Dockerfile

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@qodo-code-review
Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 Security concerns

Static file serving:
Consider adding safe path normalization to prevent potential path traversal via crafted URLs before concatenating with ./dist/client, even if Bun.file likely resolves relative paths safely. Additionally, serving static files without setting Content-Type headers could enable content sniffing; explicitly set appropriate headers to reduce risks.

⚡ Recommended focus areas for review

Possible Issue

Static file path resolution uses new URL(request.url).pathname concatenated to ./dist/client without normalizing leading slashes; this can produce double slashes and may not work as intended in Bun.file, and lacks content-type headers for static assets.

"/_expo/static/*": async (request) => {
  const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`);
  const exists = await file.exists();
  return exists
    ? new Response(file)
    : new Response("Not found", { status: 404 });
},
"/assets/*": async (request) => {
  const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`);
  const exists = await file.exists();
  return exists
    ? new Response(file)
    : new Response("Not found", { status: 404 });
},
Inconsistent Route Matching

The generic "/*" handler precedes a specific "/canvaskit.wasm" route; depending on Bun.serve route precedence, the wildcard may capture the wasm request, bypassing the intended static serve path.

"/*": async (request) => await handler(request),
"/canvaskit.wasm": async (_request) => {
  const file = Bun.file("./dist/client/canvaskit.wasm");
  const exists = await file.exists();
  return exists
    ? new Response(file)
    : new Response("Not found here", { status: 404 });
},
Build Reproducibility

Global turbo install and missing explicit turbo.json verification may be fine, but COPY . . before prune can bloat context; also distroless base lacks CA certs/timezone/locales by default which might affect outbound HTTPS calls from the compiled server.

FROM oven/bun:debian AS base 

# Setup Phase 
FROM base as builder
RUN apt-get update && \ 
    apt-get install -y watchman && \ 
    apt-get clean && \ 
    rm -rf /var/lib/apt/lists/* 
# Setup: Install turbo globally 
RUN bun install turbo --global 
WORKDIR /app 
# Install Phase 
COPY . . 
RUN turbo prune @universal/app --docker 
FROM base AS installer 
RUN bun install turbo --global 
WORKDIR /app 
# Install dependencies for the full monorepo 
COPY --from=builder /app/out/json . 
RUN bun install 
# Build Phase 
COPY --from=builder /app/out/full . 
RUN turbo run build 
# Production image 
# For Web 
FROM gcr.io/distroless/base AS web 
WORKDIR /app 
ENV NODE_ENV=production 
COPY --from=installer /app/apps/frontend/web-server /app/server 
COPY --from=installer /app/apps/frontend/dist /app/dist 
CMD [ "./server" ] 
EXPOSE 4000

@qodo-code-review
Copy link
Copy Markdown

CI Feedback 🧐

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: build-api

Failed stage: Build and push API image [❌]

Failure summary:

The Docker buildx step failed due to an invalid image tag:
- It attempted to tag the image as
ghcr.io/fyndx/universal-starter-api:-7d590ba.
- Docker image references cannot have an empty tag
component before a dash; the leading - after the colon indicates a missing tag name prefix, making
the reference format invalid.
- Error reported: invalid reference format.

Relevant error logs:
1:  ##[group]Runner Image Provisioner
2:  Hosted Compute Agent
...

427:  "Direct push": true,
428:  "Docker exporter": true,
429:  "Multi-platform build": true,
430:  "OCI exporter": true
431:  },
432:  "labels": {
433:  "org.mobyproject.buildkit.worker.moby.host-gateway-ip": "172.17.0.1"
434:  }
435:  }
436:  ],
437:  "name": "default",
438:  "driver": "docker"
439:  }
440:  ##[endgroup]
441:  [command]/usr/bin/docker buildx build --file ./apps/api/Dockerfile --iidfile /home/runner/work/_temp/docker-actions-toolkit-ustVYg/build-iidfile-b761c30cc5.txt --label org.opencontainers.image.created=--label org.opencontainers.image.description= --label org.opencontainers.image.licenses= --label org.opencontainers.image.revision=7d590ba0e834ada3845bedc4d50130a52f3c03fc --label org.opencontainers.image.source=https://github.com/fyndx/universal-starter --label org.opencontainers.image.title=universal-starter --label org.opencontainers.image.url=https://github.com/fyndx/universal-starter --label org.opencontainers.image.version=pr-22 --tag ghcr.io/fyndx/universal-starter-api:pr-22 --tag ghcr.io/fyndx/universal-starter-api:-7d590ba --metadata-file /home/runner/work/_temp/docker-actions-toolkit-ustVYg/build-metadata-2f6758095d.json --push .
442:  ERROR: failed to build: invalid tag "ghcr.io/fyndx/universal-starter-api:-7d590ba": invalid reference format
443:  ##[error]buildx failed with: ERROR: failed to build: invalid tag "ghcr.io/fyndx/universal-starter-api:-7d590ba": invalid reference format
444:  Post job cleanup.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • turbo.json is currently empty—add appropriate pipeline configurations (inputs, outputs, and dependencies) to enable caching and task orchestration.
  • The Dockerfile installs turbo globally even though it’s already a devDependency—consider using the local turbo binary (node_modules/.bin/turbo) to ensure version consistency and avoid global installs.
  • Add a .dockerignore to the frontend Docker build context to exclude unnecessary files and speed up image builds.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- turbo.json is currently empty—add appropriate pipeline configurations (inputs, outputs, and dependencies) to enable caching and task orchestration.
- The Dockerfile installs turbo globally even though it’s already a devDependency—consider using the local turbo binary (node_modules/.bin/turbo) to ensure version consistency and avoid global installs.
- Add a .dockerignore to the frontend Docker build context to exclude unnecessary files and speed up image builds.

## Individual Comments

### Comment 1
<location> `apps/frontend/server.ts:10` </location>
<code_context>
+const server = Bun.serve({
+  port: 4000,
+  routes: {
+    "/_expo/static/*": async (request) => {
+      const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`);
+      const exists = await file.exists();
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the file-serving logic into a reusable helper to eliminate code duplication across route handlers.

Here’s a quick refactor that pulls the “check-exists + return file or 404” logic into a single helper. You still end up with three route entries, but zero duplication:

```ts
import path from "node:path";
import { createRequestHandler } from "@expo/server";
import Bun from "bun";

const handler = createRequestHandler(path.join(import.meta.dir, "dist/server"));

function serveFrom(distPath: string, notFoundMsg = "Not found") {
  return async (req: Request) => {
    const urlPath = new URL(req.url).pathname;
    const file = Bun.file(path.join(import.meta.dir, distPath, urlPath));
    if (!(await file.exists())) {
      return new Response(notFoundMsg, { status: 404 });
    }
    return new Response(file);
  };
}

const server = Bun.serve({
  port: 4000,
  routes: {
    "/_expo/static/*": serveFrom("dist/client"),
    "/assets/*":         serveFrom("dist/client"),
    "/canvaskit.wasm":   serveFrom("dist/client/canvaskit.wasm", "Not found here"),
    "/*":                (req) => handler(req),
  },
});

// biome-ignore lint/suspicious/noConsole: logging the server URL
console.log(`Server listening on ${server.url}`);
```

Steps:

1. Create a single `serveFrom` helper that takes a base path & optional 404 message.  
2. Replace each inline handler with a call to `serveFrom(...)`.  
3. Keep your existing route patterns – behavior is unchanged, but duplication is gone.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread apps/frontend/server.ts
@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Sep 28, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Security
Prevent path traversal security vulnerability

Refactor the static file handlers to prevent path traversal vulnerabilities by
normalizing the file path and ensuring it stays within the intended directory.

apps/frontend/server.ts [10-23]

-"/_expo/static/*": async (request) => {
-  const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`);
+const staticAssetHandler = async (request: Request) => {
+  const staticDir = path.resolve(import.meta.dir, "dist/client");
+  const pathname = new URL(request.url).pathname;
+  const filePath = path.join(staticDir, pathname);
+
+  // Prevent path traversal attacks
+  if (!filePath.startsWith(staticDir)) {
+    return new Response("Forbidden", { status: 403 });
+  }
+
+  const file = Bun.file(filePath);
   const exists = await file.exists();
   return exists
     ? new Response(file)
     : new Response("Not found", { status: 404 });
-},
-"/assets/*": async (request) => {
-  const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`);
-  const exists = await file.exists();
-  return exists
-    ? new Response(file)
-    : new Response("Not found", { status: 404 });
-},
+};
 
+const server = Bun.serve({
+  port: 4000,
+  routes: {
+    "/_expo/static/*": staticAssetHandler,
+    "/assets/*": staticAssetHandler,
+    "/*": async (request) => await handler(request),
+    "/canvaskit.wasm": async (_request) => {
+      const file = Bun.file("./dist/client/canvaskit.wasm");
+      const exists = await file.exists();
+      return exists
+        ? new Response(file)
+        : new Response("Not found here", { status: 404 });
+    },
+  },
+});
+

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 10

__

Why: The suggestion correctly identifies a critical path traversal security vulnerability in the new static file server and provides a robust, standard solution to fix it.

High
General
Optimize Docker build with selective copy

Optimize the Docker build process by selectively copying package manager files
before the source code to better leverage Docker's layer caching.

apps/frontend/Dockerfile [11-14]

-WORKDIR /app 
-# Install Phase 
-COPY . . 
+WORKDIR /app
+
+# First, copy only the files needed for dependency installation
+COPY turbo.json turbo.json
+COPY package.json package.json
+COPY bun.lockb bun.lockb
+COPY apps/api/package.json apps/api/package.json
+COPY apps/frontend/package.json apps/frontend/package.json
+COPY packages/db/package.json packages/db/package.json
+COPY packages/ui/package.json packages/ui/package.json
+
+# Prune to the minimal workspace
 RUN turbo prune @universal/app --docker
 
+# The rest of the Dockerfile from the installer stage can be used from here
+# by combining the builder and installer stages.
+
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out that COPY . . is inefficient and proposes a standard optimization to improve Docker layer caching, which will speed up subsequent builds.

Medium
High-level
Simplify the custom Bun web server

The custom server in apps/frontend/server.ts has repetitive routing logic for
static assets. This should be refactored into a single, generic static file
handler to improve maintainability and robustness.

Examples:

apps/frontend/server.ts [7-33]
const server = Bun.serve({
  port: 4000,
  routes: {
    "/_expo/static/*": async (request) => {
      const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`);
      const exists = await file.exists();
      return exists
        ? new Response(file)
        : new Response("Not found", { status: 404 });
    },

 ... (clipped 17 lines)

Solution Walkthrough:

Before:

// apps/frontend/server.ts
const server = Bun.serve({
  port: 4000,
  routes: {
    "/_expo/static/*": async (request) => {
      const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`);
      // ... check existence and serve
    },
    "/assets/*": async (request) => {
      const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`);
      // ... check existence and serve
    },
    "/*": async (request) => await handler(request),
    "/canvaskit.wasm": async (_request) => {
      const file = Bun.file("./dist/client/canvaskit.wasm");
      // ... check existence and serve
    },
  },
});

After:

// apps/frontend/server.ts
const serveStaticAsset = async (request: Request) => {
  const pathname = new URL(request.url).pathname;
  const file = Bun.file(`./dist/client/${pathname}`);
  const exists = await file.exists();
  return exists ? new Response(file) : new Response("Not found", { status: 404 });
};

const server = Bun.serve({
  port: 4000,
  async fetch(request) {
    const { pathname } = new URL(request.url);
    if (pathname.startsWith("/assets/") || pathname.startsWith("/_expo/static/") || pathname.endsWith(".wasm")) {
      return serveStaticAsset(request);
    }
    return handler(request);
  },
});
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies duplicated, brittle logic for serving static assets in the new server.ts file and proposes a more robust and maintainable design.

Low
  • Update

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75d7b76 and 0fd251b.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • .gitignore (1 hunks)
  • .vscode/settings.json (1 hunks)
  • apps/api/package.json (1 hunks)
  • apps/frontend/.env (1 hunks)
  • apps/frontend/Dockerfile (1 hunks)
  • apps/frontend/package.json (1 hunks)
  • apps/frontend/server.ts (1 hunks)
  • package.json (1 hunks)
  • packages/db/package.json (1 hunks)
  • turbo.json (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use object destructuring for method parameters (wrap multiple parameters in an object; define shapes with TypeScript interfaces)
Use authClient.useSession() for session state
Avoid React hooks; use Legend State observables directly (exception: Better Auth hooks)
Import UI components via path aliases (e.g., @/src/components/ui/)
Prefer union string/number literals over TypeScript enums
Use export type and import type for types
Don't use non-null assertions (!) or implicit any
Avoid TypeScript namespaces; prefer ESM modules

Files:

  • apps/frontend/server.ts
**/*.{tsx,ts}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use Platform.OS and process.env.EXPO_OS for conditional platform logic

Files:

  • apps/frontend/server.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx,js,jsx}: Don't use the arguments object
Use for...of instead of Array.forEach
Use arrow functions instead of function expressions
Use Date.now() instead of new Date().getTime()
Use .flatMap() instead of map().flat() when possible
Prefer regex literals over RegExp constructor when possible
Use concise optional chaining instead of chained logical expressions
Always provide radix to parseInt()
Don't use var; use const/let and prefer const when single assignment
Use template literals over string concatenation
Use strict equality (===/!==)
Don't use console or debugger in committed code
Prevent import cycles and avoid namespace imports
Don't hardcode secrets (API keys/tokens) in source
Don't have unused vars, params, imports, labels, or private members
Avoid await inside loops; ensure Promise-like statements are handled
Disallow bitwise operators and comma operator
Ensure switch statements are exhaustive; default clause comes last; no fallthrough or duplicate cases
Use object spread over Object.assign when creating new objects
Prefer Array.isArray over instanceof Array; use at() instead of integer index access where appropriate
No unreachable code, duplicate conditions, or assignments in conditionals
Always use radix with parseInt; prefer Number.isNaN/Number.isFinite over globals
Disallow using global eval, with, var, and unsafe negation
Ensure typeof comparisons use valid strings; getters return values; constructors don’t return values

Files:

  • apps/frontend/server.ts
🧠 Learnings (1)
📚 Learning: 2025-09-02T09:56:34.362Z
Learnt from: chakrihacker
PR: fyndx/universal-starter#15
File: apps/backend/package.json:43-55
Timestamp: 2025-09-02T09:56:34.362Z
Learning: For Eden Treaty type safety in monorepo setups, TypeScript source files can be referenced directly in package.json "types" fields (e.g., "./src/index.ts") since Eden Treaty works with compile-time type inference and doesn't require emitted .d.ts files for local development.

Applied to files:

  • apps/api/package.json
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (5)
.vscode/settings.json (1)

35-38: Dockerfile formatter override aligns with container changes.

Line 37 ensures Dockerfiles get formatted with the Azure Containers extension, matching the new Docker workflow and avoiding surprise edits when switching languages.

.gitignore (1)

20-23: Ignore Turbo cache artifacts.

Line 23 keeping .turbo/ out of the repo prevents committing Turbo’s build cache while the existing *.pem rule still shields private keys—nice hygiene.

packages/db/package.json (1)

7-7: Build script now covers Prisma client generation.

Line 7 wiring build to bun run db:generate lets Turbo call into Prisma’s generator directly, so the DB package produces its client artifacts during builds without extra steps.

apps/frontend/.env (1)

1-1: Staging API endpoint switch acknowledged.

Line 1 points local Expo builds at the Coolify-hosted staging API, which lines up with the new Docker/Turbo staging workflow.

apps/api/package.json (1)

9-9: Type-check script completes the Turbo task.

Line 9’s check-types hook gives Turbo a concrete command for API type checks, catching TS regressions without emitting build artifacts.

Comment thread apps/frontend/server.ts Outdated
Comment on lines +10 to +30
"/_expo/static/*": async (request) => {
const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`);
const exists = await file.exists();
return exists
? new Response(file)
: new Response("Not found", { status: 404 });
},
"/assets/*": async (request) => {
const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`);
const exists = await file.exists();
return exists
? new Response(file)
: new Response("Not found", { status: 404 });
},
"/*": async (request) => await handler(request),
"/canvaskit.wasm": async (_request) => {
const file = Bun.file("./dist/client/canvaskit.wasm");
const exists = await file.exists();
return exists
? new Response(file)
: new Response("Not found here", { status: 404 });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Block path traversal in static asset handlers.

new URL(request.url).pathname comes straight from the caller. Because we splice it directly into ./dist/client/${…}, ../ segments are honored by the filesystem, so a request like /_expo/static/../../server.ts lets an attacker read arbitrary files from the container. Please normalize the target path against the exported client root and reject anything that escapes it before calling Bun.file. For example:

+const clientRoot = path.join(import.meta.dir, "dist/client");
+
+const serveStaticAsset = async (request: Request) => {
+  const { pathname } = new URL(request.url);
+  const assetPath = path.resolve(clientRoot, `.${pathname}`);
+  const relative = path.relative(clientRoot, assetPath);
+  if (relative.startsWith("..") || path.isAbsolute(relative)) {
+    return new Response("Not found", { status: 404 });
+  }
+  const file = Bun.file(assetPath);
+  return (await file.exists())
+    ? new Response(file)
+    : new Response("Not found", { status: 404 });
+};
+
 const server = Bun.serve({
   port: 4000,
   routes: {
-    "/_expo/static/*": async (request) => {
-      const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`);
-      const exists = await file.exists();
-      return exists
-        ? new Response(file)
-        : new Response("Not found", { status: 404 });
-    },
-    "/assets/*": async (request) => {
-      const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`);
-      const exists = await file.exists();
-      return exists
-        ? new Response(file)
-        : new Response("Not found", { status: 404 });
-    },
-    "/*": async (request) => await handler(request),
-    "/canvaskit.wasm": async (_request) => {
-      const file = Bun.file("./dist/client/canvaskit.wasm");
-      const exists = await file.exists();
-      return exists
-        ? new Response(file)
-        : new Response("Not found here", { status: 404 });
-    },
+    "/_expo/static/*": serveStaticAsset,
+    "/assets/*": serveStaticAsset,
+    "/canvaskit.wasm": async (_request) => {
+      const file = Bun.file(path.join(clientRoot, "canvaskit.wasm"));
+      return (await file.exists())
+        ? new Response(file)
+        : new Response("Not found here", { status: 404 });
+    },
+    "/*": async (request) => handler(request),
   },
 });

This keeps lookups confined to the exported web bundle and closes the disclosure hole.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"/_expo/static/*": async (request) => {
const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`);
const exists = await file.exists();
return exists
? new Response(file)
: new Response("Not found", { status: 404 });
},
"/assets/*": async (request) => {
const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`);
const exists = await file.exists();
return exists
? new Response(file)
: new Response("Not found", { status: 404 });
},
"/*": async (request) => await handler(request),
"/canvaskit.wasm": async (_request) => {
const file = Bun.file("./dist/client/canvaskit.wasm");
const exists = await file.exists();
return exists
? new Response(file)
: new Response("Not found here", { status: 404 });
// Define the client root directory once
const clientRoot = path.join(import.meta.dir, "dist/client");
// Helper to safely serve static assets under clientRoot
const serveStaticAsset = async (request: Request) => {
const { pathname } = new URL(request.url);
// Resolve against clientRoot and force the leading “.” so that paths like "/foo" map to "./foo"
const assetPath = path.resolve(clientRoot, `.${pathname}`);
// Ensure the resolved path stays within clientRoot
const relative = path.relative(clientRoot, assetPath);
if (relative.startsWith("..") || path.isAbsolute(relative)) {
return new Response("Not found", { status: 404 });
}
const file = Bun.file(assetPath);
return (await file.exists())
? new Response(file)
: new Response("Not found", { status: 404 });
};
const server = Bun.serve({
port: 4000,
routes: {
// Use the shared, hardened handler for both static routes
"/_expo/static/*": serveStaticAsset,
"/assets/*": serveStaticAsset,
// Serve the WebAssembly blob explicitly before the catch-all
"/canvaskit.wasm": async (_request) => {
const file = Bun.file(path.join(clientRoot, "canvaskit.wasm"));
return (await file.exists())
? new Response(file)
: new Response("Not found here", { status: 404 });
},
// Fallback to the application handler
"/*": async (request) => handler(request),
},
});

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0fd251b and 36db85d.

📒 Files selected for processing (1)
  • docker/docker-compose.prod.yml (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review

Comment thread docker/docker-compose.prod.yml Outdated
Comment thread docker-compose.yml
Comment on lines +21 to +29
environment:
- AXIOM_DOMAIN=${AXIOM_DOMAIN:-https://api.axiom.co}
- AXIOM_TOKEN=${AXIOM_TOKEN}
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
- DATABASE_URL=${DATABASE_URL}
- DATASET_NAME=${DATASET_NAME:-universal_starter}
- EMAIL_PROVIDER=${EMAIL_PROVIDER:-resend}
- REDIS_URL=${REDIS_URL}
- RESEND_API_KEY=${RESEND_API_KEY}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t rely on env_file for interpolation

These ${VAR} entries are expanded before service env_files are loaded. Unless each variable is exported in the shell (or defined in a root .env passed via --env-file), Compose will warn that they’re unset and inject empty strings, so secrets like AXIOM_TOKEN will silently become blank at runtime. Either keep the values in the service env_file only, or ensure they’re defined in the interpolation sources (shell/.env adjacent to this compose file) and mark them as required using :? if you want hard failures.(docs.docker.com)

🤖 Prompt for AI Agents
In docker/docker-compose.prod.yml around lines 21 to 29, the service is using
${VAR} interpolation which happens before service env_file loading so secrets
can be empty at runtime; either remove the interpolations and reference the
variables only via the service env_file (so the file contains plain VAR=value
entries), or ensure every interpolated variable is supplied from an
interpolation source (exported in the shell or placed in a root .env adjacent to
this compose file) and make required variables fail fast using the ${VAR:?error}
form; for secrets prefer external env_file or Docker secrets rather than inline
interpolation to avoid accidental empty values.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/frontend/.env (1)

1-1: Ensure no double slashes in auth-client baseURL

In apps/frontend/src/lib/auth-client.ts at line 22, you build baseURL as

`${env.EXPO_PUBLIC_API_URL}/api/auth`

Since EXPO_PUBLIC_API_URL now ends with a slash, this yields //api/auth. Either strip the trailing slash from EXPO_PUBLIC_API_URL (e.g. env.EXPO_PUBLIC_API_URL.replace(/\/$/, '')) or switch to a URL-safe join such as

new URL('/api/auth', env.EXPO_PUBLIC_API_URL).toString()

to normalize paths.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 50f8270 and a13031b.

📒 Files selected for processing (6)
  • apps/frontend/.env (1 hunks)
  • apps/frontend/.env.example (1 hunks)
  • apps/frontend/Dockerfile (1 hunks)
  • apps/frontend/src/config/env.ts (1 hunks)
  • docker-compose.yml (1 hunks)
  • docker/Dockerfile.base (0 hunks)
💤 Files with no reviewable changes (1)
  • docker/Dockerfile.base
✅ Files skipped from review due to trivial changes (1)
  • docker-compose.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/frontend/Dockerfile
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use object destructuring for method parameters (wrap multiple parameters in an object; define shapes with TypeScript interfaces)
Use authClient.useSession() for session state
Avoid React hooks; use Legend State observables directly (exception: Better Auth hooks)
Import UI components via path aliases (e.g., @/src/components/ui/)
Prefer union string/number literals over TypeScript enums
Use export type and import type for types
Don't use non-null assertions (!) or implicit any
Avoid TypeScript namespaces; prefer ESM modules

Files:

  • apps/frontend/src/config/env.ts
**/*.{tsx,ts}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use Platform.OS and process.env.EXPO_OS for conditional platform logic

Files:

  • apps/frontend/src/config/env.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx,js,jsx}: Don't use the arguments object
Use for...of instead of Array.forEach
Use arrow functions instead of function expressions
Use Date.now() instead of new Date().getTime()
Use .flatMap() instead of map().flat() when possible
Prefer regex literals over RegExp constructor when possible
Use concise optional chaining instead of chained logical expressions
Always provide radix to parseInt()
Don't use var; use const/let and prefer const when single assignment
Use template literals over string concatenation
Use strict equality (===/!==)
Don't use console or debugger in committed code
Prevent import cycles and avoid namespace imports
Don't hardcode secrets (API keys/tokens) in source
Don't have unused vars, params, imports, labels, or private members
Avoid await inside loops; ensure Promise-like statements are handled
Disallow bitwise operators and comma operator
Ensure switch statements are exhaustive; default clause comes last; no fallthrough or duplicate cases
Use object spread over Object.assign when creating new objects
Prefer Array.isArray over instanceof Array; use at() instead of integer index access where appropriate
No unreachable code, duplicate conditions, or assignments in conditionals
Always use radix with parseInt; prefer Number.isNaN/Number.isFinite over globals
Disallow using global eval, with, var, and unsafe negation
Ensure typeof comparisons use valid strings; getters return values; constructors don’t return values

Files:

  • apps/frontend/src/config/env.ts
🪛 dotenv-linter (3.3.0)
apps/frontend/.env.example

[warning] 1-1: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)

apps/frontend/.env

[warning] 1-1: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (1)
apps/frontend/src/config/env.ts (1)

4-9: No lingering EXPO_PUBLIC_STORAGE_PREFIX usages found—please verify non-code sources
Ripgrep returned zero matches for the removed variable. Manually confirm it isn’t referenced in Docker/CI configs, documentation or uncommitted .env files.

@@ -1,2 +1 @@
EXPO_PUBLIC_API_URL=https://universal-starter.onrender.com
EXPO_PUBLIC_STORAGE_PREFIX=universal-starter No newline at end of file
EXPO_PUBLIC_API_URL=https://universal-starter.onrender.com No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Synchronize example API URL with the new default

.env now defaults to https://staging-universal-api.coolify.fyndx.io/, but the example still points at the old Render endpoint. Anyone bootstrapping from the example will talk to the wrong API. Please align the example with the current default.

-EXPO_PUBLIC_API_URL=https://universal-starter.onrender.com
+EXPO_PUBLIC_API_URL=https://staging-universal-api.coolify.fyndx.io/
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
EXPO_PUBLIC_API_URL=https://universal-starter.onrender.com
# apps/frontend/.env.example
EXPO_PUBLIC_API_URL=https://staging-universal-api.coolify.fyndx.io/
🧰 Tools
🪛 dotenv-linter (3.3.0)

[warning] 1-1: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)

🤖 Prompt for AI Agents
In apps/frontend/.env.example around lines 1 to 1, the EXPO_PUBLIC_API_URL value
still points to the old Render endpoint; update this example value to match the
new default API URL (https://staging-universal-api.coolify.fyndx.io/) so that
developers bootstrapping from the example use the current default endpoint.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a13031b and 6b82b39.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • apps/frontend/package.json (1 hunks)
  • apps/frontend/server.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use object destructuring for method parameters (wrap multiple parameters in an object; define shapes with TypeScript interfaces)
Use authClient.useSession() for session state
Avoid React hooks; use Legend State observables directly (exception: Better Auth hooks)
Import UI components via path aliases (e.g., @/src/components/ui/)
Prefer union string/number literals over TypeScript enums
Use export type and import type for types
Don't use non-null assertions (!) or implicit any
Avoid TypeScript namespaces; prefer ESM modules

Files:

  • apps/frontend/server.ts
**/*.{tsx,ts}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use Platform.OS and process.env.EXPO_OS for conditional platform logic

Files:

  • apps/frontend/server.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx,js,jsx}: Don't use the arguments object
Use for...of instead of Array.forEach
Use arrow functions instead of function expressions
Use Date.now() instead of new Date().getTime()
Use .flatMap() instead of map().flat() when possible
Prefer regex literals over RegExp constructor when possible
Use concise optional chaining instead of chained logical expressions
Always provide radix to parseInt()
Don't use var; use const/let and prefer const when single assignment
Use template literals over string concatenation
Use strict equality (===/!==)
Don't use console or debugger in committed code
Prevent import cycles and avoid namespace imports
Don't hardcode secrets (API keys/tokens) in source
Don't have unused vars, params, imports, labels, or private members
Avoid await inside loops; ensure Promise-like statements are handled
Disallow bitwise operators and comma operator
Ensure switch statements are exhaustive; default clause comes last; no fallthrough or duplicate cases
Use object spread over Object.assign when creating new objects
Prefer Array.isArray over instanceof Array; use at() instead of integer index access where appropriate
No unreachable code, duplicate conditions, or assignments in conditionals
Always use radix with parseInt; prefer Number.isNaN/Number.isFinite over globals
Disallow using global eval, with, var, and unsafe negation
Ensure typeof comparisons use valid strings; getters return values; constructors don’t return values

Files:

  • apps/frontend/server.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (1)
apps/frontend/server.ts (1)

13-26: Sanitize static file paths to prevent traversal

new URL(request.url).pathname goes straight into ./dist/client/${…}. Requests such as /_expo/static/../../server.ts escape the client bundle and expose arbitrary files from the container. Normalize the target against a fixed clientRoot, reject anything that leaves it, and reuse the sanitized path for all static routes.

-const server = Bun.serve({
-  port: 4000,
-  routes: {
-    "/_expo/static/*": async (request) => {
-      const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`);
-      const exists = await file.exists();
-      return exists
-        ? new Response(file)
-        : new Response("Not found", { status: 404 });
-    },
-    "/assets/*": async (request) => {
-      const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`);
-      const exists = await file.exists();
-      return exists
-        ? new Response(file)
-        : new Response("Not found", { status: 404 });
-    },
+const clientRoot = path.join(import.meta.dir, "dist/client");
+
+const serveStatic = async (request: Request) => {
+  const { pathname } = new URL(request.url);
+  const resolved = path.resolve(clientRoot, `.${pathname}`);
+  if (!resolved.startsWith(clientRoot + path.sep)) {
+    return new Response("Not found", { status: 404 });
+  }
+  const file = Bun.file(resolved);
+  return (await file.exists())
+    ? new Response(file)
+    : new Response("Not found", { status: 404 });
+};
+
+const server = Bun.serve({
+  port: 4000,
+  routes: {
+    "/_expo/static/*": serveStatic,
+    "/assets/*": serveStatic,

Comment thread apps/frontend/server.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
apps/frontend/Dockerfile (1)

7-10: Consider if watchman is necessary in the builder stage.

Watchman is typically used for file watching during development, which isn't needed during Docker builds. This adds unnecessary dependencies and increases build time.

-RUN apt-get update && \ 
-    apt-get install -y watchman && \ 
-    apt-get clean && \ 
-    rm -rf /var/lib/apt/lists/*
+# Removed watchman as it's not needed for Docker builds
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0b56887 and e306248.

📒 Files selected for processing (5)
  • apps/api/.dockerignore (1 hunks)
  • apps/api/Dockerfile (1 hunks)
  • apps/frontend/Dockerfile (1 hunks)
  • apps/workers/Dockerfile (1 hunks)
  • apps/workers/package.json (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • apps/workers/package.json
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (4)
apps/api/.dockerignore (1)

1-96: Comprehensive .dockerignore patterns look good.

The expanded ignore rules provide excellent coverage for dependencies, build artifacts, environment files, and development-specific directories. This significantly reduces the Docker build context size and prevents sensitive files from being included in the image.

The patterns are well-organized into logical sections and follow Docker best practices.

apps/workers/Dockerfile (1)

1-4: LGTM: Standard multi-stage setup with global turbo.

The base image setup and global turbo installation follow the consistent pattern across services.

apps/api/Dockerfile (2)

2-4: LGTM: Consistent multi-stage setup.

The base image setup with global turbo installation follows the established pattern across all services.


22-23: Good practice: Creating public directory before build.

Ensuring the public directory exists before the build prevents potential build failures.

Comment thread apps/api/Dockerfile
Comment on lines +29 to 33
# Copy built server and public assets
COPY --from=installer /app/apps/api/server server
COPY --from=installer /app/apps/api/.env* ./
COPY --from=installer /app/apps/api/public ./public

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Security concern: Environment file handling and user permissions.

Similar to the workers Dockerfile, copying .env* files to the production image may expose sensitive configuration. Also consider running as non-root user.

 # Production image
 FROM gcr.io/distroless/base
 WORKDIR /app
+USER nonroot

 # Copy built server and public assets
 COPY --from=installer /app/apps/api/server server
-COPY --from=installer /app/apps/api/.env* ./
+# Consider using environment variables from Docker Compose instead
 COPY --from=installer /app/apps/api/public ./public
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Copy built server and public assets
COPY --from=installer /app/apps/api/server server
COPY --from=installer /app/apps/api/.env* ./
COPY --from=installer /app/apps/api/public ./public
# Production image
FROM gcr.io/distroless/base
WORKDIR /app
USER nonroot
# Copy built server and public assets
COPY --from=installer /app/apps/api/server server
# Consider using environment variables from Docker Compose instead
COPY --from=installer /app/apps/api/public ./public
🤖 Prompt for AI Agents
In apps/api/Dockerfile around lines 29 to 33, the Dockerfile copies `.env*` into
the production image and runs as root, which risks leaking secrets and running
with excessive privileges; remove the COPY of `.env*` from the final image (use
build-time ARGs, Docker secrets, or environment variables injected by the
orchestrator instead) and ensure the container runs as a non-root user by adding
a non-root USER (create/group and chown files or set appropriate permissions in
the builder stage) so the runtime image contains no sensitive env files and does
not run as root.

Comment thread apps/frontend/Dockerfile
Comment on lines +22 to +23
ARG EXPO_PUBLIC_API_URL
ENV EXPO_PUBLIC_API_URL=$EXPO_PUBLIC_API_URL
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add default value for build argument.

The EXPO_PUBLIC_API_URL build argument lacks a default value, which could cause build failures if not provided.

-ARG EXPO_PUBLIC_API_URL
+ARG EXPO_PUBLIC_API_URL=http://localhost:3000
 ENV EXPO_PUBLIC_API_URL=$EXPO_PUBLIC_API_URL
🤖 Prompt for AI Agents
In apps/frontend/Dockerfile around lines 22 to 23, the build ARG
EXPO_PUBLIC_API_URL has no default which can break builds when not supplied;
update the ARG to include a sensible default (for example ARG
EXPO_PUBLIC_API_URL=http://localhost:3000 or an empty string) and keep the ENV
EXPO_PUBLIC_API_URL=$EXPO_PUBLIC_API_URL line so the environment variable is
populated when no build-arg is provided.

Comment thread apps/frontend/Dockerfile
Comment on lines +28 to +34
FROM gcr.io/distroless/base AS web
WORKDIR /app
ENV NODE_ENV=production
COPY --from=installer /app/apps/frontend/web-server /app/server
COPY --from=installer /app/apps/frontend/dist /app/dist
CMD [ "./server" ]
EXPOSE 4000 No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix CMD syntax and add security user directive.

The CMD syntax should use array format, and consider running as non-root user for better security.

 FROM gcr.io/distroless/base AS web 
 WORKDIR /app 
 ENV NODE_ENV=production 
+USER nonroot
 COPY --from=installer /app/apps/frontend/web-server /app/server 
 COPY --from=installer /app/apps/frontend/dist /app/dist 
-CMD [ "./server" ] 
+CMD ["./server"]
 EXPOSE 4000
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
FROM gcr.io/distroless/base AS web
WORKDIR /app
ENV NODE_ENV=production
COPY --from=installer /app/apps/frontend/web-server /app/server
COPY --from=installer /app/apps/frontend/dist /app/dist
CMD [ "./server" ]
EXPOSE 4000
FROM gcr.io/distroless/base AS web
WORKDIR /app
ENV NODE_ENV=production
USER nonroot
COPY --from=installer /app/apps/frontend/web-server /app/server
COPY --from=installer /app/apps/frontend/dist /app/dist
CMD ["./server"]
EXPOSE 4000
🤖 Prompt for AI Agents
In apps/frontend/Dockerfile around lines 28-34, change the CMD to JSON/exec form
and run the container as a non-root user: update CMD to use array syntax (e.g.
["./server"]) and add a USER directive (e.g. USER 1000) to drop root privileges;
ensure the installer stage sets ownership of /app, /app/server and /app/dist to
that UID (chown in the installer stage or create the user there) so the non-root
user can read/execute the files before copying into the distroless image.

Comment thread apps/workers/Dockerfile
Comment on lines +27 to 29
COPY --from=installer /app/apps/workers/worker worker
COPY --from=installer /app/apps/workers/.env* ./

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Security concern: Review environment file handling.

Copying .env* files to the production image may expose sensitive configuration. Consider using explicit environment variables in the Docker Compose configuration instead of copying files.

Additionally, the distroless image runs as root by default. Consider adding a USER directive if the worker doesn't require root privileges.

 # Production image
 FROM gcr.io/distroless/base
 WORKDIR /app
+# Run as non-root user (distroless provides nonroot user)
+USER nonroot

 COPY --from=installer /app/apps/workers/worker worker
-COPY --from=installer /app/apps/workers/.env* ./
+# Consider using environment variables from Docker Compose instead of copying .env files
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
COPY --from=installer /app/apps/workers/worker worker
COPY --from=installer /app/apps/workers/.env* ./
# Production image
FROM gcr.io/distroless/base
WORKDIR /app
# Run as non-root user (distroless provides nonroot user)
USER nonroot
COPY --from=installer /app/apps/workers/worker worker
# Consider using environment variables from Docker Compose instead of copying .env files
🤖 Prompt for AI Agents
In apps/workers/Dockerfile around lines 27-29, the Dockerfile copies `.env*`
into the production image (exposing secrets) and the distroless base runs as
root; remove the COPY --from=installer /app/apps/workers/.env* ./ line and
instead surface required values via explicit build-time ARGs or runtime
environment variables passed in Docker Compose/Kubernetes (or mount a secrets
file at runtime), update documentation to list required env names and defaults,
and add a non-root USER directive appropriate for the distroless image (create
or switch to a low-privilege user) so the worker does not run as root.

@chakrihacker chakrihacker merged commit b7bb162 into main Sep 29, 2025
1 of 3 checks passed
@chakrihacker chakrihacker deleted the feat/enhance-docker-setup branch September 29, 2025 20:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant