<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Nathan Cook</title>
    <description>The latest articles on Forem by Nathan Cook (@moofoo).</description>
    <link>https://forem.com/moofoo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F542595%2F5d984e24-719c-4732-b93f-31bef7b8b0d9.png</url>
      <title>Forem: Nathan Cook</title>
      <link>https://forem.com/moofoo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/moofoo"/>
    <language>en</language>
    <item>
      <title>Typescript Monorepo Development using Docker Compose Watch, Turborepo and PNPM</title>
      <dc:creator>Nathan Cook</dc:creator>
      <pubDate>Thu, 07 May 2026 00:13:51 +0000</pubDate>
      <link>https://forem.com/moofoo/typescript-monorepo-development-using-docker-compose-watch-turborepo-and-pnpm-3hep</link>
      <guid>https://forem.com/moofoo/typescript-monorepo-development-using-docker-compose-watch-turborepo-and-pnpm-3hep</guid>
      <description>&lt;p&gt;This tutorial will show how to use &lt;a href="https://turborepo.dev/" rel="noopener noreferrer"&gt;Turborepo&lt;/a&gt; and the &lt;a href="https://pnpm.io/" rel="noopener noreferrer"&gt;PNPM package manager&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;(&lt;a href="https://github.com/moofoo/watch-turbo-tutorial" rel="noopener noreferrer"&gt;Tutorial repo&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;While developing web applications using &lt;a href="https://docs.docker.com/compose/" rel="noopener noreferrer"&gt;Docker Compose&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;The method of bind-mounting code into a Node image container and creating an anonymous volume for &lt;code&gt;node_modules&lt;/code&gt; 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 on the host with root permissions because of the bind mount.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.docker.com/blog/announcing-docker-compose-watch-ga-release/" rel="noopener noreferrer"&gt;In 2023&lt;/a&gt;, Docker announced the GA release of the Docker Compose Watch feature (&lt;a href="https://docs.docker.com/reference/compose-file/develop/" rel="noopener noreferrer"&gt;Spec&lt;/a&gt;, &lt;a href="https://docs.docker.com/compose/how-tos/file-watch/" rel="noopener noreferrer"&gt;Manual entry&lt;/a&gt;). From the announcement,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sounds great! However, using the base functionality Compose Watch offers to wrangle complex monorepo setups can get quite cumbersome and frustrating to implement, especially when you have dependencies between apps and shared packages that require build or codegen steps. &lt;/p&gt;

&lt;p&gt;The whole  conceit here is to try and make local dev with Docker Compose easier and reduce friction, and as cool as the Compose Watch functionality is it is still a general tool and not capable, on its own, to handle some scenarios elegantly.&lt;/p&gt;

&lt;p&gt;That's where Turborepo and PNPM workspaces come in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Required Packages for Tutorial repo
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You should have at least Node version &amp;gt; 20 installed (I recommend just using the LTS version)&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://pnpm.io/installation" rel="noopener noreferrer"&gt;PNPM package manager&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;It's not required, but I would recommend &lt;a href="https://turborepo.dev/docs/getting-started/installation#global-installation" rel="noopener noreferrer"&gt;installing Turborepo globally&lt;/a&gt; (&lt;code&gt;pnpm add turbo --global&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tutorial repo description and goals
&lt;/h2&gt;

&lt;p&gt;Here is the &lt;a href="https://github.com/moofoo/watch-turbo-tutorial" rel="noopener noreferrer"&gt;Tutorial Repository&lt;/a&gt;. To get this project up and running, first run &lt;a href="https://github.com/moofoo/watch-turbo-tutorial/blob/main/scripts/init.sh" rel="noopener noreferrer"&gt;&lt;code&gt;bash scripts/init.sh&lt;/code&gt;&lt;/a&gt; in the repo directory. Then, you can start the project with &lt;code&gt;docker compose up --wait&lt;/code&gt; or &lt;a href="https://dev.to/moofoo/docker-basics-using-tmux-and-tmuxinator-for-a-better-docker-compose-experience-43ma"&gt;&lt;code&gt;tmuxinator start&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This simple project runs a &lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt; app which has the &lt;a href="https://www.prisma.io/orm" rel="noopener noreferrer"&gt;Prisma ORM client&lt;/a&gt; as a shared package. Prisma was chosen specifically because the client requires &lt;a href="https://www.prisma.io/docs/cli/generate" rel="noopener noreferrer"&gt;code-generation&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;As far as goals, we want to be able to&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Work locally on source code and have those changes synced with the running container&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If we make changes to the &lt;a href="https://www.prisma.io/docs/orm/prisma-schema/overview" rel="noopener noreferrer"&gt;Prisma schema&lt;/a&gt; and rebuild the client, have the client code generation also happen in the container (without needing to rebuild the whole project)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Require rebuilding services ONLY when their dependencies change.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Development Dockerfile
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;You can view the full Dockerfile &lt;a href="https://github.com/moofoo/watch-turbo-tutorial/blob/main/Dockerfile" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let's go over what each layer does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;####### Base #######&lt;/span&gt;
&lt;span class="s"&gt;FROM node:lts-alpine AS base&lt;/span&gt;

&lt;span class="s"&gt;ENV PNPM_HOME="/pnpm"&lt;/span&gt;
&lt;span class="s"&gt;ENV PATH="$PNPM_HOME:$PATH"&lt;/span&gt;
&lt;span class="s"&gt;ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0&lt;/span&gt;

&lt;span class="s"&gt;ENV TURBO_TELEMETRY_DISABLED=1&lt;/span&gt;
&lt;span class="s"&gt;ENV NEXT_TELEMETRY_DISABLED=1&lt;/span&gt;

&lt;span class="s"&gt;RUN apk add --no-cache bash openssl \&lt;/span&gt;
  &lt;span class="s"&gt;&amp;amp;&amp;amp; corepack enable \&lt;/span&gt;
  &lt;span class="s"&gt;&amp;amp;&amp;amp; corepack prepare pnpm@10.33.0 --activate \&lt;/span&gt;
  &lt;span class="s"&gt;&amp;amp;&amp;amp; pnpm add turbo --global&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;####### Prune #######&lt;/span&gt;
&lt;span class="s"&gt;FROM base AS prune&lt;/span&gt;
&lt;span class="s"&gt;WORKDIR /usr/src/app&lt;/span&gt;
&lt;span class="s"&gt;ARG APP&lt;/span&gt;

&lt;span class="s"&gt;COPY . .&lt;/span&gt;

&lt;span class="s"&gt;RUN turbo prune --scope=$APP --docker&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This layer copies in the project files and then runs the turborepo &lt;a href="https://turborepo.dev/docs/reference/prune" rel="noopener noreferrer"&gt;prune&lt;/a&gt; command with the &lt;a href="https://turborepo.dev/docs/guides/tools/docker" rel="noopener noreferrer"&gt;--docker flag&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;--docker&lt;/code&gt; flag will create&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A folder named json with the pruned workspace's package.json files.&lt;/li&gt;
&lt;li&gt;A folder named full with the pruned workspace's full source code for the internal packages needed to build the target.&lt;/li&gt;
&lt;li&gt;A pruned lockfile containing the subset of the original lockfile needed to build the target.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;####### Install and Build #######&lt;/span&gt;
&lt;span class="s"&gt;FROM base AS builder&lt;/span&gt;
&lt;span class="s"&gt;WORKDIR /usr/src/app&lt;/span&gt;
&lt;span class="s"&gt;ARG APP&lt;/span&gt;

&lt;span class="s"&gt;COPY --from=prune /usr/src/app/out/json/ .&lt;/span&gt;

&lt;span class="s"&gt;RUN \&lt;/span&gt;
  &lt;span class="s"&gt;--mount=type=cache,id=pnpm,target=/pnpm/store \&lt;/span&gt;
    &lt;span class="s"&gt;pnpm install --frozen-lockfile&lt;/span&gt;

&lt;span class="s"&gt;COPY --from=prune /usr/src/app/out/full/ .&lt;/span&gt;

&lt;span class="s"&gt;RUN turbo run build --no-cache --filter=${APP}^...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, this layer copies the workspace's package.json files to the container before running &lt;code&gt;pnpm install --frozen-lockfile&lt;/code&gt;. A &lt;a href="https://docs.docker.com/build/cache/optimize/" rel="noopener noreferrer"&gt;cache mount&lt;/a&gt; is used for the RUN command here. Since only the package.json files are copied, this means that the &lt;code&gt;RUN&lt;/code&gt; 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. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The syntax of the filter flag (&lt;code&gt;--filter=${APP}^...&lt;/code&gt;) here is significant:&lt;/strong&gt; 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 its code generation business, which the Next.js app depends on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The turbo.json file
&lt;/h2&gt;

&lt;p&gt;Before looking at docker-compose.yml and the Docker Compose Watch setup, let's look at the &lt;a href="https://github.com/moofoo/watch-turbo-tutorial/blob/main/turbo.json" rel="noopener noreferrer"&gt;turbo.json file&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://turborepo.dev/schema.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"globalEnv"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"PORT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DATABASE_URL"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tasks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"^build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^db:generate"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"$TURBO_DEFAULT$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".env*"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"outputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;".next/**"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"!.next/cache/**"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/**"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"generated/**"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"^lint"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"check-types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"^check-types"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"^db:generate"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cache"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"db:generate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cache"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"db:push"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cache"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"db:seed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cache"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"db:reset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cache"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important configuration here is &lt;code&gt;dependsOn&lt;/code&gt; for the &lt;code&gt;build&lt;/code&gt; and &lt;code&gt;dev&lt;/code&gt; tasks.&lt;/p&gt;

&lt;p&gt;Here's the build task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"^build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^db:generate"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"$TURBO_DEFAULT$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".env*"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"outputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;".next/**"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"!.next/cache/**"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/**"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"generated/**"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, the &lt;code&gt;dependsOn&lt;/code&gt; config means that when you build a specific workspace app with &lt;code&gt;turbo run build --filter=AN_APP&lt;/code&gt;, 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 &lt;code&gt;db:generate&lt;/code&gt; in any packages with that script (Prisma, in our example), before executing the build script for the specified App.&lt;/p&gt;

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

&lt;p&gt;And now the dev task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"^db:generate"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cache"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes it so Prisma generates its client whenever &lt;code&gt;turbo run dev&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The docker-compose.yml file
&lt;/h2&gt;

&lt;p&gt;I'm only going to talk about the &lt;code&gt;develop watch&lt;/code&gt; config for the "web" Next.js service here. You can view the full docker-compose.yml file &lt;a href="https://github.com/moofoo/watch-turbo-tutorial/blob/main/docker-compose.yml" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;...other services&lt;/span&gt;
  &lt;span class="s"&gt;web&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;turbo run dev --filter=web&lt;/span&gt;
    &lt;span class="na"&gt;develop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;watch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sync&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./apps/web&lt;/span&gt;
          &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/src/app/apps/web&lt;/span&gt;
          &lt;span class="na"&gt;initial_sync&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sync&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./packages/database/prisma&lt;/span&gt;
          &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/src/app/packages/database/prisma&lt;/span&gt;
          &lt;span class="na"&gt;initial_sync&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;restart&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./packages/database/generated&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rebuild&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./apps/web/package.json&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rebuild&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./packages/database/package.json&lt;/span&gt;

  &lt;span class="s"&gt;...rest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's go over each Watch action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sync&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./apps/web&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/src/app/apps/web&lt;/span&gt;
  &lt;span class="na"&gt;initial_sync&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This synchronizes the Nextjs 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 &lt;a href="https://github.com/moofoo/watch-turbo-tutorial/blob/main/apps/web/.dockerignore" rel="noopener noreferrer"&gt;.dockerignore file&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sync&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./packages/database/prisma&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/src/app/packages/database/prisma&lt;/span&gt;
  &lt;span class="na"&gt;initial_sync&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This synchronizes the prisma schema file to the container on startup and whenever it changes, which is important for the next action...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;restart&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./packages/database/generated&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rebuild&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./apps/web/package.json&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rebuild&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./packages/database/package.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  An example workflow
&lt;/h2&gt;

&lt;p&gt;To follow this example, you'll need to have cloned the &lt;a href="https://github.com/moofoo/watch-turbo-tutorial" rel="noopener noreferrer"&gt;tutorial repo&lt;/a&gt;, run &lt;code&gt;bash scripts/init.sh&lt;/code&gt;, and have the project running in watch mode with &lt;code&gt;docker compose up --wait&lt;/code&gt;. To make the changes to code listed below, you will of course also need the project open in your code editor.&lt;/p&gt;

&lt;p&gt;With the project running, if you open &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt; in your browser you should see:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft87teffr7x9b0u51r55c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft87teffr7x9b0u51r55c.png" alt="Localhost" width="780" height="776"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's update the Prisma schema as well as the seed script to see those changes reflected in the running service.&lt;/p&gt;

&lt;p&gt;First, open &lt;a href="https://github.com/moofoo/watch-turbo-tutorial/blob/main/packages/database/prisma/schema.prisma" rel="noopener noreferrer"&gt;&lt;code&gt;/packages/database/prisma/schema.prisma&lt;/code&gt;&lt;/a&gt; and add a nullable Int column named "age" to the User model. After doing that, the schema file should look like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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?
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, open up &lt;a href="https://github.com/moofoo/watch-turbo-tutorial/blob/main/packages/database/prisma/seed.ts" rel="noopener noreferrer"&gt;&lt;code&gt;/packages/database/prisma/seed.ts&lt;/code&gt;&lt;/a&gt; and add ages for the two Users in the seed data. After making those changes, the file should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PrismaClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Prisma&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../generated/prisma/client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PrismaPg&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@prisma/adapter-pg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Pool&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dotenv/config&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;connectionString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;connectionString&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PrismaPg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PrismaClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UserCreateInput&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Alice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;alice@prisma.io&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bob&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bob@prisma.io&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;userData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;code&gt;db:reset&lt;/code&gt;, which just runs the following: &lt;code&gt;prisma generate &amp;amp;&amp;amp; prisma db push --force-reset &amp;amp;&amp;amp; prisma db seed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To call this, run &lt;code&gt;turbo run db:reset&lt;/code&gt; in the project root.&lt;/p&gt;

&lt;p&gt;After running that command, you should see the following when you refresh &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt; in the browser:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faicmkhalmqj10koqjcuc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faicmkhalmqj10koqjcuc.png" alt="Localhost changed" width="780" height="776"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>typescript</category>
      <category>turborepo</category>
      <category>pnpm</category>
    </item>
    <item>
      <title>Using TMUX and tmuxinator for a better docker compose experience</title>
      <dc:creator>Nathan Cook</dc:creator>
      <pubDate>Sat, 02 May 2026 04:54:00 +0000</pubDate>
      <link>https://forem.com/moofoo/docker-basics-using-tmux-and-tmuxinator-for-a-better-docker-compose-experience-43ma</link>
      <guid>https://forem.com/moofoo/docker-basics-using-tmux-and-tmuxinator-for-a-better-docker-compose-experience-43ma</guid>
      <description>&lt;p&gt;This tutorial provides a practical introduction to &lt;a href="https://github.com/tmux/tmux/wiki" rel="noopener noreferrer"&gt;TMUX&lt;/a&gt; and how you can use &lt;a href="https://github.com/tmuxinator/tmuxinator" rel="noopener noreferrer"&gt;tmuxinator&lt;/a&gt; to easily set up terminal dashboards with logs and shell sessions for your docker compose projects.&lt;/p&gt;

&lt;p&gt;After a (very) brief overview of basic TMUX commands, we'll look at using tmuxinator to more easily configure TMUX with &lt;a href="https://en.wikipedia.org/wiki/YAML" rel="noopener noreferrer"&gt;YAML&lt;/a&gt; configuration files, and at the end we'll go over how to install and use TMUX plugins.&lt;/p&gt;

&lt;p&gt;This tutorial was written for Linux or WSL users (I'm running Ubuntu).&lt;/p&gt;

&lt;p&gt;(&lt;a href="https://github.com/moofoo/docker-tmux-tutorial" rel="noopener noreferrer"&gt;Example repo&lt;/a&gt;)&lt;/p&gt;

&lt;h3&gt;
  
  
  Article Index
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The Pitch&lt;/li&gt;
&lt;li&gt;Install Packages&lt;/li&gt;
&lt;li&gt;Basic TMUX Commands&lt;/li&gt;
&lt;li&gt;Configuring TMUX (~/.tmux.config)&lt;/li&gt;
&lt;li&gt;Tmuxinator (.tmuxinator.yml)&lt;/li&gt;
&lt;li&gt;TMUX plugins&lt;/li&gt;
&lt;li&gt;Resources and Links&lt;/li&gt;
&lt;li&gt;Extra: Issues with Docker Compose 'Watch'&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;u&gt;The Pitch&lt;/u&gt; &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Instead of looking at a concantenated dump of service logs when you start a docker compose project, and needing to open separate terminals to run commands like &lt;code&gt;docker compose logs&lt;/code&gt; and &lt;code&gt;docker compose exec&lt;/code&gt;&lt;/p&gt;



&lt;p&gt;Wouldn't it be nicer to see a dashboard with panes for each  service's logs, with the ability to configure additional windows with separate panes for service SHELL sessions and whatever else:&lt;/p&gt;



&lt;p&gt;Fortunately, it's fairly easy to set up such dashboards with TMUX and tmuxinator.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;u&gt;Install necessary packages&lt;/u&gt; &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;TMUX&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt install tmux&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;tmuxinator&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;To install the latest version of tmuxinator:

&lt;ul&gt;
&lt;li&gt;Use &lt;a href="https://brew.sh/" rel="noopener noreferrer"&gt;Homebrew&lt;/a&gt;: &lt;code&gt;brew install tmuxinator&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Or use &lt;a href="https://rubygems.org/" rel="noopener noreferrer"&gt;RubyGems&lt;/a&gt;: &lt;code&gt;gem install tmuxinator&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;To install a usually slightly-out-of-date version of tmuxinator (which is probably just fine for this tutorial):

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt install tmuxinator&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;u&gt;TMUX command basics&lt;/u&gt; &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;As the end-goal here is to show how to use tmuxinator .yaml configs to create bespoke dashboards for docker compose projects, I'm only going to go over the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The TMUX prefix combo + keybinding system&lt;/li&gt;
&lt;li&gt;How to show the list of TMUX keybindings within TMUX&lt;/li&gt;
&lt;li&gt;How to detach from a TMUX session (leaving the session running)&lt;/li&gt;
&lt;li&gt;How to kill a TMUX session&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;The first thing to know about TMUX is that the standard commands all follow a "prefix" key combination.&lt;/p&gt;

&lt;p&gt;The default prefix is &lt;code&gt;Ctrl + b&lt;/code&gt;, but when we get to configuring TMUX with the &lt;code&gt;~/.tmux.config&lt;/code&gt; file, we'll be changing this to the more convenient combo 'Ctrl + a'.&lt;/p&gt;

&lt;p&gt;If you want to follow along for the rest of this section, run &lt;code&gt;tmux&lt;/code&gt; in your terminal to start a session.&lt;/p&gt;

&lt;h4&gt;
  
  
  How to show the list of TMUX keybindings
&lt;/h4&gt;

&lt;p&gt;Enter the prefix &lt;code&gt;Ctrl + b&lt;/code&gt;, and then press &lt;code&gt;?&lt;/code&gt; (shift key is necessary) to see the list of keybindings.&lt;/p&gt;

&lt;p&gt;Press &lt;code&gt;ESC&lt;/code&gt; to close the list.&lt;/p&gt;

&lt;h4&gt;
  
  
  How to detach from a TMUX session
&lt;/h4&gt;

&lt;p&gt;Enter the prefix &lt;code&gt;Ctrl + b&lt;/code&gt;, and then enter &lt;code&gt;d&lt;/code&gt;. This will detach the current TMUX session and return you to the command line.&lt;/p&gt;

&lt;p&gt;To see a list of running TMUX sessions (from the command line), enter &lt;code&gt;tmux ls&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To stop the currently running TMUX session, type &lt;code&gt;tmux kill-session&lt;/code&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  How to open the 'command prompt' in TMUX and kill the current session and exit TMUX
&lt;/h4&gt;

&lt;p&gt;Back in TMUX, enter the prefix &lt;code&gt;Ctrl + b&lt;/code&gt; and then &lt;code&gt;:&lt;/code&gt; (shift key necessary).&lt;/p&gt;

&lt;p&gt;This opens a command prompt at the bottom of the screen, where you can run TMUX cli commands (like 'kill-session', for example).&lt;/p&gt;

&lt;p&gt;Pressing 'ESC" will close the command prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;u&gt;Configuring TMUX with the ~/.tmux.config file&lt;/u&gt; &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Assuming you don't already have a tmux config file, create a file named &lt;code&gt;.tmux.config&lt;/code&gt; in your home directory and paste the following into it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Turn on mouse input&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; mouse on

&lt;span class="c"&gt;# Increase the scrollback limit from 2000 to 10000&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; history-limit 10000

&lt;span class="c"&gt;# Change TMUX prefix key combo from 'Ctrl + b' to 'Ctrl + a'&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; prefix C-a
unbind C-b
&lt;span class="nb"&gt;bind &lt;/span&gt;C-a send-prefix

&lt;span class="c"&gt;# Add keybinding to change the currently focused pane with Alt + direction key&lt;/span&gt;
&lt;span class="nb"&gt;bind&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; M-Left &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="nt"&gt;-pane&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt;
&lt;span class="nb"&gt;bind&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; M-Right &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="nt"&gt;-pane&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt;
&lt;span class="nb"&gt;bind&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; M-Up &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="nt"&gt;-pane&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt;
&lt;span class="nb"&gt;bind&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; M-Down &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="nt"&gt;-pane&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt;

&lt;span class="c"&gt;# Add keybinding to select windows with 'Alt + Shift + Right' (next window) or 'Alt + Shift + Left' (previous window).&lt;/span&gt;
&lt;span class="nb"&gt;bind&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; S-M-Right &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="nt"&gt;-window&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt;
&lt;span class="nb"&gt;bind&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; S-M-Left &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="nt"&gt;-window&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt;

&lt;span class="c"&gt;# Add Keybinding to kill the current tmux session and exit tmux with 'Ctrl + Shift + x'&lt;/span&gt;
&lt;span class="nb"&gt;bind&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; C-S-x kill-session

&lt;span class="c"&gt;# Add keybinding to clear focused pane (that is, the whole scrollback history) with 'Ctrl + Shift + k'&lt;/span&gt;
&lt;span class="nb"&gt;bind&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; C-S-k send-keys &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt; clear-history

&lt;span class="c"&gt;# Minor futzing around with the layout&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; status-position bottom
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; status-justify left

&lt;span class="c"&gt;# Set what appears in the lower left&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; status-left &lt;span class="s1"&gt;''&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; status-left-length 15

&lt;span class="c"&gt;# Set what appears in the lower right&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; status-right &lt;span class="s1"&gt;'%Y-%m-%d %H:%M '&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; status-right-length 50

&lt;span class="c"&gt;# Give windows reasonable titles&lt;/span&gt;
setw &lt;span class="nt"&gt;-g&lt;/span&gt; window-status-current-format &lt;span class="s1"&gt;' #I #W #F '&lt;/span&gt;

&lt;span class="c"&gt;# Give panes reasonable titles&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; pane-border-status top
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; pane-border-format &lt;span class="s1"&gt;'#[bold]#{pane_title}#[default]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Mouse mode* is turned on&lt;/li&gt;
&lt;li&gt;Scrollback (history) limit is increased&lt;/li&gt;
&lt;li&gt;The TMUX prefix is changed from &lt;code&gt;Ctrl + b&lt;/code&gt; to &lt;code&gt;Ctrl + a&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;(you do not need to enter prefix before the following shortcuts):&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Alt + Direction Key -- changes panes&lt;/li&gt;
&lt;li&gt;Alt + Shift + Left/Right Key -- changes windows&lt;/li&gt;
&lt;li&gt;Ctrl + Shift + x -- kills the current session, exiting TMUX&lt;/li&gt;
&lt;li&gt;Ctrl + Shift + k -- Clears the currently active pane (as well as scrollback history)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;* Mouse mode gives you&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mouse-wheel vertical scrolling&lt;/li&gt;
&lt;li&gt;Select Panes by clicking on them&lt;/li&gt;
&lt;li&gt;Resize Panes by click-dragging their borders&lt;/li&gt;
&lt;li&gt;Select Windows by clicking their name in the bottom left of the status bar&lt;/li&gt;
&lt;li&gt;A right-click menu with some common pane actions.&lt;/li&gt;
&lt;li&gt;You can also copy text to the clipboard by click-dragging to select, but it's a little janky. (I would recommend looking up "Copy Mode")&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;u&gt;Configuring TMUX in .yaml with tmuxinator&lt;/u&gt; &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;For the remainder of this article I will be referring to this example repo: &lt;a href="https://github.com/moofoo/docker-tmux-tutorial" rel="noopener noreferrer"&gt;docker-tmux-tutorial&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you clone the repo and CD into the directory like so&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git clone https://github.com/moofoo/docker-tmux-tutorial.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;docker-tmux-tutorial
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the following files/directories:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;|- apps
|  |- web
|  |- api
|- db
|  |- 01_schema.sql
|  |- 02_data.sql
|- .tmuxinator.watch.yml
|- .tmuxinator.yml
|- docker-compose.watch.yml
|- docker-compose.yml
|- Dockerfile
|- README.md
|- tmux.config.example
|- tmux.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you then run (in the repo directory)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;tmuxinator start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the docker compose project will build (if it needs to) and TMUX will open showing windows and panes per the &lt;code&gt;./tmuxinator.yml&lt;/code&gt; config file, like so (your colors/fonts/font size will differ):&lt;/p&gt;



&lt;p&gt;Let's look at &lt;code&gt;./tmuxinator.yml&lt;/code&gt; now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ./.tmuxinator.yml&lt;/span&gt;

&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tmuxinator_tut&lt;/span&gt;
&lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./&lt;/span&gt;

&lt;span class="na"&gt;on_project_start&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker compose up --wait -d&lt;/span&gt;

&lt;span class="na"&gt;on_project_exit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker compose stop&lt;/span&gt;

&lt;span class="na"&gt;windows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;logs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;panes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose logs --since=1s -f web&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose logs --since=1s -f api&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose logs --since=15s -f db&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose logs --since=15s -f redis&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;panes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose exec -w /usr/src/app web sh&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose exec -w /usr/src/app api sh&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;psql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose exec db psql -U postgres -d tutorial_db&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;nestjs_repl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose exec -w /usr/src/app api npm run repl&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;home&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;layout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main-vertical&lt;/span&gt;
      &lt;span class="na"&gt;panes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;home&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clear&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Going over each section of this config file from the top:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tmuxinator_tut&lt;/span&gt;
&lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fields above set the tmux session name and tells tmuxinator to run commands in the current directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on_project_start&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker compose up --wait -d&lt;/span&gt;

&lt;span class="na"&gt;on_project_exit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker compose stop&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "on_project_start" config tells tmuxinator to run &lt;a href="https://docs.docker.com/reference/cli/docker/compose/up/" rel="noopener noreferrer"&gt;&lt;code&gt;docker compose up&lt;/code&gt;&lt;/a&gt; in detached mode when it starts.&lt;/p&gt;

&lt;p&gt;The "on project_exit" config tells it to run &lt;a href="https://docs.docker.com/reference/cli/docker/compose/stop/" rel="noopener noreferrer"&gt;&lt;code&gt;docker compose stop&lt;/code&gt; &lt;/a&gt; &lt;strong&gt;&lt;em&gt;when the session ends&lt;/em&gt;&lt;/strong&gt;. (In other words, if you enter the &lt;code&gt;"Ctrl + Shift + x"&lt;/code&gt; shortcut combo we previously configured in &lt;code&gt;~/.tmux.config&lt;/code&gt;, it will end the TMUX session and stop the docker compose project).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;windows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="s"&gt;...rest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;windows:&lt;/code&gt; option starts the block where you define the windows and panes you want in the TMUX session.&lt;/p&gt;

&lt;p&gt;Here's the first window from the example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;logs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;panes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose logs --since=1s -f web&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose logs --since=1s -f api&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose logs --since=15s -f db&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose logs --since=15s -f redis&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This window definition creates a window named "logs", which has four panes showing the result of running &lt;a href="https://docs.docker.com/reference/cli/docker/compose/logs/" rel="noopener noreferrer"&gt;&lt;code&gt;docker compose logs&lt;/code&gt;&lt;/a&gt; for the web, api, db, and redis services.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;panes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose exec -w /usr/src/app web sh&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose exec -w /usr/src/app api sh&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;psql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose exec db psql -U postgres -d tutorial_db&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;nestjs_repl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose exec -w /usr/src/app api npm run repl&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This window definition creates a window named "shell", with the following panes making use of &lt;a href="https://docs.docker.com/reference/cli/docker/compose/exec/" rel="noopener noreferrer"&gt;&lt;code&gt;docker compose exec&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;"web": Opens a shell to the working_dir of the "web" service.&lt;/li&gt;
&lt;li&gt;"api": Opens a shell to the working_dir of the "api" service.&lt;/li&gt;
&lt;li&gt;"psql": Starts a &lt;a href="https://www.postgresql.org/docs/current/app-psql.html" rel="noopener noreferrer"&gt;PSQL&lt;/a&gt; terminal session in the "db" service.&lt;/li&gt;
&lt;li&gt;"nestjs_repl": Starts the &lt;a href="https://docs.nestjs.com/recipes/repl" rel="noopener noreferrer"&gt;NestJS REPL&lt;/a&gt; for the NestJS server running in the "api" service.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;home&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;layout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main-vertical&lt;/span&gt;
    &lt;span class="na"&gt;panes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;home&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clear&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just for convenience, this window definition opens your command line on the current (actual) directory.&lt;/p&gt;

&lt;p&gt;If you want to see how this dashboard would be created with TMUX commands, check out the &lt;a href="https://github.com/moofoo/docker-tmux-tutorial/blob/main/tmux.sh" rel="noopener noreferrer"&gt;tmux.sh script&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;u&gt;Extending TMUX with the Tmux Plugin Manager&lt;/u&gt; &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/tmux-plugins/tpm" rel="noopener noreferrer"&gt;TMUX Plugin Manager&lt;/a&gt; provides a (relatively) painless way to extend TMUX functionality.&lt;/p&gt;

&lt;p&gt;The following instructions will walk you through setting up TPM and installing/configuring the &lt;a href="https://github.com/tmux-plugins/tmux-prefix-highlight" rel="noopener noreferrer"&gt;tmux-prefix-highlight&lt;/a&gt; plugin, which simply lets you display an indicator after you press the TMUX prefix combo.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Install the Tmux Plugin Manager
&lt;/h4&gt;

&lt;p&gt;Run git clone as below to create the necessary files in your $HOME directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2. Edit your ~/.tmux.config file to use the plugin
&lt;/h4&gt;

&lt;p&gt;At the top of the &lt;code&gt;~/.tmux.config&lt;/code&gt; before anything else, add the line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @plugin &lt;span class="s1"&gt;'tmux-plugins/tmux-prefix-highlight'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(follow the above pattern for all plugins)&lt;/p&gt;

&lt;p&gt;Then, at the very bottom of &lt;code&gt;~/.tmux.config&lt;/code&gt; (after everything else), add the line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;run &lt;span class="s1"&gt;'~/.tmux/plugins/tpm/tpm'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2. Configure/use the plugin
&lt;/h4&gt;

&lt;p&gt;This plugin gives TMUX a new template string, &lt;code&gt;#{prefix_highlight}&lt;/code&gt;. Assuming you've been following this tutorial, change the following line in &lt;code&gt;~/.tmux.config&lt;/code&gt; (35 for me) from&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; status-left &lt;span class="s1"&gt;''&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; status-left &lt;span class="s1"&gt;'#{prefix_highlight}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also add the following lines anywhere before the first and last lines in the file to also show an indicator when you're in "Copy mode":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Tmux prefix highlight settings&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @prefix_highlight_show_copy_mode &lt;span class="s1"&gt;'on'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After making all the changes listed above, your &lt;code&gt;~/.tmux.config&lt;/code&gt; file should look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install Plugins&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @plugin &lt;span class="s1"&gt;'tmux-plugins/tmux-prefix-highlight'&lt;/span&gt;

&lt;span class="c"&gt;# Tmux prefix highlight settings&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @prefix_highlight_show_copy_mode &lt;span class="s1"&gt;'on'&lt;/span&gt;

&lt;span class="c"&gt;# Turn on mouse input&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; mouse on

&lt;span class="c"&gt;# Change TMUX prefix key combo from Ctrl-b to Ctrl-a&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; prefix C-a
unbind C-b
&lt;span class="nb"&gt;bind &lt;/span&gt;C-a send-prefix

&lt;span class="c"&gt;# Add keybinding to change the focused pane with Alt + direction key&lt;/span&gt;
&lt;span class="nb"&gt;bind&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; M-Left &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="nt"&gt;-pane&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt;
&lt;span class="nb"&gt;bind&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; M-Right &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="nt"&gt;-pane&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt;
&lt;span class="nb"&gt;bind&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; M-Up &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="nt"&gt;-pane&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt;
&lt;span class="nb"&gt;bind&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; M-Down &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="nt"&gt;-pane&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt;

&lt;span class="c"&gt;# Add keybinding to select windows with Alt+Shift+Right (next window) or Alt+Shift+Left (previous window).&lt;/span&gt;
&lt;span class="nb"&gt;bind&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; S-M-Right &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="nt"&gt;-window&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt;
&lt;span class="nb"&gt;bind&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; S-M-Left &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="nt"&gt;-window&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt;

&lt;span class="c"&gt;# Add Keybinding to kill the current tmux session and exit tmux with Ctrl+Shift+x&lt;/span&gt;
&lt;span class="nb"&gt;bind&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; C-S-x kill-session

&lt;span class="c"&gt;# Add keybinding to clear the focused pane with Ctrl+Shift+k&lt;/span&gt;
&lt;span class="nb"&gt;bind&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; C-S-k send-keys &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt; clear-history

&lt;span class="c"&gt;# Minor futzing around with the layout&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; status-position bottom
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; status-justify left

&lt;span class="c"&gt;# Set what appears in the lower left&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; status-left &lt;span class="s1"&gt;'#{prefix_highlight}'&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; status-left-length 15

&lt;span class="c"&gt;# Set what appears in the lower right&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; status-right &lt;span class="s1"&gt;'#[bold]%Y-%m-%d %H:%M#[default] '&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; status-right-length 50

&lt;span class="c"&gt;# Give windows reasonable titles&lt;/span&gt;
setw &lt;span class="nt"&gt;-g&lt;/span&gt; window-status-current-format &lt;span class="s1"&gt;' #[bold]#I #W #F#[default] '&lt;/span&gt;

&lt;span class="c"&gt;# Give panes reasonable titles&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; pane-border-status top
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; pane-border-format &lt;span class="s1"&gt;'#[bold]#{pane_title}#[default]'&lt;/span&gt;

&lt;span class="c"&gt;# Run TMUX with the installed plugins&lt;/span&gt;
run &lt;span class="s1"&gt;'~/.tmux/plugins/tpm/tpm'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;u&gt;Resources and Links&lt;/u&gt; &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;h4&gt;
  
  
  TMUX Cheat sheets
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://tmuxcheatsheet.com/?source=post_page" rel="noopener noreferrer"&gt;tmuxcheatsheet.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.github.com/MohamedAlaa/2961058" rel="noopener noreferrer"&gt;tmux-cheatsheet.markdown&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://linuxize.com/cheatsheet/tmux/" rel="noopener noreferrer"&gt;TMUX Cheatsheet&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Plugins
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://github.com/tmux-plugins/tpm" rel="noopener noreferrer"&gt;TMUX Plugin Manager (TPM)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/tmux-plugins/list" rel="noopener noreferrer"&gt;List of TPM plugins&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Docs and Other
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://man7.org/linux/man-pages/man1/tmux.1.html" rel="noopener noreferrer"&gt;TMUX Man Page&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/tmux/tmux/wiki/Getting-Started" rel="noopener noreferrer"&gt;TMUX Wiki - Getting Started&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/rothgar/awesome-tmux?tab=readme-ov-file" rel="noopener noreferrer"&gt;Awesome Tmux&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hamvocke.com/blog/a-guide-to-customizing-your-tmux-conf/" rel="noopener noreferrer"&gt;A guide to customizing your TMUX config&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;u&gt;Extra: Considerations when using docker compose develop 'watch' functionality&lt;/u&gt; &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;If you're making use of &lt;a href="https://docs.docker.com/compose/how-tos/file-watch/" rel="noopener noreferrer"&gt;"Compose Watch"&lt;/a&gt; functionality in your docker-compose.yml file, you'll need to change your &lt;code&gt;.tmuxinator.yml&lt;/code&gt; configuration. This is because &lt;code&gt;docker compose up&lt;/code&gt; cannot run detached while in 'watch' mode.&lt;/p&gt;

&lt;p&gt;See &lt;a href="https://github.com/moofoo/docker-tmux-tutorial/blob/main/docker-compose.watch.yml" rel="noopener noreferrer"&gt;&lt;code&gt;docker-compose.watch.yml&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/moofoo/docker-tmux-tutorial/blob/main/.tmuxinator.watch.yml" rel="noopener noreferrer"&gt;&lt;code&gt;.tmuxinator.watch.yml&lt;/code&gt;&lt;/a&gt; in the example &lt;a href="https://github.com/moofoo/docker-tmux-tutorial" rel="noopener noreferrer"&gt;repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can try out the 'watch' docker compose/tmuxinator config by running&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.watch.yml build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmuxinator start &lt;span class="nt"&gt;-p&lt;/span&gt; .tmuxinator.watch.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The following YAML shows how the example repo's &lt;code&gt;.tmuxinator.yml&lt;/code&gt; file would be updated to work with Compose Watch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ./.tmuxinator.watch.yml&lt;/span&gt;

&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tmuxinator_watch_tut&lt;/span&gt;
&lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./&lt;/span&gt;

&lt;span class="c1"&gt;# Don't run docker compose up on_project_start&lt;/span&gt;
&lt;span class="c1"&gt;# on_project_start: docker compose up --wait -d&lt;/span&gt;

&lt;span class="na"&gt;on_project_exit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker compose stop&lt;/span&gt;

&lt;span class="na"&gt;windows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;logs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;panes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Need to sleep before running docker compose commands, so they run after `docker compose up --watch`&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sleep &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose -f docker-compose.watch.yml logs --since=1s -f watch_web&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sleep &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose -f docker-compose.watch.yml logs --since=1s -f watch_api&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sleep &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose -f docker-compose.watch.yml logs --since=15s -f watch_db&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sleep &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose -f docker-compose.watch.yml logs --since=15s -f watch_redis&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;panes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sleep &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose -f docker-compose.watch.yml exec -w /usr/src/app watch_web sh&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sleep &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose -f docker-compose.watch.yml exec -w /usr/src/app watch_api sh&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;psql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sleep &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose -f docker-compose.watch.yml exec watch_db psql -U postgres -d tutorial_db&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;nestjs_repl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sleep &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose -f docker-compose.watch.yml exec -w /usr/src/app watch_api npm run repl&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;home&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;layout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main-vertical&lt;/span&gt;
      &lt;span class="na"&gt;panes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;home&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clear&lt;/span&gt;

    &lt;span class="c1"&gt;# Run docker compose up --watch in its own window&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;watch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;layout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main-vertical&lt;/span&gt;
      &lt;span class="na"&gt;panes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;watch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker compose up --watch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To Summarize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instead of running &lt;code&gt;docker compose up&lt;/code&gt; in detached mode on_project_start, it's run in watch mode in its own window.&lt;/li&gt;
&lt;li&gt;Because of the above change, &lt;code&gt;docker compose logs/exec&lt;/code&gt; commands need to be delayed so that they run after the &lt;code&gt;docker compose up --watch&lt;/code&gt; command.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is of course possible to wait on services to be running before &lt;code&gt;log&lt;/code&gt;/&lt;code&gt;exec&lt;/code&gt; commands using a script, rather than sleep commands. This is desirable, because otherwise you need to run &lt;code&gt;docker compose build&lt;/code&gt; before starting the TMUX dashboard. But, the docker CLI metadata commands required for this take container names, not docker compose service names, so it's a little wonky:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;sleep &lt;/span&gt;1

&lt;span class="nv"&gt;SERVICE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;

&lt;span class="nv"&gt;COMPOSE_PROJECT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;docker compose &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; json | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.[0].Name'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Waiting for &lt;/span&gt;&lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="s2"&gt; to be running..."&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;docker inspect &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'{{.State.Status}}'&lt;/span&gt; &lt;span class="nv"&gt;$COMPOSE_PROJECT&lt;/span&gt;-&lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="nt"&gt;-1&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"running"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'.'&lt;/span&gt;
  &lt;span class="nb"&gt;sleep &lt;/span&gt;2&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script takes advantage of the fact that, if you haven't given your services explicit container_names, the container names that get generated when you start compose will be &lt;code&gt;{COMPOSE_PROJECT_NAME}-{SERVICE_NAME}-1&lt;/code&gt;. (That final integer can be greater for a service if you're using the --scale parameter or if you've set replicas &amp;gt; 1 in the service's &lt;a href="https://docs.docker.com/reference/compose-file/deploy/#replicas" rel="noopener noreferrer"&gt;deploy config&lt;/a&gt;. Regardless, for the purposes of this script we only care about the first container of a service).&lt;/p&gt;

&lt;p&gt;If you have given your services explicit container names, the script is simpler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;sleep &lt;/span&gt;1

&lt;span class="nv"&gt;CONTAINER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Waiting for &lt;/span&gt;&lt;span class="nv"&gt;$CONTAINER&lt;/span&gt;&lt;span class="s2"&gt; to be running..."&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;docker inspect &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'{{.State.Status}}'&lt;/span&gt; &lt;span class="nv"&gt;$CONTAINER&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"running"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'.'&lt;/span&gt;
  &lt;span class="nb"&gt;sleep &lt;/span&gt;2&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In either case, you could then rewrite the panes above like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bash the_wait_script.sh watch_web&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;clear &amp;amp;&amp;amp; docker compose -f docker-compose.watch.yml logs --since=1s -f watch_web&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>docker</category>
      <category>tmux</category>
      <category>terminal</category>
      <category>tui</category>
    </item>
    <item>
      <title>Using mkcert and Caddy with Docker Compose to host web services over HTTPS for local development</title>
      <dc:creator>Nathan Cook</dc:creator>
      <pubDate>Tue, 21 Apr 2026 07:16:47 +0000</pubDate>
      <link>https://forem.com/moofoo/docker-basics-using-mkcert-and-caddy-with-docker-compose-to-host-web-services-over-https-for-local-2a3d</link>
      <guid>https://forem.com/moofoo/docker-basics-using-mkcert-and-caddy-with-docker-compose-to-host-web-services-over-https-for-local-2a3d</guid>
      <description>&lt;p&gt;This tutorial walks you through setting up a simple &lt;a href="https://docs.docker.com/compose/" rel="noopener noreferrer"&gt;Docker Compose&lt;/a&gt; project that serves two &lt;a href="https://nodejs.org/en" rel="noopener noreferrer"&gt;Node&lt;/a&gt; web servers over &lt;a href="https://en.wikipedia.org/wiki/HTTPS" rel="noopener noreferrer"&gt;HTTPS&lt;/a&gt; using &lt;a href="https://caddyserver.com/" rel="noopener noreferrer"&gt;Caddy&lt;/a&gt; as a &lt;a href="https://caddyserver.com/docs/quick-starts/reverse-proxy" rel="noopener noreferrer"&gt;reverse proxy&lt;/a&gt;. You will learn how to use &lt;a href="https://github.com/filosottile/mkcert" rel="noopener noreferrer"&gt;mkcert&lt;/a&gt; to generate &lt;a href="https://caddyserver.com/docs/automatic-https#wildcard-certificates" rel="noopener noreferrer"&gt;wildcard certificates&lt;/a&gt; and the minimal configuration needed in the &lt;a href="https://caddyserver.com/docs/caddyfile" rel="noopener noreferrer"&gt;Caddyfile&lt;/a&gt; and &lt;a href="https://docs.docker.com/reference/compose-file/" rel="noopener noreferrer"&gt;docker-compose.yml&lt;/a&gt; to get it all working.&lt;/p&gt;

&lt;p&gt;This tutorial was written for Linux or WSL users (I'm running Ubuntu).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/moofoo/compose-caddy-tutorial" rel="noopener noreferrer"&gt;Github repo&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;u&gt;Introduction&lt;/u&gt;
&lt;/h2&gt;

&lt;p&gt;When running web services locally with Docker Compose, it’s easy to default to plain HTTP for simplicity. But doing so creates a gap between your development setup and production, where HTTPS is almost always used. This gap matters because of &lt;a href="https://12factor.net/dev-prod-parity" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;development/production parity&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;: the closer your environments match, the fewer surprises you’ll hit when deploying.&lt;/p&gt;

&lt;p&gt;Using HTTPS locally helps you catch issues early, like secure cookies not being set, browser features being blocked in non-secure contexts, or mixed-content errors. By adding a reverse proxy and self-signed certificates to your Compose setup, you mirror real-world conditions more closely and avoid the classic &lt;strong&gt;&lt;em&gt;“well, it worked on my machine...”&lt;/em&gt;&lt;/strong&gt; problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;u&gt;First Steps&lt;/u&gt;
&lt;/h2&gt;

&lt;h4&gt;
  
  
  - Make sure you have the following required packages installed
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://git-scm.com/install/linux" rel="noopener noreferrer"&gt;Git&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Docker and Docker Compose (naturally). (&lt;a href="https://docs.docker.com/engine/install/" rel="noopener noreferrer"&gt;Install Docker Engine&lt;/a&gt;)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's worthwhile to check if docker and docker compose are up to date. As I'm writing this tutorial, Docker is currently at version &lt;code&gt;v29.4.1&lt;/code&gt;, and docker compose at &lt;code&gt;v5.1.2&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;docker -v&lt;/code&gt; and &lt;code&gt;docker compose --version&lt;/code&gt; to check your versions&lt;/li&gt;
&lt;li&gt;If you're out of date by a major version for either, consider following the intructions for uninstalling old versions at &lt;a href="https://docs.docker.com/engine/install/ubuntu/#uninstall-old-versions" rel="noopener noreferrer"&gt;https://docs.docker.com/engine/install/ubuntu/#uninstall-old-versions&lt;/a&gt; and re-installing docker.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;mkcert&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Follow the instructions at &lt;a href="https://github.com/FiloSottile/mkcert#installation" rel="noopener noreferrer"&gt;https://github.com/FiloSottile/mkcert#installation&lt;/a&gt; to install mkcert for your environment&lt;/li&gt;
&lt;li&gt;After mkcert is installed, run "mkcert -install". This is a one-time setup step that enables your computer to trust the locally generated SSL certificates created by mkcert. &lt;strong&gt;The docker compose project described in this tutorial will not work otherwise.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h4&gt;
  
  
  - Run the following commands in whatever directory you keep your coding projects to clone the tutorial repo and cd into the directory:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/moofoo/compose-caddy-tutorial.git

&lt;span class="nb"&gt;cd &lt;/span&gt;compose-caddy-tutorial
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the following directory structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;├── Caddyfile
├── Dockerfile
├── README.md
├── docker-compose.yml
├── apps
│   ├── admin.js
│   └── www.js
└── scripts
    ├── caddyfile.sh
    └── certs.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;u&gt;Update /etc/hosts&lt;/u&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Did you run &lt;code&gt;mkcert -install&lt;/code&gt; after installing mkcert? If you haven't, do that now.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For this tutorial, our docker compose project will be serving two extremely simple node web servers at &lt;code&gt;https://www.caddy-test.local&lt;/code&gt; and &lt;code&gt;https://admin.caddy-test.local&lt;/code&gt;. We will be generating wildcard certificates to manage the two subdomains (www and admin).&lt;/p&gt;

&lt;p&gt;Before we get to the Caddy/Compose configuration, the first thing we're going to do is update the &lt;code&gt;/etc/hosts&lt;/code&gt; file to map those domains to a &lt;a href="https://en.wikipedia.org/wiki/Localhost" rel="noopener noreferrer"&gt;local loopback ip address&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The available loopback address range you have for local usage is &lt;code&gt;127.0.0.1&lt;/code&gt; to &lt;code&gt;127.255.255.255&lt;/code&gt;, but I've found that below &lt;code&gt;127.0.0.10&lt;/code&gt; can sometimes be hit or miss on availability, so we're going to use &lt;code&gt;127.0.0.11&lt;/code&gt; for this tutorial.&lt;/p&gt;

&lt;p&gt;At any rate,&lt;/p&gt;

&lt;h4&gt;
  
  
  - Open your the hosts file at &lt;code&gt;/etc/hosts&lt;/code&gt; and add these lines at the bottom:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;127.0.0.11 www.caddy-test.local
127.0.0.11 admin.caddy-test.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;u&gt;Creating certificates for Caddy&lt;/u&gt;
&lt;/h2&gt;

&lt;p&gt;As mentioned, for this project we're going to create "wildcard" certificates. Wildcard SSL/TLS certificates are certificates that secure an entire domain and all of its first-level subdomains with a single certificate. For example, &lt;code&gt;*.example.com&lt;/code&gt; covers &lt;code&gt;api.example.com&lt;/code&gt;, &lt;code&gt;www.example.com&lt;/code&gt;, and &lt;code&gt;admin.example.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Running "&lt;code&gt;mkcert --help&lt;/code&gt;" tells us the syntax to create wildcard certificates is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;mkcert &lt;span class="s2"&gt;"*.example.it"&lt;/span&gt;
    Generate &lt;span class="s2"&gt;"_wildcard.example.it.pem"&lt;/span&gt; and &lt;span class="s2"&gt;"_wildcard.example.it-key.pem"&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Knowing that,&lt;/p&gt;

&lt;h4&gt;
  
  
  - Run the following individual commands (or use the helper ./scripts/certs.sh script mentioned afterwards) to generate wildcard certificates for "*.caddy-test.local" in the ./certs directory
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mkcert &lt;span class="s2"&gt;"*.caddy-test.local"&lt;/span&gt;

&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ./certs

&lt;span class="nb"&gt;mv&lt;/span&gt; ./_wildcard.caddy-test.local.pem ./certs

&lt;span class="nb"&gt;mv&lt;/span&gt; ./_wildcard.caddy-test.local-key.pem ./certs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tutorial repo has a helper bash script &lt;code&gt;scripts/certs.sh&lt;/code&gt;, which takes a domain as an argument and performs the above commands. To create certificates in ./certs like the commands would do above, call it like &lt;code&gt;bash scripts/certs.sh caddy-test.local&lt;/code&gt; from the project's root directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;u&gt;The Caddy configuration file&lt;/u&gt;
&lt;/h2&gt;

&lt;p&gt;Here is the Caddyfile the caddy service will use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;(tls) {&lt;/span&gt;
    &lt;span class="s"&gt;tls /etc/caddy/certs/_wildcard.caddy-test.local.pem /etc/caddy/certs/_wildcard.caddy-test.local-key.pem&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;www.caddy-test.local{&lt;/span&gt;
    &lt;span class="s"&gt;import tls&lt;/span&gt;
    &lt;span class="s"&gt;reverse_proxy www:3000&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;admin.caddy-test.local{&lt;/span&gt;
    &lt;span class="s"&gt;import tls&lt;/span&gt;
    &lt;span class="s"&gt;reverse_proxy admin:3001&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's go over each block&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;(tls) {&lt;/span&gt;
    &lt;span class="s"&gt;tls /etc/caddy/certs/_wildcard.caddy-test.local.pem /etc/caddy/certs/_wildcard.caddy-test.local-key.pem&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;a href="https://caddyserver.com/docs/caddyfile/concepts#snippets" rel="noopener noreferrer"&gt;snippet&lt;/a&gt; configures tls to use the wildcard certificates we created earlier. The caddy &lt;a href="https://docs.docker.com/reference/compose-file/services/" rel="noopener noreferrer"&gt;docker compose service&lt;/a&gt; config, which we'll get to in a bit, will use a &lt;a href="https://docs.docker.com/engine/storage/bind-mounts/" rel="noopener noreferrer"&gt;bind mount volume&lt;/a&gt; to make our local &lt;code&gt;./certs&lt;/code&gt; directory available in the container at path &lt;code&gt;/etc/caddy/certs&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;www.caddy-test.local {&lt;/span&gt;
    &lt;span class="s"&gt;import tls&lt;/span&gt;
    &lt;span class="s"&gt;reverse_proxy www:3000&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This site block configures the reverse proxy for &lt;code&gt;https://www.caddy-test.local&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;import tls&lt;/code&gt; line copies the previously defined &lt;code&gt;tls&lt;/code&gt; snippet into the block.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;reverse_proxy www:3000&lt;/code&gt; creates a reverse proxy for host &lt;code&gt;www&lt;/code&gt; and port &lt;code&gt;3000&lt;/code&gt;. 

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;www:3000&lt;/code&gt; is the service/host name and port of the node service within the docker compose bridge network.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;In other words, this site block makes it so requests to &lt;code&gt;&lt;a href="http://www.caddy-test.local" rel="noopener noreferrer"&gt;www.caddy-test.local&lt;/a&gt;&lt;/code&gt; will get routed to address &lt;code&gt;www:3000&lt;/code&gt;, which within the internal docker bridge network belongs to a service running a Node web server that accepts requests on port 3000.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;The site block for &lt;code&gt;admin.caddy-test.local&lt;/code&gt; follows the same pattern, but with &lt;code&gt;admin:3001&lt;/code&gt; for the service/host name and port.&lt;/p&gt;

&lt;p&gt;Now, let's take a look at how the services are set up in docker-compose.yml:&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;u&gt;Service setup in docker-compose.yml&lt;/u&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;caddy-tutorial&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;caddy_tutorial_network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;caddy_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;caddy_config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;www&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_tutorial_network&lt;/span&gt;
    &lt;span class="na"&gt;expose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PORT=3000&lt;/span&gt;
    &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node www.js&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;www&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./Dockerfile&lt;/span&gt;

  &lt;span class="na"&gt;admin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_tutorial_network&lt;/span&gt;
    &lt;span class="na"&gt;expose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="m"&gt;3001&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PORT=3001&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node admin.js&lt;/span&gt;
    &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./Dockerfile&lt;/span&gt;

  &lt;span class="na"&gt;caddy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;caddy:2.11.2-alpine&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_tutorial_network&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.11:80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.11:443:443"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;www&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;admin&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./certs:/etc/caddy/certs&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./Caddyfile:/etc/caddy/Caddyfile&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_data:/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_config:/config&lt;/span&gt;
    &lt;span class="na"&gt;cap_add&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NET_ADMIN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's go over each section, starting from the top:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;caddy_tutorial_network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a &lt;a href="https://docs.docker.com/engine/network/drivers/bridge/#differences-between-user-defined-bridges-and-the-default-bridge" rel="noopener noreferrer"&gt;"user defined bridge network"&lt;/a&gt;, which allows services to communicate with each other by name or alias (versus by ip address). Like &lt;code&gt;admin:3001&lt;/code&gt; for the admin Node service, for example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;caddy_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;caddy_config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This defines two named volumes which are used by the Caddy service for persisting whatever data/config in its container (the official caddy examples say they're needed).&lt;/p&gt;

&lt;p&gt;Now the node services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;www&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_tutorial_network&lt;/span&gt;
    &lt;span class="na"&gt;expose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PORT=3000&lt;/span&gt;
    &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node www.js&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;www&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./Dockerfile&lt;/span&gt;

&lt;span class="na"&gt;admin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_tutorial_network&lt;/span&gt;
  &lt;span class="na"&gt;expose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="m"&gt;3001&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PORT=3001&lt;/span&gt;
  &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node admin.js&lt;/span&gt;
  &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./Dockerfile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looking at the config for service &lt;code&gt;www&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_tutorial_network&lt;/span&gt;
    &lt;span class="na"&gt;expose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This config connects the service to the user defined bridge network, and exposes it to the other services on the network. The service is therefore visible at address &lt;code&gt;www:3000&lt;/code&gt; to other services running on that internal docker network. Because &lt;code&gt;ports&lt;/code&gt; has not been defined, the service cannot be accessed directly by the host (which is because it doesn't need to be).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PORT=3000&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node www.js&lt;/span&gt;
    &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This config sets the environment variable &lt;code&gt;PORT&lt;/code&gt; to 3000 in the running container, and specifies that the command &lt;code&gt;node www.js&lt;/code&gt; on path &lt;code&gt;/app&lt;/code&gt; should run when the service starts up.&lt;/p&gt;

&lt;p&gt;Let's look at &lt;code&gt;./apps/www.js&lt;/code&gt; now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0.0.0.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 3000 for service `www`, 3001 for service `admin`, per the service environment config&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestListener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User Site&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestListener&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Server is running on http://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This very simple server runs at address 0.0.0.0 on the port determined by the environment variable from the service config. 0.0.0.0 isn't a real address, but basically translates to "all IPv4 addresses". The server simply responds to requests with the text 'User Site'.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;./apps/admin.js&lt;/code&gt; is the exact same code, except it responds with the text 'Admin Site'.&lt;/p&gt;

&lt;p&gt;Moving on with the Docker Compose service config,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;www&lt;/span&gt;
  &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
  &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./Dockerfile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This part of the config determines how the &lt;a href="https://docs.docker.com/reference/compose-file/build/" rel="noopener noreferrer"&gt;container image for the service will be built&lt;/a&gt;. The NAME arg with value &lt;code&gt;www&lt;/code&gt; is available for the Dockerfile on build, which allows the use of a single Dockerfile for this simple project. Let's look at that Dockerfile now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:lts-alpine&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; NAME&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./apps/$NAME.js /app/$NAME.js&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "$NAME.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, the container image for the &lt;code&gt;www&lt;/code&gt; service would basically be this, with $NAME resolving to &lt;code&gt;www&lt;/code&gt; upon build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:lts-alpine&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./apps/www.js /app/www.js&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "www.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And likewise for the admin service's image.&lt;/p&gt;

&lt;p&gt;Finally, let's go over the the caddy service configuration, which is using the &lt;code&gt;caddy:2.11.2-alpine&lt;/code&gt; image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;caddy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;caddy:2.11.2-alpine&lt;/span&gt;
  &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_tutorial_network&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.11:80:80"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.11:443:443"&lt;/span&gt;
  &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;www&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;admin&lt;/span&gt;
  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./certs:/etc/caddy/certs&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./Caddyfile:/etc/caddy/Caddyfile&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_data:/data&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_config:/config&lt;/span&gt;
  &lt;span class="na"&gt;cap_add&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NET_ADMIN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Starting from the top:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_tutorial_network&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.11:80:80"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.11:443:443"&lt;/span&gt;
  &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;www&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;admin&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Like the node services, the Caddy service runs on the &lt;code&gt;caddy_tutorial_network&lt;/code&gt; bridge network. But unlike the Node services, it exposes itself to the host at address &lt;code&gt;127.0.0.11&lt;/code&gt; on ports 80 and 443.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;depends_on&lt;/code&gt; config tells the Caddy service to wait until the &lt;code&gt;www&lt;/code&gt; and &lt;code&gt;admin&lt;/code&gt; services are running before it gets going, since Caddy will freak if the Node servers aren't responsive when it starts up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./certs:/etc/caddy/certs&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./Caddyfile:/etc/caddy/Caddyfile&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_data:/data&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_config:/config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first two volumes bind mount our ./certs directory and the Caddyfile to the appropriate paths in the container. The second two volumes are the named volumes defined at the top of the docker-compose.yml, which Caddy needs for storing whatever.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;cap_add&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NET_ADMIN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This config permits the Caddy service to override buffer limits as needed without requiring manual changes to your linux configuration. Caddy running as a compose service is oddly temperamental about whether it has buffers of the size it thinks it needs. In any case, adding this config will fix that problem when/if caddy complains about buffer sizes.&lt;/p&gt;

&lt;p&gt;Alright! Let's run this thing!&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;&lt;u&gt;Run that thang!&lt;/u&gt;&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;From the project root directory, run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(I'm assuming docker must be run by the root user, which can be changed by following the instructions &lt;a href="https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user" rel="noopener noreferrer"&gt;here&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;After everything builds and starts up, you should see the text 'User Site' when you open &lt;a href="https://www.caddy-test.local" rel="noopener noreferrer"&gt;https://www.caddy-test.local&lt;/a&gt; in your browser and 'Admin Site' when you open &lt;a href="https://admin.caddy-test.local" rel="noopener noreferrer"&gt;https://admin.caddy-test.local&lt;/a&gt;. Huzzah!&lt;/p&gt;

&lt;h2&gt;
  
  
  Tips
&lt;/h2&gt;

&lt;p&gt;After you've made changes to the caddy service definition, the Caddyfile, and/or your /etc/hosts file, you may find that your project's web services aren't available as expected at the domain(s) you've specified.&lt;/p&gt;

&lt;p&gt;Assuming your configuration is actually correct, you can (usually) resolve such docker networking issues / caddy confusion by running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;--remove-orphans&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker network prune
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then do &lt;code&gt;docker compose up --build&lt;/code&gt; when you start the compose project&lt;/p&gt;

</description>
      <category>docker</category>
      <category>node</category>
      <category>caddy</category>
      <category>mkcert</category>
    </item>
    <item>
      <title>Optimize Yarn 1.2 Monorepo Docker Development with Turborepo: Tips for Prisma and Dockerfile build filtering</title>
      <dc:creator>Nathan Cook</dc:creator>
      <pubDate>Fri, 02 Jun 2023 09:44:52 +0000</pubDate>
      <link>https://forem.com/moofoo/optimize-yarn-12-monorepo-docker-development-with-turborepo-tips-for-prisma-and-dockerfile-build-filtering-4pm3</link>
      <guid>https://forem.com/moofoo/optimize-yarn-12-monorepo-docker-development-with-turborepo-tips-for-prisma-and-dockerfile-build-filtering-4pm3</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/moofoo/turborepo-tricks-prisma-workspace" rel="noopener noreferrer"&gt;Github Repository for this article&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This yarn 1.2 monorepo demonstrates&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why putting Prisma in its own workspace is a good idea&lt;/li&gt;
&lt;li&gt;Docker container build time difference between &lt;code&gt;turbo run build --filter=service&lt;/code&gt; and &lt;code&gt;turbo run build --filter=service^...&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;bash setup.sh
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

docker pull node:20.2.0-alpine3.17
docker pull postgres:15.3-alpine3.17

docker image build &lt;span class="nt"&gt;-f&lt;/span&gt; dockerfiles/Dockerfile.node &lt;span class="nt"&gt;-t&lt;/span&gt; custom-node:latest dockerfiles

yarn
yarn workspace prisma-client build
yarn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This monorepo has the following workspaces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;apps/frontend&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;apps/backend&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/prisma-client&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why put Prisma in a workspace?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  App workspaces can access Prisma types
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;apps/backend/src/app.service.ts&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@nestjs/common&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;PrismaService&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nestjs-prisma&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prisma-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PrismaService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="nf"&gt;getHello&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello World!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;getUsers&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;apps/frontend/src/use-data.ts&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ofetch&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ofetch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prisma-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;

  &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ofetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:3333/users&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  In frontend/backend package.json
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prisma-client"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Simplifies generating the prisma client in Docker containers
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;packages/prisma-client/package.json&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prisma-client"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"src/index.ts"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"repository"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/moofoo/turborepo-tricks-prisma-workspace.git"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"moofoo &amp;lt;nathancookdev@gmail.com&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MIT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"private"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx -y rimraf dist/* &amp;amp;&amp;amp; prisma generate &amp;amp;&amp;amp; npx tsc"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@prisma/client"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4.15.0"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"devDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@types/node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^20.2.5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prisma"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4.15.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rimraf"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^5.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"typescript"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^5.1.3"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;prisma generate&lt;/code&gt; in the build script, &lt;code&gt;turbo run build ...&lt;/code&gt; will generate the prisma client for workspaces that have &lt;code&gt;prisma-client&lt;/code&gt; as a dependency. This is particularly useful when it comes to writing Dockerfiles, since it makes it easier to write generic Dockerfiles for development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build time difference between &lt;code&gt;--filter=service&lt;/code&gt; and &lt;code&gt;--filter=service^...&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If you compare &lt;a href="https://github.com/moofoo/turborepo-tricks-prisma-workspace/docker-compose.good.yml" rel="noopener noreferrer"&gt;docker-compose.good.yml&lt;/a&gt; and &lt;a href="https://github.com/moofoo/turborepo-tricks-prisma-workspace/docker-compose.bad.yml" rel="noopener noreferrer"&gt;docker-compose.bad.yml&lt;/a&gt;, you'll see that the only difference is whether the Node.js services use &lt;a href="https://github.com/moofoo/turborepo-tricks-prisma-workspace/dockerfiles/Dockerfile.good" rel="noopener noreferrer"&gt;Dockerfile.good&lt;/a&gt; or &lt;a href="https://github.com/moofoo/turborepo-tricks-prisma-workspace/dockerfiles/Dockerfile.bad" rel="noopener noreferrer"&gt;Dockerfile.bad&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The only difference between those Dockerfiles is on line 30&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Dockerfile.good&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;turbo run build &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;^...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Dockerfile.bad&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;turbo run build &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  The latter RUN command is equivalent to running yarn workspace ... build for the given app workspace and all dependent workspaces
&lt;/h4&gt;

&lt;h2&gt;
  
  
  PLEASE NOTE THAT THE &lt;code&gt;compare.sh&lt;/code&gt; BASH SCRIPT RUNS &lt;code&gt;docker system prune -f&lt;/code&gt; TO ACCURATELY COMPARE BUILD TIMES
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;compare.sh&lt;/code&gt; to see the difference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bash compare.sh
&lt;span class="go"&gt;Cleaning up
Building bad
yarn run v1.22.19
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.bad.yml build &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt;
&lt;span class="go"&gt;Done in 155.18s.

real    2m35.339s
user    0m0.531s
sys     0m0.230s
Cleaning up
Building good
yarn run v1.22.19
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.good.yml build &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt;
&lt;span class="go"&gt;Done in 94.58s.

real    1m34.768s
user    0m0.498s
sys     0m0.165s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;--filter=workspace&lt;/code&gt; : 2m35.339s
&lt;/h3&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;--filter=workspace^...&lt;/code&gt; : 1m34.768s
&lt;/h3&gt;

&lt;p&gt;(I intentionally didn't follow dependency best practices with the Frontend app to increase its build time, to make the difference between the two build commands more clear. The basic point is don't build workspaces if you don't need to)&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Fully featured report generation in Node with the JSReports rendering core</title>
      <dc:creator>Nathan Cook</dc:creator>
      <pubDate>Fri, 26 May 2023 05:39:03 +0000</pubDate>
      <link>https://forem.com/moofoo/fully-featured-and-free-report-generation-from-the-ground-up-using-the-jsreport-rendering-core-gep</link>
      <guid>https://forem.com/moofoo/fully-featured-and-free-report-generation-from-the-ground-up-using-the-jsreport-rendering-core-gep</guid>
      <description>&lt;p&gt;(Check out &lt;a href="https://github.com/moofoo/nestjs-jsreport-examples" rel="noopener noreferrer"&gt;this repo&lt;/a&gt; to get an idea of what's possible)&lt;/p&gt;

&lt;p&gt;If you're interested in a free, self-hosted NodeJS report generation solution that doesn't compromise on templating functionality, you should check out &lt;a href="https://jsreport.net/" rel="noopener noreferrer"&gt;JSReport&lt;/a&gt;, specifically the functionality one can build using the jsreport '&lt;a href="https://github.com/jsreport/jsreport/tree/master/packages/jsreport-core" rel="noopener noreferrer"&gt;rendering core&lt;/a&gt;'. &lt;/p&gt;

&lt;p&gt;JSReport is open-source, and unlike many SaaS providers in this space, they do not gatekeep &lt;strong&gt;ANY&lt;/strong&gt; of their product's core functionality behind tiered subscription pricing. While JSReport does charge for their web-based 'Template Studio' and SaaS offerings, the report generation framework that powers these products is open source and free to use however you want.&lt;/p&gt;

&lt;p&gt;Let's get started with a basic example.&lt;/p&gt;

&lt;p&gt;Open up your terminal (I'm assuming linux, no promises if you're in Windows-land) and run the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;git clone https://github.com/moofoo/basic-jsreport-core-example.git &amp;amp;&amp;amp; \
cd basic-jsreport-core-example &amp;amp;&amp;amp; \
yarn

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, lets look at the &lt;a href=""&gt;package.json&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"basic-jsreport-core-example"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MIT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@jsreport/jsreport-core"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3.11.4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@jsreport/jsreport-docx"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3.7.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@jsreport/jsreport-handlebars"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3.2.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@jsreport/jsreport-unoconv"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"handlebars"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4.7.7"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/jsreport/jsreport/tree/master/packages/jsreport-core" rel="noopener noreferrer"&gt;&lt;code&gt;@jsreport/jsreport-core&lt;/code&gt;&lt;/a&gt; is the "minimalist jsreport rendering core", on to which we'll add 'extensions' (plugins, what have you), to build up the functionality we want.&lt;/p&gt;

&lt;p&gt;JSReport categorizes extensions into three categories: Engines, Recipes, Everything Else&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Engines - These are templating languages, i.e. handlebars, ejs, etc. The current example uses handlebars, by way of the &lt;code&gt;@jsreport/jsreport-handlebars&lt;/code&gt; extension. For any given Engine you will also need the actual templating language in your dependencies - &lt;code&gt;handlebars@4.7.7&lt;/code&gt; in this case.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Recipes &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"A recipe represents the technique used to print the document. It can be an HTML to pdf conversion, DOCX rendering, and others."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The current example employs the &lt;code&gt;docx&lt;/code&gt; recipe, so we have the &lt;code&gt;@jsreport/jsreport-docx&lt;/code&gt; extension as a dependency.&lt;/p&gt;

&lt;p&gt;3, Everything Else - for instance, this repo uses the &lt;code&gt;@jsreport/jsreport-unoconv&lt;/code&gt; extension to do PDF conversion via unoconv.&lt;/p&gt;

&lt;p&gt;Now let's look at &lt;a href="https://github.com/moofoo/basic-jsreport-core-example/blob/main/index.js" rel="noopener noreferrer"&gt;index.js&lt;/a&gt;. Here's that file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unlinkSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;templateData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./data.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// remove existing report files, if they exist&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;unlinkSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./report.docx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;unlinkSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./report.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no report files&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Create jsreport instance&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jsreport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@jsreport/jsreport-core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)();&lt;/span&gt;

&lt;span class="c1"&gt;// Add plugins: docx recipe, handlebars engine and unoconv extension for pdf conversion&lt;/span&gt;
&lt;span class="nx"&gt;jsreport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@jsreport/jsreport-docx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)());&lt;/span&gt;
&lt;span class="nx"&gt;jsreport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@jsreport/jsreport-handlebars&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)());&lt;/span&gt;
&lt;span class="nx"&gt;jsreport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@jsreport/jsreport-unoconv&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)());&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Initialize jsreport instance&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;jsreport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Read template.docx, base64 encoded&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./template.docx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Render a .docx report from template&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resultDocx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;jsreport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;docx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;handlebars&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;docx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;templateAsset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;templateData&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Write report.docx to disk&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./report.docx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resultDocx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;report.docx created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Render a .pdf report from template using unoconv extension&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resultPdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;jsreport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;docx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;handlebars&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;docx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;templateAsset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;unoconv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;templateData&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Write report.pdf to disk&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./report.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resultPdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;binary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;report.pdf created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's go over the JsReport-relevant lines. We start with some initialization boilerplate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Create jsreport instance&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jsreport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@jsreport/jsreport-core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)();&lt;/span&gt;

&lt;span class="c1"&gt;// Add plugins: docx recipe, handlebars engine and unoconv extension for pdf conversion&lt;/span&gt;
&lt;span class="nx"&gt;jsreport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@jsreport/jsreport-docx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)());&lt;/span&gt;
&lt;span class="nx"&gt;jsreport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@jsreport/jsreport-handlebars&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)());&lt;/span&gt;
&lt;span class="nx"&gt;jsreport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@jsreport/jsreport-unoconv&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)());&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The next lines initialize the instance given the added extensions and gets the template file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// Initialize jsreport instance&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;jsreport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Read template.docx, base64 encoded&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./template.docx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the base64 encoding.&lt;/p&gt;

&lt;p&gt;At this point, the jsReport instance is ready to render docx (or pdf) reports from .docx templates:&lt;br&gt;
&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Render a .docx report from template&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resultDocx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;jsreport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;docx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;handlebars&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;docx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;templateAsset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;templateData&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Write report.docx to disk&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./report.docx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resultDocx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;report.docx created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's go over the options passed to jsreport.render, specifically the template config:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;content: "",&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;While the content param isn't used here,  JsReport will throw an error if it is undefined. An empty string suffices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;recipe: "docx"&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This sets the render 'recipe'. Regarding naming conventions:&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;"The convention is that jsreport repository extension starts with jsreport-xxx, but the extension real name and also the recipes or engines it registers excludes the jsreport- prefix. This means if you install extension @jsreport/jsreport-handlebars the engine's name you specify in the render should be handlebars."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Accordingly, &lt;code&gt;@jsreport/jsreport-docx&lt;/code&gt; transmutes to 'docx', here.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;engine: "handlebars"&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;same story
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;docx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;templateAsset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;content&lt;/code&gt; is just the template.docx file, base64 encoded, and &lt;code&gt;encoding&lt;/code&gt; informs the render method about that encoding.&lt;/p&gt;

&lt;p&gt;The next &lt;a href="https://github.com/moofoo/basic-jsreport-core-example/blob/8f15fd3c8f43f6f1d2ff118b91e2a294bd8a9f6e/index.js#L49" rel="noopener noreferrer"&gt;jsreport.render call&lt;/a&gt; is identical to the previous one, with one addition to the &lt;code&gt;template&lt;/code&gt; options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; unoconv: {
    format: "pdf",
    enabled: true,
 },

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, when this render runs, a PDF will be created, rather than a .docx file. Note that enabled is assumed to be false if undefined, so it must be set to &lt;code&gt;true&lt;/code&gt; here.&lt;/p&gt;

&lt;p&gt;Using whatever document viewer you have handy, check out whats in &lt;a href="https://github.com/moofoo/basic-jsreport-core-example/raw/main/template.docx" rel="noopener noreferrer"&gt;template.docx&lt;/a&gt;. This is just an invoice example I pulled from the &lt;a href="https://playground.jsreport.net/" rel="noopener noreferrer"&gt;JSReport Playground&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Ok, let's run this thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node index.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Assuming nothing goes horribly wrong, you should now have two report files in the repo root, &lt;code&gt;report.pdf&lt;/code&gt; and &lt;code&gt;report.docx&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  --------------------
&lt;/h3&gt;

&lt;p&gt;While the code presented here is nothing fancy, the real magic is how this extremely simple setup gives you full access to JSReport's robust templating features, for example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://jsreport.net/learn/docx#docxhtml" rel="noopener noreferrer"&gt;embedded html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jsreport.net/learn/docx#docximage" rel="noopener noreferrer"&gt;images&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jsreport.net/learn/docx#docxlist" rel="noopener noreferrer"&gt;lists&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jsreport.net/learn/docx#docxtable" rel="noopener noreferrer"&gt;tables&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jsreport.net/learn/docx#docxchart" rel="noopener noreferrer"&gt;charts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jsreport.net/learn/docx#forms" rel="noopener noreferrer"&gt;form inputs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jsreport.net/learn/docx#docxwatermark" rel="noopener noreferrer"&gt;watermarks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jsreport.net/learn/docx#docxraw" rel="noopener noreferrer"&gt;raw XML&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jsreport.net/learn/docx#docxpagebreak" rel="noopener noreferrer"&gt;conditional page breaks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that's just for &lt;a href="https://jsreport.net/learn/docx" rel="noopener noreferrer"&gt;.docx based templates&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That's all for now!&lt;/p&gt;

&lt;p&gt;NestJS + JSReport integration will be covered in a future article, however the repo for that article is already complete: &lt;a href="https://github.com/moofoo/nestjs-jsreport-examples" rel="noopener noreferrer"&gt;https://github.com/moofoo/nestjs-jsreport-examples&lt;/a&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>jsreports</category>
      <category>reports</category>
      <category>templates</category>
    </item>
    <item>
      <title>Creating a development dockerfile and docker-compose.yml for yarn 1.22 monorepos using Turborepo.</title>
      <dc:creator>Nathan Cook</dc:creator>
      <pubDate>Wed, 17 May 2023 03:01:31 +0000</pubDate>
      <link>https://forem.com/moofoo/creating-a-development-dockerfile-and-docker-composeyml-for-yarn-122-monorepos-using-turborepo-896</link>
      <guid>https://forem.com/moofoo/creating-a-development-dockerfile-and-docker-composeyml-for-yarn-122-monorepos-using-turborepo-896</guid>
      <description>&lt;p&gt;Check out my new tutorial on this subject &lt;a href="https://dev.to/moofoo/typescript-monorepo-development-using-docker-compose-watch-turborepo-and-pnpm-3hep"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(Be sure to check out &lt;a href="https://github.com/moofoo/turbo-docker-monorepo" rel="noopener noreferrer"&gt;the example repo&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;If you've ever worked in a yarn monorepo project with inter-dependent workspaces, you know that creating docker containers for development in a way that avoids unnecessary rebuilds can be a challenge.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://turbo.build/repo" rel="noopener noreferrer"&gt;Turborepo&lt;/a&gt;, "...an intelligent build system optimized for JavaScript and TypeScript codebases", provides tools that make this task much easier.&lt;/p&gt;

&lt;p&gt;Let's assume a project that looks like something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- /project-directory
    - /apps
        - /frontend
            - package.json
            - other files...
        - /backend
            - package.json
            - other files...
    - /packages
        - /shared-stuff
            - package.json
            - other files...
    - .dockerignore
    - package.json
    - turbo.json
    - Dockerfile.dev
    - docker-compose.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;a href="https://docs.docker.com/engine/reference/builder/#dockerignore-file" rel="noopener noreferrer"&gt;.dockerignore&lt;/a&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;**/&lt;span class="n"&gt;node_modules&lt;/span&gt;
**/.&lt;span class="n"&gt;next&lt;/span&gt;
**/&lt;span class="n"&gt;dist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures that node_modules and build artifacts are excluded when copying directories to the container from the host in our Dockerfile.&lt;/p&gt;

&lt;h3&gt;
  
  
  package.json
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"turbo-docker-monorepo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MIT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"private"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"workspaces"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"apps/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"packages/*"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"packageManager"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yarn@1.22.19"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"devDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"turbo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^1.9.6"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the minimal package.json you'll need for a yarn monorepo that makes use of turborepo with workspaces in the 'apps' and 'packages' directories. Not much else to say!&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://turbo.build/repo/docs/reference/configuration" rel="noopener noreferrer"&gt;turbo.json&lt;/a&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://turbo.build/schema.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pipeline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"^build"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"outputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"dist/**"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".next/**"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"!.next/cache/**"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cache"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"persistent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This turbo.json file is taken from &lt;a href="https://github.com/vercel/turbo/tree/main/examples/basic" rel="noopener noreferrer"&gt;the basic turborepo example&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let's assume that the &lt;code&gt;backend&lt;/code&gt; app has the &lt;code&gt;packages/shared-stuff&lt;/code&gt; workspace as a dependency (meaning the backend package.json has &lt;code&gt;"shared-stuff": "*"&lt;/code&gt; in its list of dependencies).&lt;/p&gt;

&lt;p&gt;We can use turborepo to build backend in a way that respects its dependence on the &lt;code&gt;shared-stuff&lt;/code&gt; package with the command &lt;code&gt;turbo run build --filter=backend&lt;/code&gt;, which will build (in order) the &lt;code&gt;shared-stuff&lt;/code&gt; package and then &lt;code&gt;backend&lt;/code&gt; app. Cool!&lt;/p&gt;

&lt;h3&gt;
  
  
  Dockerfile.dev
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# syntax=docker/dockerfile:1.5.2&lt;/span&gt;
&lt;span class="c"&gt;# based on: https://github.com/vercel/turbo/blob/main/examples/with-docker/apps/api/Dockerfile&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20.2-alpine3.17&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;

&lt;span class="c"&gt;# adding apk deps to avoid node-gyp related errors and some other stuff. adds turborepo globally&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;--update&lt;/span&gt; &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="nt"&gt;--virtual&lt;/span&gt; .gyp nano bash libc6-compat python3 make g++ &lt;span class="se"&gt;\
&lt;/span&gt;      &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; yarn global add turbo &lt;span class="se"&gt;\
&lt;/span&gt;      &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apk del .gyp

&lt;span class="c"&gt;#############################################&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;pruned&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; APP&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="c"&gt;# see https://turbo.build/repo/docs/reference/command-line-reference#turbo-prune---scopetarget&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;turbo prune &lt;span class="nt"&gt;--scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$APP&lt;/span&gt; &lt;span class="nt"&gt;--docker&lt;/span&gt;

&lt;span class="c"&gt;#############################################&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;installer&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; APP&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=pruned /app/out/json/ .&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=pruned /app/out/yarn.lock /app/yarn.lock&lt;/span&gt;

&lt;span class="c"&gt;# Forces the layer to recreate if the app's package.json changes&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; apps/${APP}/package.json /app/apps/${APP}/package.json&lt;/span&gt;

&lt;span class="c"&gt;# see https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md#run---mounttypecache&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;      &lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/usr/local/share/.cache/yarn/v6,sharing&lt;span class="o"&gt;=&lt;/span&gt;locked &lt;span class="se"&gt;\
&lt;/span&gt;      yarn &lt;span class="nt"&gt;--prefer-offline&lt;/span&gt; &lt;span class="nt"&gt;--frozen-lockfile&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=pruned /app/out/full/ .&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; turbo.json turbo.json&lt;/span&gt;

&lt;span class="c"&gt;# For example: `--filter=frontend^...` means all of frontend's dependencies will be built, but not the frontend app itself (which we don't need to do for dev environment)&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;turbo run build &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;^...

&lt;span class="c"&gt;# re-running yarn ensures that dependencies between workspaces are linked correctly&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;      &lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/usr/local/share/.cache/yarn/v6,sharing&lt;span class="o"&gt;=&lt;/span&gt;locked &lt;span class="se"&gt;\
&lt;/span&gt;      yarn &lt;span class="nt"&gt;--prefer-offline&lt;/span&gt; &lt;span class="nt"&gt;--frozen-lockfile&lt;/span&gt;

&lt;span class="c"&gt;#############################################&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;runner&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; APP&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; START_COMMAND=dev&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=installer /app .&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; yarn workspace ${APP} ${START_COMMAND}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's go over what happens in each layer. For the following discussion, assume that the &lt;code&gt;backend&lt;/code&gt; app service is being created, which has &lt;code&gt;shared-stuff&lt;/code&gt; as a dependency. &lt;/p&gt;

&lt;h3&gt;
  
  
  base
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20.2-alpine3.17&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;--update&lt;/span&gt; &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="nt"&gt;--virtual&lt;/span&gt; .gyp nano bash libc6-compat python3 make g++ &lt;span class="se"&gt;\
&lt;/span&gt;      &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; yarn global add turbo &lt;span class="se"&gt;\
&lt;/span&gt;      &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apk del .gyp

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This layer adds container dependencies to ensure NPM modules that use node-gyp build correctly. It also adds turborepo globally. The remaining layers are built from 'base'&lt;/p&gt;

&lt;h3&gt;
  
  
  pruned
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;pruned&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; APP&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;turbo prune &lt;span class="nt"&gt;--scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$APP&lt;/span&gt; &lt;span class="nt"&gt;--docker&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This layer copies over the project files and runs &lt;a href="https://turbo.build/repo/docs/reference/command-line-reference#turbo-prune---scopetarget" rel="noopener noreferrer"&gt;turbo prune&lt;/a&gt; for the service in question. The $APP argument will be either 'frontend' or 'backend' (assuming the later, for this example), which is set in docker-compose.yml (this will be covered last). Regarding the &lt;code&gt;--docker&lt;/code&gt; flag:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;With the docker flag, the prune command will generate folder called out with the following inside of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A folder json with the pruned workspace's package.jsons&lt;/li&gt;
&lt;li&gt;A folder full with the pruned workspace's full source code, but only including the internal packages that are needed to build the target.&lt;/li&gt;
&lt;li&gt;A new pruned lockfile that only contains the pruned subset of the original root lockfile with the dependencies that are actually used by the packages in the pruned workspace.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Assuming we're building the &lt;code&gt;backend&lt;/code&gt; service container, here is what the 'out' directory in the ‘pruned’ layer would look like at this point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- /out
    - /full
       - /apps
           - /backend (all files)
       - /packages
           - /shared-stuff (all files)
       - .gitignore
       - package.json
       - turbo.json
    - /json
        - /apps
            - /backend
                - package.json
        - /packages
            - /shared-stuff
                - package.json
        - package.json
    - yarn.lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that the 'frontend' app is not present and (according to the turborepo docs) its dependencies were excluded from &lt;code&gt;yarn.lock&lt;/code&gt;, which is what we want!&lt;/p&gt;

&lt;h3&gt;
  
  
  installer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;installer&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; APP&lt;/span&gt;

&lt;span class="c"&gt;# COPY 1&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=pruned /app/out/json/ .&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=pruned /app/out/yarn.lock /app/yarn.lock&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; apps/${APP}/package.json /app/apps/${APP}/package.json&lt;/span&gt;

&lt;span class="c"&gt;# RUN 1&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;      &lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/usr/local/share/.cache/yarn/v6,sharing&lt;span class="o"&gt;=&lt;/span&gt;locked &lt;span class="se"&gt;\
&lt;/span&gt;      yarn &lt;span class="nt"&gt;--prefer-offline&lt;/span&gt; &lt;span class="nt"&gt;--frozen-lockfile&lt;/span&gt;

&lt;span class="c"&gt;# COPY 2&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=pruned /app/out/full/ .&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; turbo.json turbo.json&lt;/span&gt;

&lt;span class="c"&gt;# RUN 2&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;turbo run build &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;^...

&lt;span class="c"&gt;# RUN 3&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;      &lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/usr/local/share/.cache/yarn/v6,sharing&lt;span class="o"&gt;=&lt;/span&gt;locked &lt;span class="se"&gt;\
&lt;/span&gt;      yarn &lt;span class="nt"&gt;--prefer-offline&lt;/span&gt; &lt;span class="nt"&gt;--frozen-lockfile&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;COPY 1&lt;/strong&gt; command series begins by copying the &lt;code&gt;json&lt;/code&gt; directory in &lt;code&gt;out&lt;/code&gt; from the pruned layer, as well as the 'scoped' yarn.lock file. It also redundantly copies the app's package.json from the host, which I've found necessary for the container to recreate correctly when shared workspace dependencies change.&lt;/p&gt;

&lt;p&gt;The first run command &lt;strong&gt;(RUN 1)&lt;/strong&gt; runs &lt;code&gt;yarn&lt;/code&gt; and uses a &lt;a href="https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md#run---mounttypecache" rel="noopener noreferrer"&gt;cache mount&lt;/a&gt; targeting the yarn cache directory, which will help speed up subsequent re-builds.&lt;/p&gt;

&lt;p&gt;The next two statements &lt;strong&gt;(COPY 2)&lt;/strong&gt; copy the contents of &lt;code&gt;out/full&lt;/code&gt; from the &lt;code&gt;pruned&lt;/code&gt; layer as well as turbo.json from the host.&lt;/p&gt;

&lt;p&gt;Since this container will run in a dev environment, it means we can safely skip building the &lt;code&gt;backend&lt;/code&gt; app. However, we do need to build &lt;code&gt;packages/shared-stuff&lt;/code&gt;, which in the current example is a dependency of &lt;code&gt;backend&lt;/code&gt;. This is done with the command in &lt;strong&gt;RUN 2&lt;/strong&gt;: &lt;code&gt;RUN turbo run build --no-cache --filter=${APP}^...&lt;/code&gt;. In this case, since we're building the backend service container, the filter flag will resolve to &lt;code&gt;--filter=backend^...&lt;/code&gt;. What this means is that all workspace dependencies of backend, &lt;strong&gt;but not backend itself&lt;/strong&gt;, will be built, which is what we want. &lt;/p&gt;

&lt;p&gt;This is a huge time saver for certain frameworks (looking at you, Next.js).&lt;/p&gt;

&lt;p&gt;The final RUN command &lt;strong&gt;(RUN 3)&lt;/strong&gt; is just a copy of the first &lt;strong&gt;(RUN 1)&lt;/strong&gt;. This final &lt;code&gt;yarn&lt;/code&gt; call ensures that dependencies between workspaces are linked correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  runner
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;runner&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; APP&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; START_COMMAND=dev&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=installer /app .&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; yarn workspace ${APP} ${START_COMMAND}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final layer copies over the installer layer and runs the command to start the app (based on the &lt;code&gt;START_COMMAND&lt;/code&gt; argument).&lt;/p&gt;

&lt;p&gt;Now let's look at docker-compose.yml, specifically the config for the &lt;code&gt;backend&lt;/code&gt; service (almost done!)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;

&lt;span class="na"&gt;x-defaults&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="nl"&gt;&amp;amp;defaults&lt;/span&gt;
  &lt;span class="na"&gt;init&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;tty&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;my_monorepo_network&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;my_monorepo_network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*defaults&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3333:3333"&lt;/span&gt;
        &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn workspace backend dev&lt;/span&gt;
        &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PORT=3333&lt;/span&gt;
        &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;APP&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;
            &lt;span class="na"&gt;START_COMMAND&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;
          &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
          &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./Dockerfile.dev&lt;/span&gt;
        &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./apps/backend:/app/apps/backend&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/app/apps/backend/node_modules&lt;/span&gt;
    &lt;span class="s"&gt;...other services...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a pretty typical definition for a Node service. Here you can see the build args, which are used in Dockerfile.dev.&lt;/p&gt;

&lt;h3&gt;
  
  
  Changing dependencies
&lt;/h3&gt;

&lt;p&gt;tl;dr: &lt;code&gt;docker compose up -d -V --build &amp;lt;service&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;When you change dependencies for your node services on the host, you need to rebuild the container for it to reflect those changes.&lt;/p&gt;

&lt;p&gt;You don't need to use &lt;code&gt;docker compose build --no-cache &amp;lt;service&amp;gt;&lt;/code&gt; for this. There are better methods which don't require completely busting cache and starting from scratch. &lt;/p&gt;

&lt;p&gt;The complicating factor for node services, as commonly configured in docker compose projects, is anonymous volumes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;...&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./apps/backend:/app/apps/backend&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/app/apps/backend/node_modules&lt;/span&gt; &lt;span class="c1"&gt;# &amp;lt;-- this guy&lt;/span&gt;

&lt;span class="nn"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the context of yarn 1.22 monorepos, &lt;code&gt;apps/backend/node_modules&lt;/code&gt; contains development dependencies that are unique to that workspace. &lt;/p&gt;

&lt;p&gt;The point of the anonymous volume &lt;code&gt;/app/apps/backend/node_modules&lt;/code&gt; is to make it so &lt;code&gt;node_modules&lt;/code&gt; in the backend workspace within the container is excluded from the bind mount &lt;code&gt;./apps/backend:/app/apps/backend&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Because of that volume, simply running &lt;code&gt;docker compose build &amp;lt;service&amp;gt;&lt;/code&gt; or &lt;code&gt;docker compose up -d --build &amp;lt;service&amp;gt;&lt;/code&gt; won't work like you expect it to when development dependencies for &lt;code&gt;backend&lt;/code&gt; change.&lt;/p&gt;

&lt;p&gt;There are two ways I've found to reliably recreate containers  when deps change, while dealing with anonymous volumes:&lt;/p&gt;

&lt;h4&gt;
  
  
  1 The -V, --renew-anon-volumes flag
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;docker compose up -d -V --build &amp;lt;service&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You don't need to stop or restart the service.&lt;/p&gt;

&lt;h4&gt;
  
  
  2 rm container and build
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;docker compose rm -f -s &amp;lt;service&amp;gt; &amp;amp;&amp;amp; docker compose up -d --build &amp;lt;service&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Either of these methods will do the trick.&lt;/p&gt;

</description>
      <category>turborepo</category>
      <category>docker</category>
      <category>node</category>
      <category>monorepo</category>
    </item>
    <item>
      <title>NestJS + Prisma + PostgreSQL ~ RLS multi-tenancy using nestjs-prisma, nestjs-cls and Prisma Client Extensions</title>
      <dc:creator>Nathan Cook</dc:creator>
      <pubDate>Thu, 11 May 2023 23:33:00 +0000</pubDate>
      <link>https://forem.com/moofoo/nestjspostgresprisma-multi-tenancy-using-nestjs-prisma-nestjs-cls-and-prisma-client-extensions-ok7</link>
      <guid>https://forem.com/moofoo/nestjspostgresprisma-multi-tenancy-using-nestjs-prisma-nestjs-cls-and-prisma-client-extensions-ok7</guid>
      <description>&lt;p&gt;(&lt;a href="https://github.com/moofoo/nestjs-prisma-postgres-tenancy" rel="noopener noreferrer"&gt;Github repository for this article&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Here's a simple way to do multi-tenancy in NestJS with Prisma using Client Extensions and AsyncLocalStorage (via nestjs-cls). &lt;/p&gt;

&lt;p&gt;This post describes the basics of a &lt;em&gt;generic&lt;/em&gt; NestJS implementation. Look at &lt;a href="https://github.com/moofoo/nestjs-prisma-postgres-tenancy" rel="noopener noreferrer"&gt;this repo&lt;/a&gt; for a complete and functional example app. Checkout branch &lt;a href="https://github.com/moofoo/nestjs-prisma-postgres-tenancy/tree/async-hooks" rel="noopener noreferrer"&gt;&lt;code&gt;async-hooks&lt;/code&gt;&lt;/a&gt; to see the AsyncLocalStorage-based implementation (which relates to this post). The repo also demonstrates how to use request-scoped providers (&lt;a href="https://github.com/moofoo/nestjs-prisma-postgres-tenancy" rel="noopener noreferrer"&gt;&lt;code&gt;main&lt;/code&gt; branch&lt;/a&gt;) and durable request-scoped providers (&lt;a href="https://github.com/moofoo/nestjs-prisma-postgres-tenancy/tree/durable" rel="noopener noreferrer"&gt;&lt;code&gt;durable&lt;/code&gt; branch&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Step #1: Setup AsyncLocalStorage using nestjs-cls in your bootstrap function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// main.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ClsMiddleware&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nestjs-cls&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;bootstrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// init app...&lt;/span&gt;

  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ClsMiddleware&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TENANT_ID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tenant_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;use&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// etc&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step #2: Add &lt;code&gt;ClsModule.forRoot({ global:true })&lt;/code&gt; to your App Module (app.module.ts) imports.&lt;/p&gt;

&lt;p&gt;Step #3: Create a file that exports a custom Factory Provider that returns an extended Prisma client.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// prisma-tenancy.provider.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PrismaModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PrismaService&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nestjs-prisma&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ClsService&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nestjs-cls&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useFactory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PrismaService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ClsService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$extends&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;$allModels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;$allOperations&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tenantId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TENANT_ID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;


                    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$transaction&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
                        &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$executeRaw&lt;/span&gt;&lt;span class="s2"&gt;`SELECT set_config('tenancy.tenant_id', &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, TRUE)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="p"&gt;]);&lt;/span&gt;

                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ExtendedTenantClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;useFactory&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TENANCY_CLIENT_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Symbol&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TENANCY_CLIENT_TOKEN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PrismaTenancyClientProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;provide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TENANCY_CLIENT_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;PrismaModule&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;PrismaService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ClsService&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;useFactory&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step #4: Create a module that exports the above Factory Provider&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// prisma-tenancy.module.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Global&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PrismaTenancyClientProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TENANCY_CLIENT_TOKEN&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./prisma-tenancy.provider&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PrismaModule&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nestjs-prisma&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Global&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;PrismaModule&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;PrismaTenancyClientProvider&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;TENANCY_CLIENT_TOKEN&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PrismaTenancyModule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step #5: Add the module above to your root &lt;code&gt;app.module.ts&lt;/code&gt; imports.&lt;/p&gt;

&lt;p&gt;Now, you can inject the extended client using the token for your custom provider (&lt;code&gt;TENANCY_CLIENT_TOKEN&lt;/code&gt;, in this case).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Inject&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@nestjs/common&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;TENANCY_CLIENT_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;ExtendedTenantClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./prisma-tenancy.provider&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SomeService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TENANCY_CLIENT_TOKEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExtendedTenantClient&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="c1"&gt;// etc&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Useful Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions" rel="noopener noreferrer"&gt;Prisma Client Extensions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/prisma/prisma-client-extensions/tree/main/row-level-security" rel="noopener noreferrer"&gt;Prisma Client Extensions RLS Example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions/query" rel="noopener noreferrer"&gt;Prisma Query Extension&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.prisma.io/docs/concepts/components/prisma-client/transactions" rel="noopener noreferrer"&gt;Prisma Transactions and batch queries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access#executeraw" rel="noopener noreferrer"&gt;Prisma Raw database access&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/ddl-rowsecurity.html" rel="noopener noreferrer"&gt;Postgres Row Security Policies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.nestjs.com/fundamentals/custom-providers#factory-providers-usefactory" rel="noopener noreferrer"&gt;NestJS Custom Factory Provider&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.nestjs.com/recipes/async-local-storage" rel="noopener noreferrer"&gt;NestJS Recipe: AsyncLocalStorage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.nestjs.com/recipes/prisma" rel="noopener noreferrer"&gt;NestJS Recipe: Prisma&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Papooch/nestjs-cls" rel="noopener noreferrer"&gt;nestjs-cls&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nestjs-prisma.dev/" rel="noopener noreferrer"&gt;nestjs-prisma&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>prisma</category>
      <category>postgres</category>
      <category>node</category>
      <category>nestjs</category>
    </item>
    <item>
      <title>Introducing DNV - Frictionless Node development with Docker Compose</title>
      <dc:creator>Nathan Cook</dc:creator>
      <pubDate>Tue, 03 Aug 2021 00:23:21 +0000</pubDate>
      <link>https://forem.com/moofoo/introduction-dnv-frictionless-node-development-with-docker-compose-bea</link>
      <guid>https://forem.com/moofoo/introduction-dnv-frictionless-node-development-with-docker-compose-bea</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx1n0vx9jfv3zpl7zjuf5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx1n0vx9jfv3zpl7zjuf5.png" alt="Alt Text" width="800" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hello! If you're a Node developer who uses Docker Compose, you should check out &lt;a href="https://www.npmjs.com/package/dnv" rel="noopener noreferrer"&gt;DNV&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;DNV works behind the scenes to keep dependencies in your Docker container in-sync with your local project.&lt;/p&gt;

&lt;p&gt;It also comes with a custom made, featureful ncurses-like UI designed for use when developing apps using Docker Compose.&lt;/p&gt;

&lt;p&gt;This was my 'covid isolation' personal project to learn more about Node fundamentals, and I think it turned out pretty good! If you do check it out and run into any problems, please &lt;a href="https://github.com/moofoo/dnv" rel="noopener noreferrer"&gt;create an issue&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Here's a showcase recorded using Asciinema which shows how you can shell in to containers, install and run programs with full interactivity, open the README.md for your dependencies, show a live 'metrics' display with CPU, memory and event loop stats of the Node process, view a list of and run .sh scripts and script entries from your package.json in the container, and more!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://asciinema.org/a/429194" rel="noopener noreferrer"&gt;DNV Showcase&lt;/a&gt;&lt;/p&gt;


&lt;div class="ltag_asciinema"&gt;
  
&lt;/div&gt;


</description>
      <category>docker</category>
      <category>node</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
