惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
LangChain Blog
Engineering at Meta
Engineering at Meta
S
Securelist
M
MIT News - Artificial intelligence
GbyAI
GbyAI
O
OpenAI News
W
WeLiveSecurity
T
Troy Hunt's Blog
L
LINUX DO - 最新话题
博客园_首页
C
Check Point Blog
Martin Fowler
Martin Fowler
The Last Watchdog
The Last Watchdog
量子位
Cloudbric
Cloudbric
S
SegmentFault 最新的问题
Recent Commits to openclaw:main
Recent Commits to openclaw:main
SecWiki News
SecWiki News
L
Lohrmann on Cybersecurity
Forbes - Security
Forbes - Security
雷峰网
雷峰网
H
Heimdal Security Blog
P
Palo Alto Networks Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Y
Y Combinator Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
博客园 - 聂微东
Hacker News: Ask HN
Hacker News: Ask HN
Hacker News - Newest:
Hacker News - Newest: "LLM"
Help Net Security
Help Net Security
U
Unit 42
N
News and Events Feed by Topic
Hugging Face - Blog
Hugging Face - Blog
A
About on SuperTechFans
Stack Overflow Blog
Stack Overflow Blog
TaoSecurity Blog
TaoSecurity Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - Franky
Jina AI
Jina AI
美团技术团队
L
LINUX DO - 热门话题
F
Fortinet All Blogs
The GitHub Blog
The GitHub Blog
Security Latest
Security Latest
S
Secure Thoughts
Microsoft Azure Blog
Microsoft Azure Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
IT之家
IT之家
P
Privacy International News Feed
博客园 - 司徒正美

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Typescript Monorepo Development using Docker Compose Watch, Turborepo and PNPM
Nathan Cook · 2026-05-07 · via DEV Community

Typescript Monorepo Development using Docker Compose Watch, Turborepo and PNPM

Introduction

While developing web applications using Docker Compose has many positives, like portability and making it easy to add databases and other services like Redis to your environment, it's important to remember that Docker and containers generally were not originally meant to facilitate the sort of immediate-feedback development workflows which web developers expect.

The method of bind-mounting code into a Node image container and creating an anonymous volume for node_modules was always something of a hack, and brings with it little annoyances that can add up to a frustrating experience, like dependency de-sync between host and container, and packages that require building or code generation creating folders locally with root permissions because of the bind mount.

In 2023, Docker announced the GA relase of the Docker Compose Watch feature (Spec, Manual entry). From the announcement,

With containerized application development, there are more steps than Alt+Tab and hitting reload in your browser. Even with caching, rebuilding the image and re-creating the container — especially after waiting on stop and start time — can disrupt focus. We built Docker Compose Watch to smooth away these workflow papercuts. We have learned from many people using our open source Docker Compose project for local development. Now we are natively addressing common workflow friction we observe, like the use case of hot reload for frontend development.

This tutorial will show how to use Turborepo and the PNPM package manager with the Docker Compose Watch feature to create a smooth development experience, and how to resolve some of the difficulties that arise when trying to deal with shared packages that require a build or code-generation step in a monorepo setup.

Here is the Tutorial Repository. To get this project up and running, first run bash scripts/init.sh in the repo directory. Then, you can start the project with docker compose up --wait or tmuxinator start.

Required Packages

Project repo description and goals

This simple project runs a Next.js app which has the Prisma ORM client as a shared package. Prisma was chosen specifically because the client requires code-generation that must be run locally as well as in the container, and setting it up also demonstrates how to configure the environment so Prisma can talk to the Postgresql database from the host as well as when run in the container.

As far as goals, we want to be able to

  • Work locally on source code and have those changes synced with the running container

  • If we make changes to the Prisma schema and rebuild the client, have the client code generation also happen in the container (without needing to rebuild the whole project)

  • Require rebuilding services ONLY when their dependencies change.

Development Dockerfile

First, let's go over the Development Dockerfile. I say "development" Dockerfile because it makes use of Turborepo features that wouldn't make sense when building an image for production.

You can view the full Dockerfile here.

Let's go over what each layer does:

####### Base #######
FROM node:lts-alpine AS base

ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0

ENV TURBO_TELEMETRY_DISABLED=1
ENV NEXT_TELEMETRY_DISABLED=1

RUN apk add --no-cache bash openssl \
  && corepack enable \
  && corepack prepare pnpm@10.33.0 --activate \
  && pnpm add turbo --global

Enter fullscreen mode Exit fullscreen mode

This is our 'base' Node layer. First, it sets some environment variables that are needed so Corepack and PNPM work. Then, it adds some packages to the container (Prisma complains if OpenSSL is not installed) and installs PNPM. Finally, it adds Turborepo globally.

####### Prune #######
FROM base AS prune
WORKDIR /usr/src/app
ARG APP

COPY . .

RUN turbo prune --scope=$APP --docker

Enter fullscreen mode Exit fullscreen mode

This layer copies in the project files and then runs the turborepo prune command with the --docker flag for a specific package in the monorepo, as determined by the $APP argument. $APP will be defined for the service in docker-compose.yml. This setup is useful, since if additional Apps are ever added to the project we can just reuse this Dockerfile and pass the right $APP value for the service in docker-compose.yml.

The directories/files turborepo creates here will be used in subsequent layers. From the Turborepo docs, the prune command will "Generate a partial monorepo for a target package.", and passing the --docker flag will create

  • A folder named json with the pruned workspace's package.json files.
  • A folder named full with the pruned workspace's full source code for the internal packages needed to build the target.
  • A pruned lockfile containing the subset of the original lockfile needed to build the target.
####### Install and Build #######
FROM base AS builder
WORKDIR /usr/src/app
ARG APP

COPY --from=prune /usr/src/app/out/json/ .

RUN \
  --mount=type=cache,id=pnpm,target=/pnpm/store \
    pnpm install --frozen-lockfile

COPY --from=pruner /usr/src/app/out/full/ .

RUN turbo run build --no-cache --filter=${APP}^...

Enter fullscreen mode Exit fullscreen mode

First, this layer copies the workspace's package.json files to the container before running pnpm install --frozen-lockfile. A cache mount is used for the RUN command here. Since only the package.json files are copied, this means that the RUN command will only re-execute on build if workspace dependencies have changed (vs re-running it if ANY source files have changed). Next, the workspace's source files are copied in and packages are built, if necessary.

The syntax of the filter flag (--filter=${APP}^...) here is significant: What this does is cause only the packages that $APP depends on to be built, but not $APP itself. This is what we want, because we don't need to build the Next.js app for local development purposes (we have the dev server), however we DO need Prisma to do it's code generation business, which the Next.js app depends on.

The turbo.json file

Before looking at docker-compose.yml and the Docker Compose Watch setup, let's look at the turbo.json file.

{
  "$schema": "https://turborepo.dev/schema.json",
  "globalEnv": ["PORT", "DATABASE_URL"],
  "tasks": {
    "build": {
      "dependsOn": ["^build", "^db:generate"],
      "inputs": ["$TURBO_DEFAULT$", ".env*"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**", "generated/**"]
    },
    "lint": {
      "dependsOn": ["^lint"]
    },
    "check-types": {
      "dependsOn": ["^check-types"]
    },
    "dev": {
      "dependsOn": ["^db:generate"],
      "cache": false
    },
    "db:generate": {
      "cache": false
    },
    "db:push": {
      "cache": false
    },
    "db:seed": {
      "cache": false
    },
    "db:reset": {
      "cache": false
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

The important configuration here is dependsOn for the build and dev tasks.

Here's the build task:

"build": {
      "dependsOn": ["^build", "^db:generate"],
      "inputs": ["$TURBO_DEFAULT$", ".env*"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**", "generated/**"]
    },

Enter fullscreen mode Exit fullscreen mode

Here, the dependsOn config means that when you build a specific workspace app with turbo run build --filter=AN_APP, turbo repo will run the build script(s) for any packages that "AN_APP" depends on (as defined in their package.json file), as well as running db:generate in any packages with that script (Prisma, in our example), before executing the build script for the specified App.

In other words, this means that when turbo run build --no-cache --filter=${APP}^... is run in the Dockerfile, the Prisma client will also be generated.

And now the dev task:

 "dev": {
      "dependsOn": ["^db:generate"],
      "cache": false
    },

Enter fullscreen mode Exit fullscreen mode

This makes it so Prisma generates its client whenever turbo run dev is called for apps that depend on the shared Prisma package. The reason for doing this for the dev command (in addition to build) will make more sense when we get to setting up Docker Compose Watch for the Next.js service.

The docker-compose.yml file

I'm only going to talk about the develop watch config for the "web" Next.js service here. You can view the full docker-compose.yml file here.

services:
  ...other services
  web:
    command: turbo run dev --filter=web
    develop:
      watch:
        - action: sync
          path: ./apps/web
          target: /usr/src/app/apps/web
          initial_sync: true

        - action: sync
          path: ./packages/database/prisma
          target: /usr/src/app/packages/database/prisma
          initial_sync: true

        - action: restart
          path: ./packages/database/generated

        - action: rebuild
          path: ./apps/web/package.json

        - action: rebuild
          path: ./packages/database/package.json

  ...rest

Enter fullscreen mode Exit fullscreen mode

Let's go over each Watch action:

- action: sync
  path: ./apps/web
  target: /usr/src/app/apps/web
  initial_sync: true

Enter fullscreen mode Exit fullscreen mode

This synchronizes the Nest.js app's source files with those in the container when the compose project starts up and whenever they change. Files/directories we DON'T want to sync are listed in a .dockerignore file.

- action: sync
  path: ./packages/database/prisma
  target: /usr/src/app/packages/database/prisma
  initial_sync: true

Enter fullscreen mode Exit fullscreen mode

This synchronizes the prisma schema file to the container on startup and whenever it changes, which is important for the next action...

- action: restart
  path: ./packages/database/generated

Enter fullscreen mode Exit fullscreen mode

This restarts the 'web' container when the prisma client is generated locally (when the generated client's code changes). In other words, if you make changes to the Prisma schema and then generate the client locally, this will cause the "web" service to restart.

Here's the clever part: Recall that in the turbo.json file, the dev task was configured to also run db:generate whenever it's called (per the app's dependencies). Consequently, when the 'web' service restarts and calls its command (turbo run dev --filter=web), thise will re-generate the Prisma client in the container based on the updated schema.prisma file, which are synchronized in the container by the previous watch action.

- action: rebuild
  path: ./apps/web/package.json

- action: rebuild
  path: ./packages/database/package.json

Enter fullscreen mode Exit fullscreen mode

These actions simply cause the service to rebuild if the package.json files change. This is unavoidable, but fortunately the multi-layer development Dockerfile has been optimized for local development to try and make this as painless as possible.

An example workflow

To follow this example, you'll need to have cloned the tutorial repo, run bash scripts/init.sh, and have the project running in watch mode with docker compose up --wait. To make the changes to code listed below, you will of course also need the project open in your code editor.

With the project running, if you open http://localhost:3000 in your browser you should see:

Localhost

Let's update the Prisma schema as well as the seed script to see those changes reflected in the running service.

First, open /packages/database/prisma/schema.prisma and add a nullable Int column named "age" to the User model. After doing that, the schema file should look like so:

generator client {
  provider = "prisma-client"
  output   = "../generated/prisma"
}

datasource db {
  provider = "postgresql"
}

enum Role {
  USER
  ADMIN
}

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  age Int?
}

Enter fullscreen mode Exit fullscreen mode

Next, open up /packages/database/prisma/seed.ts and add ages for the two Users in the seed data. After making those changes, the file should look like this:

import { PrismaClient, Prisma } from "../generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
import { Pool } from "pg";
import "dotenv/config";

const connectionString = `${process.env.DATABASE_URL}`;

const pool = new Pool({ connectionString });

const adapter = new PrismaPg(pool);

const prisma = new PrismaClient({ adapter });

const userData: Prisma.UserCreateInput[] = [
  {
    name: "Alice",
    email: "alice@prisma.io",
    age: 29,
  },
  {
    name: "Bob",
    email: "bob@prisma.io",
    age: 32,
  },
];

async function main() {
  for (const u of userData) {
    await prisma.user.create({ data: u });
  }
}

main()
  .then(async () => {
    await prisma.$disconnect();
    await pool.end();
  })
  .catch(async (e) => {
    console.error(e);
    await prisma.$disconnect();
    await pool.end();
    process.exit(1);
  });

Enter fullscreen mode Exit fullscreen mode

Finally, we're going re-generate the client locally, update the Postgresql database to match the prisma schema, and then re-seed the database, all without having to stop docker compose or rebuild the service. I created a script entry for the Prisma package which does all that named db:reset, which just runs the following: prisma generate && prisma db push --force-reset && prisma db seed.

To call this, run turbo run db:reset in the project root.

After running that command, you should see the following when you refresh http://localhost:3000 in the browser:

Localhost changed