Patrick Desjardins Blog
Patrick Desjardins picture from a conference

How to configure Docker to have a shared folder between two containers?

Posted on: 2022-05-09

In this article, I explain how to configure Docker to have two independent services in two different containers, but both of them reference a shared folder.

repository
   |----> service1
   |----> service2
   |----> shared

The article focus on:

  1. Docker configuration
  2. TypeScript configuration

The challenge with sharing a folder when using a different container is to bring the shared folder into the respective containers and have TypeScript working at design time (inside your developer editor) and at compilation. The solution revolves around the path capability of TypeScript, allowing to create aliases. With aliases, you can import modules from an absolute point instead of a relative path. The relativity issue is to refer to a folder that is outside the container realm. Hence, to refer to a folder outside the Docker's context, we need to use TypeScript path.

TypeScript Configuration

The first step is to go into each service tsconfig.json and add the path. In my case, both services are at the same level: sibling to the shared folder. Thus, they have inside their tsconfig.json the same new configuration:

"baseUrl": "./",
"paths": {
  "@shared/*": ["../shared/*"]
}

There is a caveat in the configuration of paths, and it is that you must have a baseUrl. In both services, the base is the root of the folder. So, what the configuration means is that any time we are using import {xyz} from "@shared/folder/file" that the import will refer to the file from the baseUrl and then apply the @shared.

import {xyz} from "@shared/folder/file"   ---> import {xyz} from "./..shared/folder/file"

Absolute Reference

ViteJS

Once the TypeScript configuration is in place, the next hurdle is to have the code that executes to know what to do with the paths. The generated Javascript remains with the @shared prefix. While TypeScript compilers (build time and design time) can interpret the paths, the browser or Node does not.

One of the services was using ViteJS. There is a plugin for ViteJS that can be used. In my case, it didn't work. So, I had to resolve the path manually. In the vite.config.js, you need to add a resolve. For simplicity, I removed the rest of the configuration so you will have more than the required configuration for the paths. As you can see, the paths in ViteJS is named alias. The configuration is very similar to TypeScript.

export default (conf: any) => {
  return defineConfig({
    resolve: {
      alias: [{ find: "@shared", replacement: path.resolve(__dirname, "../shared") }],
    },
  });
};

If you are using Webpack instead of ViteJS, something similar would be needed.

NodeJS

The other service was a NodeJS Express server. A similar configuration was needed. This time, the configuration is with Node. Here are the two commands I had to modify. One modifies the nodemon, the other one node. In the end, the same idea: rely on a code modifier that will hand the path.

"start:development": "npx tsc -v && npx tsc && concurrently \"npx tsc -w --preserveWatchOutput\" \"npx nodemon --inspect=0.0.0.0:9229 --watch ./build -r ts-node/register/transpile-only -r tsconfig-paths/register ./build/app/src/index.js\"",
"start:production": "node -r ts-node/register/transpile-only -r tsconfig-paths/register build/node/src/index.js"

The fix was to add -r tsconfig-paths/register which will apply the transformation.

Docker

Finally, the shared folder must be included in both Docker containers. Because in development, we want to be able to quickly modify, add or remove code, we want to rely on Docker's volume. However, when we deploy for production, we must copy the shared folder into the container as a sibling to keep the reference intact.

The configuration of the docker-compose.yml maps the local development path ./services/shared to the docker folder /node/shared, which is a sibling to the actual code in /node/app

volumes:
  - "./services/shared:/node/shared:delegated"

The configuration within each DockerFile. Next to the command that copied the service1, a new entry for shared is added.

COPY services/service1 /node
COPY services/shared /shared

Conclusion

Sharing interfaces, classes, types, constants, or logics between a frontend and a backend service is natural. Leveraging the possibility to reuse code because both projects are using the same languages is a huge time saver. Even if Docker divides the two services, it is possible to keep sharing by allowing during development a common volume. Copying the shared folder into each container at production time is the key. Once the configuration of TypeScript and your builder, like ViteJS, the future will be smooth and enjoyable and will foster a great habit of code sharing because the hurdle to execute is reduced to none. You can see an example of the pattern described in this article in my Real Time Pixel repository.

References