Skip to content
Merged
Show file tree
Hide file tree
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
10 changes: 10 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
node_modules
.git
.github
.next
dist
*.md
!README.md
.env*
.turbo
apps/desktop
16 changes: 16 additions & 0 deletions apps/api/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM node:22-slim
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable && corepack prepare pnpm@9.0.0 --activate

WORKDIR /app
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json turbo.json ./
COPY apps/api/ ./apps/api/
COPY packages/ ./packages/
RUN pnpm install --frozen-lockfile
RUN pnpm --filter @repo/api build
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -i "package.json" apps/api --exec sh -c 'echo "== $1 =="; jq -r ".scripts.build" "$1"' sh {}

Repository: BuckyMcYolo/townhall

Length of output: 153


Fix the build script in apps/api/package.json to use tsc && tsc-alias instead of tsup && tsc.

Line 11 calls pnpm --filter @repo/api build, which currently executes tsup && tsc --declaration --emitDeclarationOnly --outDir dist. This violates the required API compile contract: the build must use tsc && tsc-alias (no bundler) to properly resolve workspace dependencies via pnpm linking and ensure correct runtime import paths.

🧰 Tools
🪛 Hadolint (2.14.0)

[info] 11-11: Multiple consecutive RUN instructions. Consider consolidation.

(DL3059)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/Dockerfile` at line 11, The package's build script currently uses a
bundler and wrong order (tsup && tsc); update the apps/api package.json "build"
script to run the TypeScript compiler then path-fix step: replace the existing
"build" command with "tsc && tsc-alias" (ensure tsc is configured to emit
declarations/outDir as before and tsc-alias runs against that outDir), and keep
the Dockerfile call (pnpm --filter `@repo/api` build) unchanged so the image runs
the corrected build pipeline; verify tsconfig.json output paths and that
tsc-alias is added to devDependencies if missing.


ENV NODE_ENV=production
USER node
EXPOSE 8080
CMD ["node", "apps/api/dist/index.js"]
Comment on lines +1 to +16
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

🧩 Analysis chain

🏁 Script executed:

# Check if the package.json file exists and read the build script
if [ -f "apps/api/package.json" ]; then
  cat apps/api/package.json | jq '.scripts.build // "Not found"'
else
  echo "apps/api/package.json not found"
fi

Repository: BuckyMcYolo/townhall

Length of output: 127


🌐 Web query:

node:22-slim docker image user configuration non-root

💡 Result:

The node:22-slim Docker image runs as the root user by default. To configure it for non-root operation using the built-in 'node' user (UID 1000), add these steps to your Dockerfile after installing dependencies but before the final CMD/ENTRYPOINT: 1. Change ownership of your application directory to the node user: RUN chown -R node:node /app 2. Switch to the non-root user: USER node Example complete Dockerfile: FROM node:22-slim WORKDIR /app COPY package*.json ./ RUN npm ci --only=production && npm cache clean --force COPY --chown=node:node . . EXPOSE 3000 USER node CMD ["node", "server.js"] This follows official Node Docker best practices for security (principle of least privilege). The entrypoint.sh script supports running as node user. Verify with docker run --rm node:22-slim whoami (outputs 'root') vs. after USER node (outputs 'node'). Key benefits: - Reduces attack surface if compromised - Meets security scanners/compliance requirements - No performance impact Common issues & fixes: - Permission errors on bind mounts: Use --user $(id -u):$(id -g) or volume permissions - Ports <1024: Use high ports (>1024) or CAP_NET_BIND_SERVICE - npm install as root first, then chown/switch

Citations:


Run API service as non-root in the final image.

The Dockerfile lacks a USER directive, so the container runs as root. Add USER node before the CMD statement to run the service with the built-in unprivileged user.

Suggested hardening patch
 FROM node:22-slim
 ENV PNPM_HOME="/pnpm"
 ENV PATH="$PNPM_HOME:$PATH"
 RUN corepack enable && corepack prepare pnpm@9.0.0 --activate
 
 WORKDIR /app
 COPY pnpm-lock.yaml pnpm-workspace.yaml package.json turbo.json ./
 COPY apps/api/ ./apps/api/
 COPY packages/ ./packages/
 RUN pnpm install --frozen-lockfile
 RUN pnpm --filter `@repo/api` build
 
 ENV NODE_ENV=production
 EXPOSE 8080
+USER node
 CMD ["node", "apps/api/dist/index.js"]
📝 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 node:22-slim
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable && corepack prepare pnpm@9.0.0 --activate
WORKDIR /app
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json turbo.json ./
COPY apps/api/ ./apps/api/
COPY packages/ ./packages/
RUN pnpm install --frozen-lockfile
RUN pnpm --filter @repo/api build
ENV NODE_ENV=production
EXPOSE 8080
CMD ["node", "apps/api/dist/index.js"]
FROM node:22-slim
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable && corepack prepare pnpm@9.0.0 --activate
WORKDIR /app
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json turbo.json ./
COPY apps/api/ ./apps/api/
COPY packages/ ./packages/
RUN pnpm install --frozen-lockfile
RUN pnpm --filter `@repo/api` build
ENV NODE_ENV=production
EXPOSE 8080
USER node
CMD ["node", "apps/api/dist/index.js"]
🧰 Tools
🪛 Checkov (3.2.510)

[low] 1-15: Ensure that HEALTHCHECK instructions have been added to container images

(CKV_DOCKER_2)


[low] 1-15: Ensure that a user for the container has been created

(CKV_DOCKER_3)

🪛 Hadolint (2.14.0)

[info] 11-11: Multiple consecutive RUN instructions. Consider consolidation.

(DL3059)

🪛 Trivy (0.69.3)

[error] 1-1: Image user should not be 'root'

Specify at least 1 USER command in Dockerfile with non-root user as argument

Rule: DS-0002

Learn more

(IaC/Dockerfile)


[info] 1-1: No HEALTHCHECK defined

Add HEALTHCHECK instruction in your Dockerfile

Rule: DS-0026

Learn more

(IaC/Dockerfile)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/Dockerfile` around lines 1 - 15, The image currently runs the API as
root; add a non-root user step by setting USER node before the CMD to run the
final process unprivileged, and ensure the built assets are owned/readable by
that user by adjusting ownership/permissions after build (references: WORKDIR
/app, RUN pnpm --filter `@repo/api` build, and CMD ["node",
"apps/api/dist/index.js"]); place the USER node directive just before the CMD
and add a chown/chmod step after build so the node user can access /app and
apps/api/dist.

4 changes: 3 additions & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"dev": "tsx watch src/index.ts",
"build": "tsup && tsc --declaration --emitDeclarationOnly --outDir dist",
"start": "node dist/index.js",
"check-types": "tsc --noEmit"
"check-types": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.1004.0",
Expand Down
41 changes: 41 additions & 0 deletions apps/api/src/__tests__/auth-guard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, expect, it } from "vitest"
import { client } from "./helpers"

describe("Auth Guard — unauthenticated requests return 401", () => {
it("GET /v1/allies", async () => {
const res = await client.v1.allies.$get()
expect(res.status).toBe(401)
})

it("GET /v1/dms", async () => {
const res = await client.v1.dms.$get({
query: {
page: 1,
perPage: 50,
},
})
expect(res.status).toBe(401)
})

it("GET /v1/blocks", async () => {
const res = await client.v1.blocks.$get()
expect(res.status).toBe(401)
})

it("GET /v1/notification-settings", async () => {
const res = await client.v1["notification-settings"].$get()
expect(res.status).toBe(401)
})

it("GET /v1/privacy-settings", async () => {
const res = await client.v1["privacy-settings"].$get()
expect(res.status).toBe(401)
})

it("POST /v1/allies/requests", async () => {
const res = await client.v1.allies.requests.$post({
json: { userId: "fake-id" },
})
expect(res.status).toBe(401)
})
})
15 changes: 15 additions & 0 deletions apps/api/src/__tests__/health.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { describe, expect, it } from "vitest"
import app from "@/app"

describe("Health Check", () => {
it("GET / returns 200 with status ok", async () => {
const res = await app.request("/")
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ status: "ok" })
})

it("GET /unknown returns 404", async () => {
const res = await app.request("/this-does-not-exist")
expect(res.status).toBe(404)
})
})
22 changes: 22 additions & 0 deletions apps/api/src/__tests__/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { ClientRequestOptions } from "hono/client"
import { testClient } from "hono/testing"
import app, { type AppType } from "@/app"

/**
* Typed test client for the Townhall API.
* Provides autocomplete on routes, params, and response bodies.
*/
export const client = testClient<AppType>(app as unknown as AppType)
Comment on lines +3 to +9
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the unsafe cast and app export/type mismatch evidence.
rg -n 'testClient<AppType>\(app as unknown as AppType\)' apps/api/src/__tests__/helpers.ts
rg -n 'const app = createApp\(\)|const routes = app|export type AppType|export default app|export default routes' apps/api/src/app.ts

Repository: BuckyMcYolo/townhall

Length of output: 299


🏁 Script executed:

rg -n "from ['\"]@/app['\"]|from ['\"]./app['\"]" apps/api/src --type ts --type tsx

Repository: BuckyMcYolo/townhall

Length of output: 91


🏁 Script executed:

rg -n "from ['\"]@/app['\"]" apps/api/src -A 1

Repository: BuckyMcYolo/townhall

Length of output: 399


🏁 Script executed:

cat -n apps/api/src/index.ts

Repository: BuckyMcYolo/townhall

Length of output: 354


🏁 Script executed:

cat -n apps/api/src/app.ts

Repository: BuckyMcYolo/townhall

Length of output: 2700


🏁 Script executed:

cat -n apps/api/src/__tests__/health.test.ts

Repository: BuckyMcYolo/townhall

Length of output: 606


🏁 Script executed:

# Check what methods are available on both app and routes
rg -A 5 "const app = createApp" apps/api/src/lib/helpers/app/create-app.ts | head -20

Repository: BuckyMcYolo/townhall

Length of output: 46


🏁 Script executed:

fd create-app apps/api/src

Repository: BuckyMcYolo/townhall

Length of output: 106


🏁 Script executed:

cat -n apps/api/src/lib/helpers/app/create-app.ts

Repository: BuckyMcYolo/townhall

Length of output: 1164


🏁 Script executed:

# Verify that app and routes are the same Hono instance
# by checking if .route() returns the same instance (fluent API)
rg -n "\.route\(" apps/api/src/app.ts | head -5

Repository: BuckyMcYolo/townhall

Length of output: 222


🏁 Script executed:

# Verify the type relationship: check if changing default export would work
# by confirming that app at line 72 has all routes attached
sed -n '38,72p' apps/api/src/app.ts

Repository: BuckyMcYolo/townhall

Length of output: 1008


Remove the unsafe double-cast in test client setup.

Line 9 (app as unknown as AppType) hides a real type mismatch. The imported app is the base Hono instance, but AppType represents the fully-routed app type. While both are the same runtime object (Hono's fluent API returns the instance), TypeScript sees them as different types.

Fix this by exporting the fully-routed app type from apps/api/src/app.ts. Change the default export from app to routes on line 72, so the export matches the type definition. Then update the three files that import from @/app (index.ts, health.test.ts, and helpers.ts) to use the correctly-typed export without casting.

Suggested changes
// apps/api/src/app.ts
export type AppType = typeof routes

- export default app
+ export default routes
// apps/api/src/__tests__/helpers.ts
- export const client = testClient<AppType>(app as unknown as AppType)
+ export const client = testClient<AppType>(app)
// apps/api/src/index.ts (also update references if needed)
- import app from "@/app"
+ import app from "@/app"  // Now correctly typed as routed app

- serve({ fetch: app.fetch, port: env.PORT })
+ serve({ fetch: app.fetch, port: env.PORT })
// apps/api/src/__tests__/health.test.ts (also update references if needed)
- import app from "@/app"
+ import app from "@/app"  // Now correctly typed as routed app
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/__tests__/helpers.ts` around lines 3 - 9, The test client uses
an unsafe double-cast (app as unknown as AppType); fix by exporting the
fully-routed Hono instance under the correct symbol and updating imports: in
apps/api/src/app.ts change the default export to export the fully-routed
instance as "routes" (ensure the variable name that holds the routed Hono app is
"routes" and its type matches AppType), then update all import sites that
currently import the default from "@/app" (index.ts, health.test.ts, and
apps/api/src/__tests__/helpers.ts) to import the named/exported "routes" (or the
corrected export) and remove the casts so testClient<AppType>(routes) uses the
proper type without any as unknown as AppType conversion; ensure the exported
symbol name you reference matches the AppType definition.

/**
* Creates request options with a session cookie for authenticated requests.
*
* Usage:
* const res = await client.v1.allies.$get(undefined, withSession(cookie))
*/
export function withSession(sessionCookie: string): ClientRequestOptions {
return {
headers: {
Cookie: sessionCookie,
},
}
}
14 changes: 14 additions & 0 deletions apps/api/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { resolve } from "node:path"
import { defineConfig } from "vitest/config"

export default defineConfig({
test: {
environment: "node",
include: ["src/**/*.test.ts"],
},
resolve: {
alias: {
"@": resolve(__dirname, "src"),
},
},
})
16 changes: 16 additions & 0 deletions apps/realtime/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM node:22-slim
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable && corepack prepare pnpm@9.0.0 --activate

WORKDIR /app
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json turbo.json ./
COPY apps/realtime/ ./apps/realtime/
COPY packages/ ./packages/
RUN pnpm install --frozen-lockfile
RUN pnpm --filter @repo/realtime build

ENV NODE_ENV=production
USER node
EXPOSE 8000
CMD ["node", "apps/realtime/dist/index.js"]
Comment on lines +1 to +16
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🌐 Web query:

node:22-slim docker image node user

💡 Result:

The node:22-slim Docker image is based on Debian bookworm-slim and creates a non-root 'node' user (UID 1000, GID 1000) with home directory /home/node. The default entrypoint (docker-entrypoint.sh) switches to this 'node' user if no USER is specified or if running as root, making the effective default user 'node' for running Node.js applications. This follows Docker best practices for security by avoiding root. The slim variant minimizes packages while including essentials for Node.js. To verify locally: docker run --rm node:22-slim whoami # outputs 'node' docker run --rm node:22-slim id # outputs uid=1000(node) gid=1000(node) The Dockerfile (from repo github.com/nodejs/docker-node, 22/bookworm-slim/Dockerfile) includes: RUN groupadd --gid 1000 node && useradd --uid 1000 --gid node --shell /bin/bash --create-home node No explicit USER node at the end, but entrypoint handles the switch: setUserIfNeeded function checks if running as root/PID 1 and su-exec to node if so.

Citations:


🏁 Script executed:

# Check if there are any other Dockerfiles or container configurations in the repo
find . -name "Dockerfile*" -o -name "*.docker" | head -20

Repository: BuckyMcYolo/townhall

Length of output: 137


🏁 Script executed:

# Look at the actual Dockerfile content and surrounding context
cat -n apps/realtime/Dockerfile

Repository: BuckyMcYolo/townhall

Length of output: 598


🏁 Script executed:

# Check the other Dockerfiles to see if there's a pattern
for dockerfile in ./apps/api/Dockerfile ./apps/worker/Dockerfile; do
  echo "=== $dockerfile ==="
  cat -n "$dockerfile"
done

Repository: BuckyMcYolo/townhall

Length of output: 1149


Explicitly run as non-root user for clarity and consistent file permissions.

The node:22-slim base image includes an entrypoint that automatically switches to the node user, so the container already runs with non-root privileges. However, adding an explicit USER node directive improves clarity and ensures copied files have correct ownership:

Suggested improvement
 FROM node:22-slim
 ENV PNPM_HOME="/pnpm"
 ENV PATH="$PNPM_HOME:$PATH"
 RUN corepack enable && corepack prepare pnpm@9.0.0 --activate
 
 WORKDIR /app
-COPY pnpm-lock.yaml pnpm-workspace.yaml package.json turbo.json ./
-COPY apps/realtime/ ./apps/realtime/
-COPY packages/ ./packages/
+COPY --chown=node:node pnpm-lock.yaml pnpm-workspace.yaml package.json turbo.json ./
+COPY --chown=node:node apps/realtime/ ./apps/realtime/
+COPY --chown=node:node packages/ ./packages/
 RUN pnpm install --frozen-lockfile
 RUN pnpm --filter `@repo/realtime` build
 
 ENV NODE_ENV=production
 EXPOSE 8000
+USER node
 CMD ["node", "apps/realtime/dist/index.js"]
📝 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 node:22-slim
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable && corepack prepare pnpm@9.0.0 --activate
WORKDIR /app
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json turbo.json ./
COPY apps/realtime/ ./apps/realtime/
COPY packages/ ./packages/
RUN pnpm install --frozen-lockfile
RUN pnpm --filter @repo/realtime build
ENV NODE_ENV=production
EXPOSE 8000
CMD ["node", "apps/realtime/dist/index.js"]
FROM node:22-slim
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable && corepack prepare pnpm@9.0.0 --activate
WORKDIR /app
COPY --chown=node:node pnpm-lock.yaml pnpm-workspace.yaml package.json turbo.json ./
COPY --chown=node:node apps/realtime/ ./apps/realtime/
COPY --chown=node:node packages/ ./packages/
RUN pnpm install --frozen-lockfile
RUN pnpm --filter `@repo/realtime` build
ENV NODE_ENV=production
EXPOSE 8000
USER node
CMD ["node", "apps/realtime/dist/index.js"]
🧰 Tools
🪛 Checkov (3.2.510)

[low] 1-15: Ensure that HEALTHCHECK instructions have been added to container images

(CKV_DOCKER_2)


[low] 1-15: Ensure that a user for the container has been created

(CKV_DOCKER_3)

🪛 Hadolint (2.14.0)

[info] 11-11: Multiple consecutive RUN instructions. Consider consolidation.

(DL3059)

🪛 Trivy (0.69.3)

[error] 1-1: Image user should not be 'root'

Specify at least 1 USER command in Dockerfile with non-root user as argument

Rule: DS-0002

Learn more

(IaC/Dockerfile)


[info] 1-1: No HEALTHCHECK defined

Add HEALTHCHECK instruction in your Dockerfile

Rule: DS-0026

Learn more

(IaC/Dockerfile)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/realtime/Dockerfile` around lines 1 - 15, Add an explicit non-root user
switch to the Dockerfile: keep the current root-required build steps (COPY, RUN
pnpm install, RUN pnpm --filter `@repo/realtime` build) but after those steps
change ownership of /app to the node user (e.g., chown -R node:node /app) and
then add USER node before EXPOSE/CMD so the container runs as the non-root node
user; reference the existing WORKDIR /app, COPY, RUN pnpm install, RUN pnpm
--filter `@repo/realtime` build and CMD ["node", "apps/realtime/dist/index.js"]
when making these changes.

15 changes: 15 additions & 0 deletions apps/worker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM node:22-slim
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable && corepack prepare pnpm@9.0.0 --activate

WORKDIR /app
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json turbo.json ./
COPY apps/worker/ ./apps/worker/
COPY packages/ ./packages/
RUN pnpm install --frozen-lockfile
RUN pnpm --filter @repo/worker build

ENV NODE_ENV=production
USER node
CMD ["node", "apps/worker/dist/index.js"]
Comment on lines +1 to +15
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

🧩 Analysis chain

🏁 Script executed:

# Check the actual Dockerfile content
find . -name "Dockerfile" -path "*/worker/*" | head -20

Repository: BuckyMcYolo/townhall

Length of output: 88


🏁 Script executed:

# Also search for realtime and api Dockerfiles mentioned in the comment
find . -name "Dockerfile" | grep -E "(realtime|api)" | head -20

Repository: BuckyMcYolo/townhall

Length of output: 112


🌐 Web query:

node:22-slim Docker image non-root user documentation

💡 Result:

The official node:22-slim Docker image includes a non-root 'node' user (UID 1000, GID 1000) by default, created in its Dockerfile. It does not run as this user by default (runs as root), but documentation recommends switching to it for security following the principle of least privilege. To run as non-root user: 1. In docker-compose.yml: services: node: image: node:22-slim user: "node" working_dir: /home/node/app ... 2. In docker run: docker run -it --rm -u node -v "$PWD":/home/node/app node:22-slim node app.js 3. In Dockerfile: FROM node:22-slim ... USER node CMD ["node", "app.js"] For mounted volumes, ensure permissions allow the node user access, e.g., chown node:node /path on host or use --user $(id -u):$(id -g). Primary documentation sources: nodejs/docker-node GitHub repo README and BestPractices.md, Docker Hub node page linking to GitHub "How to use this image", Docker blog posts on Node Official Image.

Citations:


🏁 Script executed:

# Read the worker Dockerfile to verify current state
cat -n ./apps/worker/Dockerfile

Repository: BuckyMcYolo/townhall

Length of output: 571


🏁 Script executed:

# Check the api Dockerfile
cat -n ./apps/api/Dockerfile

Repository: BuckyMcYolo/townhall

Length of output: 578


🏁 Script executed:

# Check the realtime Dockerfile
cat -n ./apps/realtime/Dockerfile

Repository: BuckyMcYolo/townhall

Length of output: 598


Drop root privileges in the worker image.

Lines 1–14 do not declare USER, so the worker runs as root. The node:22-slim base image includes a non-root node user but does not use it by default. This pattern exists across all three service Dockerfiles (worker, api, realtime) and should be hardened for all.

Suggested hardening patch
 FROM node:22-slim
 ENV PNPM_HOME="/pnpm"
 ENV PATH="$PNPM_HOME:$PATH"
 RUN corepack enable && corepack prepare pnpm@9.0.0 --activate
 
 WORKDIR /app
-COPY pnpm-lock.yaml pnpm-workspace.yaml package.json turbo.json ./
-COPY apps/worker/ ./apps/worker/
-COPY packages/ ./packages/
+COPY --chown=node:node pnpm-lock.yaml pnpm-workspace.yaml package.json turbo.json ./
+COPY --chown=node:node apps/worker/ ./apps/worker/
+COPY --chown=node:node packages/ ./packages/
 RUN pnpm install --frozen-lockfile
 RUN pnpm --filter `@repo/worker` build
 
 ENV NODE_ENV=production
+USER node
 CMD ["node", "apps/worker/dist/index.js"]
🧰 Tools
🪛 Checkov (3.2.510)

[low] 1-14: Ensure that HEALTHCHECK instructions have been added to container images

(CKV_DOCKER_2)


[low] 1-14: Ensure that a user for the container has been created

(CKV_DOCKER_3)

🪛 Hadolint (2.14.0)

[info] 11-11: Multiple consecutive RUN instructions. Consider consolidation.

(DL3059)

🪛 Trivy (0.69.3)

[error] 1-1: Image user should not be 'root'

Specify at least 1 USER command in Dockerfile with non-root user as argument

Rule: DS-0002

Learn more

(IaC/Dockerfile)


[info] 1-1: No HEALTHCHECK defined

Add HEALTHCHECK instruction in your Dockerfile

Rule: DS-0026

Learn more

(IaC/Dockerfile)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/worker/Dockerfile` around lines 1 - 14, The Dockerfile currently builds
and runs the worker image as root; update it to drop root privileges by
switching to the non-root node user provided by the base image (node:22-slim).
After the build steps and before CMD, add a USER declaration that uses the
existing node user (e.g., USER node or USER 1000) and ensure file ownership is
correct for the build artifacts used by CMD ["node",
"apps/worker/dist/index.js"] (chown or adjust permissions for /app and the dist
files so the non-root user can read/execute). Apply the same change pattern to
the other service Dockerfiles (api, realtime).

116 changes: 116 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: townhall
POSTGRES_PASSWORD: townhall
POSTGRES_DB: townhall
Comment on lines +6 to +8
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

Avoid embedding database credentials directly in committed compose config.

Lines 6-8 and Lines 32/58/82 hardcode credentials (townhall:townhall) in plain text. Even for local defaults, this weakens security posture and encourages credential reuse.

Suggested refactor
   postgres:
     image: postgres:16-alpine
     restart: unless-stopped
     environment:
-      POSTGRES_USER: townhall
-      POSTGRES_PASSWORD: townhall
-      POSTGRES_DB: townhall
+      POSTGRES_USER: ${POSTGRES_USER:-townhall}
+      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-townhall}
+      POSTGRES_DB: ${POSTGRES_DB:-townhall}
@@
-      DATABASE_URL: postgresql://townhall:townhall@postgres:5432/townhall
+      DATABASE_URL: postgresql://${POSTGRES_USER:-townhall}:${POSTGRES_PASSWORD:-townhall}@postgres:5432/${POSTGRES_DB:-townhall}
@@
-      DATABASE_URL: postgresql://townhall:townhall@postgres:5432/townhall
+      DATABASE_URL: postgresql://${POSTGRES_USER:-townhall}:${POSTGRES_PASSWORD:-townhall}@postgres:5432/${POSTGRES_DB:-townhall}
@@
-      DATABASE_URL: postgresql://townhall:townhall@postgres:5432/townhall
+      DATABASE_URL: postgresql://${POSTGRES_USER:-townhall}:${POSTGRES_PASSWORD:-townhall}@postgres:5432/${POSTGRES_DB:-townhall}

Also applies to: 32-33, 58-59, 82-83

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker-compose.yml` around lines 6 - 8, Replace embedded plaintext DB
credentials (POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB) with
environment-variable substitution and/or a .env file; for each occurrence (the
POSTGRES_* entries and any repeats at the other locations) change the values to
use ${POSTGRES_USER:-townhall}, ${POSTGRES_PASSWORD:-townhall},
${POSTGRES_DB:-townhall} or reference variables like ${DB_USER}, ${DB_PASS},
${DB_NAME}, and document defaults in a .env.example; ensure the real .env
(containing secrets) is added to .gitignore and update any service references
that read these vars so they use the same variable names (e.g., postgres service
env block and any consumer services).

ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U townhall"]
interval: 5s
timeout: 3s
retries: 5

redis:
image: redis:7-alpine
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_data:/data
Comment thread
coderabbitai[bot] marked this conversation as resolved.
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5

api:
build:
context: .
dockerfile: apps/api/Dockerfile
restart: unless-stopped
ports:
- "8080:8080"
environment:
NODE_ENV: production
PORT: 8080
DATABASE_URL: postgresql://townhall:townhall@postgres:5432/townhall
REDIS_URL: redis://redis:6379
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:8080}
S3_ENDPOINT: ${S3_ENDPOINT}
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
S3_BUCKET_NAME: ${S3_BUCKET_NAME}
S3_REGION: ${S3_REGION:-auto}
S3_PUBLIC_URL: ${S3_PUBLIC_URL}
RESEND_API_KEY: ${RESEND_API_KEY}
Comment on lines +44 to +52
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Fail fast when required secrets are missing.

At Line 34 (and similarly Lines 36-42, 60-69, 85-92), ${VAR} resolves to empty string when unset. That causes runtime failures later instead of immediate compose-time failure.

Suggested strict env interpolation
-      BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
+      BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?BETTER_AUTH_SECRET is required}
@@
-      S3_ENDPOINT: ${S3_ENDPOINT}
+      S3_ENDPOINT: ${S3_ENDPOINT:?S3_ENDPOINT is required}
-      S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID}
+      S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:?S3_ACCESS_KEY_ID is required}
-      S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
+      S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:?S3_SECRET_ACCESS_KEY is required}
-      S3_BUCKET_NAME: ${S3_BUCKET_NAME}
+      S3_BUCKET_NAME: ${S3_BUCKET_NAME:?S3_BUCKET_NAME is required}
-      S3_PUBLIC_URL: ${S3_PUBLIC_URL}
+      S3_PUBLIC_URL: ${S3_PUBLIC_URL:?S3_PUBLIC_URL is required}
-      RESEND_API_KEY: ${RESEND_API_KEY}
+      RESEND_API_KEY: ${RESEND_API_KEY:?RESEND_API_KEY is required}

Apply the same pattern for realtime and worker.

Also applies to: 60-69, 85-92

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker-compose.yml` around lines 34 - 42, The docker-compose env entries use
permissive interpolation (${VAR}) which yields empty strings when unset; update
each required variable (e.g., BETTER_AUTH_SECRET, NEXT_PUBLIC_API_URL,
S3_ENDPOINT, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_BUCKET_NAME, S3_REGION,
S3_PUBLIC_URL, RESEND_API_KEY) to use strict interpolation so compose fails fast
(use the shell-style required form so missing vars raise an error), and apply
the same change to the corresponding env blocks for the realtime and worker
services referenced in the compose file.

EMAIL_FROM: ${EMAIL_FROM:-Townhall <noreply@team.townhall.chat>}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy

realtime:
build:
context: .
dockerfile: apps/realtime/Dockerfile
restart: unless-stopped
ports:
- "8000:8000"
environment:
NODE_ENV: production
REALTIME_PORT: 8000
DATABASE_URL: postgresql://townhall:townhall@postgres:5432/townhall
REDIS_URL: redis://redis:6379
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:8080}
REALTIME_CORS_ORIGIN: ${REALTIME_CORS_ORIGIN:-http://localhost:3000,http://localhost:3001}
S3_ENDPOINT: ${S3_ENDPOINT}
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
S3_BUCKET_NAME: ${S3_BUCKET_NAME}
S3_REGION: ${S3_REGION:-auto}
S3_PUBLIC_URL: ${S3_PUBLIC_URL}
RESEND_API_KEY: ${RESEND_API_KEY}
EMAIL_FROM: ${EMAIL_FROM:-Townhall <noreply@team.townhall.chat>}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy

worker:
build:
context: .
dockerfile: apps/worker/Dockerfile
restart: unless-stopped
environment:
NODE_ENV: production
DATABASE_URL: postgresql://townhall:townhall@postgres:5432/townhall
REDIS_URL: redis://redis:6379
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:8080}
S3_ENDPOINT: ${S3_ENDPOINT}
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
S3_BUCKET_NAME: ${S3_BUCKET_NAME}
S3_REGION: ${S3_REGION:-auto}
S3_PUBLIC_URL: ${S3_PUBLIC_URL}
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
RESEND_API_KEY: ${RESEND_API_KEY}
EMAIL_FROM: ${EMAIL_FROM:-Townhall <noreply@team.townhall.chat>}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy

volumes:
postgres_data:
redis_data:
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"generate:auth-schema": "pnpm --filter @repo/auth exec npx @better-auth/cli@latest generate --output ../../packages/db/src/generated-schema.ts",
"db:push": "pnpm --filter @repo/db db:push",
"db:studio": "pnpm --filter @repo/db db:studio",
"test": "turbo run test",
"desktop": "pnpm --filter desktop dev",
"prepare": "husky"
},
Expand All @@ -18,7 +19,8 @@
"@types/node": "^25.2.2",
"husky": "^9.1.7",
"turbo": "^2.8.3",
"typescript": "5.9.3"
"typescript": "5.9.3",
"vitest": "^4.1.2"
},
"packageManager": "pnpm@9.0.0",
"engines": {
Expand Down
4 changes: 2 additions & 2 deletions packages/auth/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export const auth = betterAuth({
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 480px; margin: 0 auto; padding: 32px 0;">
<h2 style="margin: 0 0 8px; font-size: 24px; color: #1a1a1a;">Reset Your Password</h2>
<p style="margin: 0 0 24px; color: #555; font-size: 16px; line-height: 1.5;">Click the button below to reset your password.</p>
<a href="${url}" style="display: inline-block; background: #8B6914; color: #fff; padding: 12px 28px; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 16px;">Reset Password</a>
<a href="${url}" style="display: inline-block; background: #994920; color: #fff; padding: 12px 28px; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 16px;">Reset Password</a>
<p style="color: #999; font-size: 13px; margin-top: 24px; line-height: 1.4;">If you didn't request a password reset, you can safely ignore this email.</p>
</div>
`,
Expand Down Expand Up @@ -177,7 +177,7 @@ export const auth = betterAuth({
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 480px; margin: 0 auto; padding: 32px 0;">
<h2 style="margin: 0 0 8px; font-size: 24px; color: #1a1a1a;">Welcome to Townhall</h2>
<p style="margin: 0 0 24px; color: #555; font-size: 16px; line-height: 1.5;">Click the button below to verify your email address and get started.</p>
<a href="${url}" style="display: inline-block; background: #8B6914; color: #fff; padding: 12px 28px; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 16px;">Verify Email</a>
<a href="${url}" style="display: inline-block; background: #994920; color: #fff; padding: 12px 28px; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 16px;">Verify Email</a>
<p style="color: #999; font-size: 13px; margin-top: 24px; line-height: 1.4;">If you didn't create a Townhall account, you can safely ignore this email.</p>
</div>
`,
Expand Down
Loading
Loading