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
26 changes: 14 additions & 12 deletions apps/api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -40,27 +40,29 @@ COPY apps/api/package.json ./apps/api/
# Production deps only
RUN pnpm install --frozen-lockfile --prod

# Copy compiled output from builder
COPY --from=builder /app/apps/api/dist ./apps/api/dist
COPY --from=builder /app/packages/shared ./packages/shared
COPY --from=builder /app/packages/schema/dist ./packages/schema/dist
# Copy compiled output from builder — owned by `node` at copy time (near-free),
# instead of a separate recursive `chown -R /app` layer.
COPY --chown=node:node --from=builder /app/apps/api/dist ./apps/api/dist
COPY --chown=node:node --from=builder /app/packages/shared ./packages/shared
COPY --chown=node:node --from=builder /app/packages/schema/dist ./packages/schema/dist

# Migrations folder needed at runtime for drizzle
COPY apps/api/drizzle ./apps/api/drizzle
COPY --chown=node:node apps/api/drizzle ./apps/api/drizzle

# Static templates (not emitted by tsc — copy from source)
COPY apps/api/src/templates ./apps/api/dist/templates
COPY --chown=node:node apps/api/src/templates ./apps/api/dist/templates
# Email HTML templates (inliner reads dist/emails/html/*.html at runtime)
COPY apps/api/src/emails/html ./apps/api/dist/emails/html
COPY --chown=node:node apps/api/src/emails/html ./apps/api/dist/emails/html

HEALTHCHECK --interval=10s --timeout=5s --retries=12 --start-period=40s \
CMD node -e "fetch('http://localhost:8080/health').then(r => process.exit(r.ok?0:1)).catch(()=>process.exit(1))"

# Run as the image's built-in unprivileged `node` user (uid 1000). /app is owned
# by root after the COPYs above; hand it (and node's home for any transient
# writes) to `node` so the runtime can write caches/tmp without root. Port 8080
# is >1024, so the non-root bind is fine.
RUN chown -R node:node /app
# Run as the image's built-in unprivileged `node` user (uid 1000). App code and
# build output are COPYed --chown=node:node above; the prod node_modules stays
# root-owned and world-readable, which is all the runtime needs — nothing is
# written under /app at runtime (uploads → object storage, sessions → sealed
# cookies). This drops the ~170s recursive `chown -R /app`. Port 8080 is >1024,
# so the non-root bind is fine.
USER node

EXPOSE 8080
Expand Down
15 changes: 8 additions & 7 deletions apps/api/Dockerfile.collab
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@ COPY apps/api/package.json ./apps/api/

RUN pnpm install --frozen-lockfile --prod

COPY --from=builder /app/apps/api/dist ./apps/api/dist
COPY --from=builder /app/packages/shared ./packages/shared
COPY --from=builder /app/packages/schema/dist ./packages/schema/dist

# Run as the image's built-in unprivileged `node` user (uid 1000). Port 1234 is
# >1024, so binding as non-root is fine.
RUN chown -R node:node /app
COPY --chown=node:node --from=builder /app/apps/api/dist ./apps/api/dist
COPY --chown=node:node --from=builder /app/packages/shared ./packages/shared
COPY --chown=node:node --from=builder /app/packages/schema/dist ./packages/schema/dist

# Run as the image's built-in unprivileged `node` user (uid 1000). Build output is
# COPYed --chown=node:node above; the prod node_modules stays root-owned and
# world-readable (read-only at runtime — nothing is written under /app). This
# drops the recursive `chown -R /app`. Port 1234 is >1024, so non-root binds fine.
USER node

EXPOSE 1234
Expand Down
18 changes: 11 additions & 7 deletions apps/api/Dockerfile.workers
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,20 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
RUN cd apps/api && pnpm exec playwright install --with-deps chromium \
&& chmod -R a+rX /ms-playwright

COPY --from=builder /app/apps/api/dist ./apps/api/dist
COPY --from=builder /app/packages/shared ./packages/shared
COPY --from=builder /app/packages/schema/dist ./packages/schema/dist
COPY --chown=node:node --from=builder /app/apps/api/dist ./apps/api/dist
COPY --chown=node:node --from=builder /app/packages/shared ./packages/shared
COPY --chown=node:node --from=builder /app/packages/schema/dist ./packages/schema/dist
# Email HTML templates (inliner reads dist/emails/html/*.html at runtime)
COPY apps/api/src/emails/html ./apps/api/dist/emails/html
COPY --chown=node:node apps/api/src/emails/html ./apps/api/dist/emails/html

# Drop to the image's built-in unprivileged `node` user (uid 1000). Chromium runs fine
# non-root inside the container's default seccomp profile. /app and the browser cache
# are handed to `node`; chromium also needs a writable HOME for its singleton profile.
RUN chown -R node:node /app /ms-playwright
# non-root inside the container's default seccomp profile. Build output is COPYed
# --chown=node:node above; /ms-playwright is already world-readable/executable via the
# `chmod -R a+rX` at install time above, and Chromium's writable singleton profile lives
# under HOME=/home/node (not the browsers path) — so it needs no ownership change.
# Dropping the recursive `chown -R /app /ms-playwright` avoids rewriting the entire
# Chromium tree into a second image layer (the ~130s layer-export cost). node_modules
# stays root-owned/read-only, which is all the runtime needs.
USER node
ENV HOME=/home/node

Expand Down
19 changes: 11 additions & 8 deletions apps/web/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,18 @@ COPY apps/web/package.json ./apps/web/

RUN pnpm install --frozen-lockfile --prod

# Compiled output + workspace packages
COPY --from=builder /app/apps/web/dist ./apps/web/dist
COPY --from=builder /app/packages/shared ./packages/shared
COPY --from=builder /app/packages/schema/dist ./packages/schema/dist
# Compiled output + workspace packages — owned by `node` at copy time (near-free),
# instead of a separate recursive `chown -R /app` layer.
COPY --chown=node:node --from=builder /app/apps/web/dist ./apps/web/dist
COPY --chown=node:node --from=builder /app/packages/shared ./packages/shared
COPY --chown=node:node --from=builder /app/packages/schema/dist ./packages/schema/dist

# Run as the image's built-in unprivileged `node` user (uid 1000). Hand /app to
# `node` for any runtime cache writes. Port 4321 is >1024, so binding as non-root
# is fine.
RUN chown -R node:node /app
# Run as the image's built-in unprivileged `node` user (uid 1000). Build output is
# COPYed --chown=node:node above; the prod node_modules stays root-owned and
# world-readable. Nothing is written under /app at runtime — sessions are sealed
# iron-session cookies, not Astro's filesystem session store — so read-only
# node_modules is all the runtime needs. This drops the ~124s recursive
# `chown -R /app`. Port 4321 is >1024, so binding as non-root is fine.
USER node

EXPOSE 4321
Expand Down
9 changes: 8 additions & 1 deletion scripts/self-host-init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -126,5 +126,12 @@ echo ""
echo "Next:"
echo " docker compose up -d --build # (public repo: docker-compose.yml is the self-host stack)"
echo ""
echo "Then open ${WEB_URL} and sign up at ${WEB_URL}/auth/local"
echo " [!] First build compiles the api, web, collab and workers images (the workers"
echo " image also downloads Chromium), so a cold first build takes several minutes"
echo " - much faster on later builds. Postgres and Redis go healthy in seconds,"
echo " then the terminal will look idle for a few minutes while the images build."
echo " That is normal - it is NOT a hang, so don't Ctrl-C. Watch progress with:"
echo " docker compose logs -f"
echo ""
echo "When ready, open ${WEB_URL} and sign up at ${WEB_URL}/auth/local"
echo "Connect an MCP client (Claude/Cursor) to: ${API_URL}/mcp"
Loading