Docker
Docker configuration for containerized Loop Health services.
Patient Graph API
The Patient Graph API uses a multi-stage Docker build optimized for production:
Dockerfile
Located at apps/patient-graph/Dockerfile:
FROM node:20-alpine AS base
RUN corepack enable && corepack prepare pnpm@latest --activate
FROM base AS deps
WORKDIR /app
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
# Copy package.json files for all needed packages
COPY apps/patient-graph/package.json apps/patient-graph/
COPY packages/core/package.json packages/core/
COPY packages/shared/package.json packages/shared/
COPY packages/hono/package.json packages/hono/
COPY packages/patient-graph/package.json packages/patient-graph/
RUN pnpm install --frozen-lockfile --prod
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm build --filter=@loop/patient-graph-api
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/apps/patient-graph/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]Build Stages
| Stage | Purpose | Size Impact |
|---|---|---|
base | Node.js 20 Alpine + pnpm | ~120MB |
deps | Install production dependencies | Cached layer |
builder | Build TypeScript to JavaScript | Discarded |
runner | Final production image | ~200MB |
Building Locally
# Build the image
docker build -f apps/patient-graph/Dockerfile -t patient-graph-api .
# Run locally
docker run -p 3000:3000 \
-e DATABASE_URL="postgresql://..." \
-e CLERK_ISSUER_URL="https://..." \
patient-graph-api
# Verify
curl http://localhost:3000/healthBest Practices
Layer Caching
The Dockerfile is structured to maximize Docker layer caching:
package.jsonfiles are copied first (rarely change)pnpm installis cached when dependencies don’t change- Source code is copied last (changes frequently)
Security
- Uses
node:20-alpinefor minimal attack surface - Runs as non-root user (Node.js default in Alpine)
- Production dependencies only (
--prodflag) - No source code in final image (only compiled JS)
Environment Variables
Never bake secrets into Docker images. Pass them at runtime:
docker run -p 3000:3000 \
--env-file .env.production \
patient-graph-apiOr use Fly.io secrets:
fly secrets set DATABASE_URL="postgresql://..."Docker Compose (Development)
For local development with multiple services:
version: '3.8'
services:
patient-graph:
build:
context: .
dockerfile: apps/patient-graph/Dockerfile
ports:
- "3000:3000"
env_file:
- .env.local
depends_on:
- db
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: loop
POSTGRES_USER: loop
POSTGRES_PASSWORD: loop
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata: