Logo

dev-resources.site

for different kinds of informations.

Build and Deploy a Monorepo WebSocket web application with Turbo, Express, and Vite on Render Using Docker

Published at
12/31/2024
Categories
webdev
docker
express
vite
Author
Jen C.
Categories
4 categories in total
webdev
open
docker
open
express
open
vite
open
Build and Deploy a Monorepo WebSocket web application with Turbo, Express, and Vite on Render Using Docker

Introduction

Tip: This post only focuses on the build and deployment phase, and the problems and solutions I encountered. For the project itself, feel free to refer to the full repository on my github here.

Prerequisites

  • Install and configure Docker on your local machine and make sure it is running
  • Get a Render account

Steps

First, since I'm using pnpm as the package manager for my project, I followed this instruction Working with Docker
to create and build my Dockerfile.

Dockerfile

FROM node:23-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"

RUN corepack enable
RUN corepack pnpm --version

RUN pnpm add -g serve

FROM base AS build
COPY . /usr/src/app
WORKDIR /usr/src/app

RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm run -r build
RUN pnpm deploy --filter=backend --prod /prod/backend
RUN pnpm deploy --filter=web --prod /prod/web

# backend
FROM base AS backend
COPY --from=build /prod/backend /prod/backend
WORKDIR /prod/backend
EXPOSE 8000
CMD [ "pnpm", "start" ]

# web
FROM base AS web
COPY --from=build /prod/web /prod/web
WORKDIR /prod/web
EXPOSE 5173
CMD [ "serve", "-s", "dist", "-l", "5173" ]

Build Docker images and run Docker containers on local machine

Backend

docker build . --target backend --tag backend:latest

Frontend

docker build . --target web --tag web:latest

Because the client of the WebSocket application needs to connect to the server, and the server needs to allow the cross-domain resource sharing of the WebSocket client, it needs to allow services running on the Docker container to communicate with each other.

At the local machine, create a custom, user-defined network my-net

docker network create -d bridge my-net

Run backend container on the created network

docker run --network=my-net -d -p 8000:8000 backend:latest

Run web container on the created network

docker run --network=my-net -d -p 5173:5173 web:latest

However, due to the limitations of Render's free tier, other solutions than setting up user-defined networks are required.

Deploy the web services on Render

Solution 1: using Infrastructure as code

Refer to Render Blueprints (IaC)

Note: I did not choose this approach because credit card info is required at the moment

services:
  - type: web
    runtime: docker
    name: backend
    envVars:
      - key: SERVER_PORT
        value: "8000"
      - key: CLIENT_URL
        value: "http://frontend.onrender.com"
    dockerCommand: |
      docker build . --target backend --tag backend:latest && \
      docker run -d -p 8000:8000 backend:latest

  - type: web
    runtime: docker
    name: frontend
    envVars:
      - key: VITE_SERVER_URL
        value: "http://backend.onrender.com"
    dockerCommand: |
      docker build . --target web --tag web:latest && \
      docker run -d -p 5173:5173 web:latest

Solution 2: Divide the Dockerfile into 2 parts, respectively for backend and frontend

Since Render will automatically run the docker build based on the Dockerfile of the target repo, in order to create 2 web services and allow them to communicate with each other, create 2 Dockerfile in each project based on above Dockerfile:

apps/backend/Dockerfile

FROM node:23-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"

RUN corepack enable
RUN corepack pnpm --version

FROM base AS build
COPY . /usr/src/app
WORKDIR /usr/src/app

RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm run -r build
RUN pnpm deploy --filter=backend --prod /prod/backend

FROM base AS backend
COPY --from=build /prod/backend /prod/backend
WORKDIR /prod/backend
EXPOSE 8000
CMD [ "pnpm", "start" ]

apps/web/Dockerfile

FROM node:23-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"

RUN corepack enable
RUN corepack pnpm --version

RUN pnpm add -g serve

FROM base AS build
COPY . /usr/src/app
WORKDIR /usr/src/app

RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm run -r build
RUN pnpm deploy --filter=web --prod /prod/web

FROM base AS web
COPY --from=build /prod/web /prod/web
WORKDIR /prod/web
EXPOSE 5173
CMD [ "serve", "-s", "dist", "-l", "5173" ]

Then follow instructions to deploy these two web services

Note:
Before deployment, set the environment variables here and set the client url and server url of the back-end service and front-end service respectively, as shown below

Set client url as environment variable to backend service
Image description

Set server url as environment variable to client service

Image description

Problems and Solutions

1. Error: tsconfig.json:4:5 - error TS6310: Referenced project '/socket-react-fullstack-monorepo/apps/web/tsconfig.app.json' may not disable emit

Root cause

TBC

Solution

Add "files": [], to apps/web/tsconfig.json

2. Error: ERR_PNPM_LOCKFILE_BREAKING_CHANGE  Lockfile /usr/src/app/pnpm-lock.yaml not compatible with current pnpm

During building docker file RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile

Root cause

Because RUN corepack enable in the Dockerfile downloads pnpm version 8.15.6, but the pnpm-lock.yaml file generated by executing pnpm i is generated by the pnpm package installed locally (through npm install -g pnpm), which is version 9.15.1. So version mismatch will cause errors

Image description

Solution

Change (downgrade) the locally installed pnpm version to match the target version downloaded by corepack in the Dockerfile, which is 8.15.6. and generate a new pnpm-lock.yaml with the target version of pnpm

npm install -g [email protected]

After the installation is complete, check the pnpm version and make sure it shows the correct target version (here 8.15.6)

pnpm --version

Delete the local pnpm-lock.yaml

rm pnpm-lock.yaml

Execute the pnpm install command to generate a new rm pnpm-lock.yaml file

pnpm i

3. Error: After executing the commands pnpm run -r build and pnpm deploy --filter=backend --prod /prod/backend, there is no dist/ folder in /prod/backend

Root cause

Missing files field in package.json

Solution

Add files in apps/backend/package.json, and run pnpm deploy again

 "files": [
    "dist"
  ],

4. Error: Cannot find module 'tslib'

After attempting to run the container using the command

docker run -d -p 8000:8000 backend:latest

Root cause

The package tslib in apps/backend/package.json is in devDependencies, however, according to tslib it should be in dependencies

...

"devDependencies": {
    "@types/cors": "^2.8.17",
    "@types/express": "^4.17.21",
    "@types/node": "^20.14.12",
    "nodemon": "^3.1.4",
    "tslib": "^2.6.3",
    "typescript": "^5.3.3"
  },

...

Solution

Move tslib to dependencies and run pnpm i to generate pnpm-lock.yaml based on the new dependencies in apps/backend/package.json

...

"dependencies": {
    "tslib": "^2.6.3",
    "cors": "^2.8.5",
    "express": "^4.19.2",
    "socket.io": "^4.7.5",
    "ts-node": "^10.9.2",
    "zod": "^3.23.8"
  }

...

5. Error: ERR_PNPM_NO_SCRIPT_OR_SERVER  Missing script start or file server.js

When trying to launch the Vite application using pnpm start in a Docker container

Root cause

There is no start in the script in apps/web/package.json

"scripts": {
    "dev": "vite --clearScreen false",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "lint": "eslint \"src/**/*.ts\""
  },

Solution

Use serve to serve the static files

Install serve globally in Dockerfile

RUN pnpm add -g serve

Change this from

...

CMD [ "pnpm", "start" ]

To

...

CMD [ "serve", "-s", "dist", "-l", "5173" ]

Build and run the Docker container again in detached mode (-d), using the web:latest image to map port 5173 on the host to port 5173 in the container

docker run -d -p 5173:5173 web:latest

Resources

How to debug a running Docker container: using docker container exec

For example, check whether certain environment variables exist in the executing docker container.

List all running docker containers and view the container ID of the target container

docker ps

Allocate a pseudo-TTY of the target container using the container ID (ae980e452cbe here is the container ID)

docker exec -it ae980e452cbe /bin/sh

List all the environment variables

env

Example output


# env             
NODE_VERSION=23.5.0

...

PWD=/prod/backend
PNPM_HOME=/pnpm
# 

Featured ones: