DEV Community

Cover image for Docker setup for yarn workspaces
Siddharth Venkatesh
Siddharth Venkatesh

Posted on • Updated on

Docker setup for yarn workspaces

Introduction

As monorepos seem to be having their moment in the developer community right now, we can see quite a bit of new monorepo tools popping up. npm recently announced npm workspaces with version 7, Nx has been gaining a lot popularity and lerna has been around for quite a while now. I use yarn in most of my projects now, and thought it would be fun to explore yarn workspaces with a simple monorepo setup.

In this workspace, I am going to be adding two React applications. Further, we can also add docker support to make it easier for deployments. Let's get started.

Initialising the workspace

Let's start by creating a folder for our project and initialise yarn

mkdir yarn-docker-setup
cd yarn-docker-setup
yarn init -p
Enter fullscreen mode Exit fullscreen mode

If you do not have yarn installed already, you can install by npm install yarn -g.

After filling out basic questions, you would have a package.json file.

To turn this project into a workspace, we need to add workspaces option in our package.json

"workspaces": ["apps/*"]
Enter fullscreen mode Exit fullscreen mode

apps is a directory where all our apps live.
Great! We've initialised our workspace, next step is to add applications.

Adding apps

We're going to be adding two React applications to this project namely admin and product. I'm using Create React App to scaffold our apps.

yarn create react-app apps/admin
yarn create react-app apps/product
Enter fullscreen mode Exit fullscreen mode

This would take a couple of minutes to finish and by the end you would have two folders called admin and product inside the apps folder.

Great! We've added two apps to our workspace. The next step is let yarn know about each app's dependencies, so it can optimise and cache them. In the project root folder, run

yarn install
Enter fullscreen mode Exit fullscreen mode

This goes through the dependencies and moves them to a central node_modules folder in the project's root.

Let's test out our setup to see everything works. Let's add scripts in our package.json to start and build our apps

"scripts": {
    "admin": "yarn workspace admin start",
    "product": "yarn workspace product start",
    "build:admin": "yarn workspace admin build",
    "build:product": "yarn workspace product build"
}
Enter fullscreen mode Exit fullscreen mode

We've also added build scripts to compile our apps into static files.
If we run yarn admin or yarn product, we should see the standard create react app screen
image

Adding Docker support

Docker provides us with a simple and effective way to package our apps into images that could be run anywhere without any dependence on the environment or operating system. With docker-compose, we can orchestrate multiple services(apps) with a simple configuration. Going too much into docker and docker-compose maybe a bit out of reach for this article, so let's dive into the docker setup.

First step is add a Dockerfile. We can add individual Dockerfiles for each app, but since the build process is same for both the apps, we can use a single Dockerfile for both of them.

First, we need a node environment to compile our React projects, and we need the name of the folder which we need to build, in this case admin or product. We get that using the BUILD_CONTEXT argument.

FROM node:14.17.1 as build
ARG BUILD_CONTEXT
Enter fullscreen mode Exit fullscreen mode

The next step is to copy over the source code into the image.

WORKDIR /base
COPY package.json .
COPY yarn.lock .
COPY ./apps/$BUILD_CONTEXT/package.json apps/$BUILD_CONTEXT/
RUN yarn install
Enter fullscreen mode Exit fullscreen mode

We are defining /base as our working directory. All our code goes here.
In the next 3 lines, we are copying package.json, yarn.lock and the package.json file of the particular app into the image.
Then we run yarn install to install our dependencies.

Interesting thing to note here is, we could have copied our entire source code into the container in one go. The reason we don't do that is, every instruction in a Dockerfile is cached in the background. By copying just the package.json and yarn.lock files, we can take advantage of this caching system. These files rarely change in the course of the project, so if we install our dependencies once, and if they don't change the next time we build, Docker will use the existing cache and not run yarn install every-time we build. This will significantly reduce our build times.

The next step is to copy the app's code and build.

COPY ./apps/$BUILD_CONTEXT apps/$BUILD_CONTEXT
RUN yarn build:$BUILD_CONTEXT
Enter fullscreen mode Exit fullscreen mode

Great, as of now our Dockerfile looks like this

FROM node:14.17.1 as build
ARG BUILD_CONTEXT

WORKDIR /fe
COPY package.json .
COPY yarn.lock .
COPY ./apps/$BUILD_CONTEXT/package.json apps/$BUILD_CONTEXT/
RUN yarn install
COPY ./apps/$BUILD_CONTEXT apps/$BUILD_CONTEXT
RUN yarn build:$BUILD_CONTEXT
Enter fullscreen mode Exit fullscreen mode

Our compilation step is complete. Our React app has been compiled into static files and they are inside the image. But order to serve them, we need a web server. We could use node as our web server as we are already using it for building. But a node image is significantly bigger(close to a gigabyte) in size compared to a traditional web server like nginx.

We'll add nginx configuration as part our build step in our Dockerfile.

FROM nginx:stable-alpine
ARG BUILD_CONTEXT
COPY --from=build /fe/apps/$BUILD_CONTEXT/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Enter fullscreen mode Exit fullscreen mode

The first two lines are self-explanatory.
The third line is where is gets interesting. If you see the first line of our Dockerfile, it says as build next to our node version. This is done so we can refer to this as context in later parts of our build steps.
We have our compiled React app in the node image. We need to take those files and put it in our nginx image. That's what this line does. It copies the /fe/apps/$BUILD_CONTEXT/build folder from build context into /usr/share/nginx/html.
The last line is to start our nginx web server.

The next step is to define an nginx.conf config file nginx can use to run our app, which looks like this. This is a barebones nginx web server configuration which can be used for any frontend application.

server {

  listen 80;

  location / {
    root   /usr/share/nginx/html;
    index  index.html index.htm;
    try_files $uri $uri/ /index.html;
  }

  error_page   500 502 503 504  /50x.html;

  location = /50x.html {
    root   /usr/share/nginx/html;
  }

}
Enter fullscreen mode Exit fullscreen mode

Our entire Dockerfile now looks like this

#build
FROM node:14.17.1 as build
ARG BUILD_CONTEXT

WORKDIR /base
COPY package.json .
COPY yarn.lock .
COPY ./apps/$BUILD_CONTEXT/package.json apps/$BUILD_CONTEXT/
RUN yarn install
COPY ./apps/$BUILD_CONTEXT apps/$BUILD_CONTEXT
RUN yarn build:$BUILD_CONTEXT

#webserver
FROM nginx:stable-alpine
ARG BUILD_CONTEXT
COPY --from=build /base/apps/$BUILD_CONTEXT/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Enter fullscreen mode Exit fullscreen mode

This setup is enough for us to build a Docker image of our app and run by running

docker run <image-name> -e BUILD_CONTEXT=admin/product
Enter fullscreen mode Exit fullscreen mode

We want to go a bit further and add in an orchestration step using docker-compose

For this, we need to add a docker-compose.yml file in the root of our project.

version: '3'

services:
  admin:
    container_name: admin
    build:
      context: .
      dockerfile: Dockerfile
      args:
        - BUILD_CONTEXT=admin
    ports:
      - '8080:80'
  product:
    container_name: product
    build:
      context: .
      dockerfile: Dockerfile
      args:
        - BUILD_CONTEXT=product
    ports:
      - '8082:80'

Enter fullscreen mode Exit fullscreen mode

We define two services here, admin and product for our two apps.
In our service section, we define three properties, container_name, build and ports.

  • container_name defines the name of the container
  • context in build refers to the directory this build needs to be executed on, dockerfile refers to the name and location of the Dockerfile and args refer to build time arguments. These are the arguments that will be used in the Dockerfile ARG section
  • ports lets us map ports on the host machine to the container port. Value 8082:80 indicates that any request on port 8082 on host machine will be routed to port 80 on the container.

Awesome! We are done with our docker-compose setup. Final thing left to do is run and see for ourselves.

docker-compose build
Enter fullscreen mode Exit fullscreen mode

command is used to build out both our apps. This will compile our app using instructions from our Dockerfile and create an image.

To run these images,

docker-compose up
Enter fullscreen mode Exit fullscreen mode

This command will take our images and create containers and run them.

Now we can go to http://localhost:8080 and http://localhost:8082 to see our apps in action.

Conclusion

What we have now is a very simple implementation of workspace and docker setup. We can use this as a starting point and start adding backend services and component libraries to this setup.

If you are interested in setting up a component library from scratch, check out my article on Setting up a component library with React, TypeScript and Rollup

We can add new projects into the apps folder and yarn would take care of the dependency resolutions for us.

The source code for this setup can be found here

Cheers!

Discussion (5)

Collapse
jonlauridsen profile image
Jon Lauridsen • Edited

Thanks, I enjoy reading monorepo setups, there are many valid approaches, and little details and pitfalls.

In this case, how would you use local libraries? If your app requires several local libraries out of many.

Also, a small note, I believe docker-compose has been folded into docker compose. It doesn’t really matter to your article, but sometimes it’s nice to install one less tool :)

Collapse
sidv93 profile image
Siddharth Venkatesh Author

Hey,
The simplest way I can think of to a local library to the app is to add the library as dependency in the app's package.json and make sure the library is compiled before you start the app.
I've added this step in the repo
If you see the package.json scripts section, I'm just adding a build step for lib before starting/building the apps.

"scripts": {
    "admin": "yarn build:lib && yarn workspace admin start",
    "product": "yarn build:lib && yarn workspace product start",
    "build:admin": "yarn build:lib && yarn workspace admin build",
    "build:product": "yarn build:lib && yarn workspace product build",
    "build:lib": "yarn workspace lib build"
  }
Enter fullscreen mode Exit fullscreen mode

I believe we can extend this approach for multiple libraries.

Not sure if this is the best approach, but it works :)

Collapse
bk_973 profile image
Benjamin Kalungi

Great read

Collapse
sidv93 profile image
Siddharth Venkatesh Author

Thank you!

Collapse
floukna profile image
floukna • Edited

Thanks guys, but I got some errors while docker-compose build. How to fix it.

dev-to-uploads.s3.amazonaws.com/up...