node.jsdockerdockerfilerazzle

Node serves empty static files in a SSR when dockerized


I have a sample application with razzle, react and typescript. I am trying to dockerize it and run it as a container, but although the container runs, the static files are empty.

This is my Dockerfile

FROM node:alpine
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

ENV NODE_ENV production
ENV RAZZLE_CUSTOM_VARIABLE xxxxx

COPY package.json .

RUN npm install --production

COPY build ./build

EXPOSE 3000

CMD [ "node", "build/server.js" ]

I create the image with docker build --tag=my-app:0.0.1 .

I get the following warning (not sure if related to my issue):

SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories.

And finally I run my container with for example: docker run -p 3001:3000 my-app:0.0.1 and it executes properly.

When I go to localhost:30001 I can see my react application but without styles. For example, I can see a http://localhost:3003/static/css/bundle.b1e53d9a.css is being served, but it's empty also the http://localhost:3003/static/js/bundle.fd36658a.js is empty and that must be the issue. The question is Why?

If I do a node build/server.js locally on my filesystem I can see the served react app on localhost:30001 working properly with styles.

Am I missing something or doing something wrong?

I am way too new with docker and node js to figure it out myself.

UPDATE 1: In case it's needed this is my package.json

{
  "name": "llevatelo-web",
  "version": "0.1.0",
  "license": "MIT",
  "scripts": {
    "start": "razzle start",
    "build": "razzle build",
    "test": "razzle test --env=jsdom",
    "start:prod": "NODE_ENV=production node build/server.js"
  },
  "dependencies": {
    "express": "4.17.1",
    "razzle": "3.0.0",
    "react": "16.9.0",
    "react-dom": "16.9.0",
    "react-router-dom": "5.0.1"
  },
  "devDependencies": {
    "@testing-library/jest-dom": "^4.0.0",
    "@testing-library/react": "^8.0.9",
    "@types/express": "^4.17.0",
    "@types/jest": "^24.0.17",
    "@types/react": "^16.9.0",
    "@types/react-dom": "^16.8.5",
    "@types/react-router-dom": "^4.3.4",
    "@types/webpack-env": "^1.14.0",
    "eslint": "^6.1.0",
    "razzle-plugin-typescript": "^3.0.0",
    "ts-jest": "^24.0.2",
    "typescript": "^3.5.3"
  },
  "jest": {
    "transform": {
      "\\.(ts|tsx)$": "ts-jest",
      "\\.css$": "<rootDir>/node_modules/razzle/config/jest/cssTransform.js",
      "^(?!.*\\.(js|jsx|css|json)$)": "<rootDir>/node_modules/razzle/config/jest/fileTransform.js"
    },
    "testMatch": [
      "<rootDir>/src/**/__tests__/**/*.(ts|js)?(x)",
      "<rootDir>/src/**/?(*.)(spec|test).(ts|js)?(x)"
    ],
    "moduleFileExtensions": [
      "ts",
      "tsx",
      "js",
      "json"
    ],
    "collectCoverageFrom": [
      "src/**/*.{js,jsx,ts,tsx}"
    ]
  }
}


Solution

  • You should build your app on the image instead of building it on windows and copying it.

    You can optimize the size later with multi stage builds

    Full Dockerfile :

    FROM node:alpine as builder
    RUN mkdir -p /app
    WORKDIR /app
    ENV NODE_ENV production
    
    COPY package.json ./
    RUN npm install --production
    
    COPY src ./src
    COPY public ./public
    RUN npm run build
    
    
    FROM node:alpine
    RUN mkdir -p /app
    WORKDIR /app
    ENV NODE_ENV production
    
    COPY --from=builder /app/node_modules ./node_modules
    COPY --from=builder /app/build ./build
    
    EXPOSE 3000
    
    CMD [ "node", "build/server.js" ]
    

    With the multi stage build, the image size drops from 290MB to 267MB for the razzle sample app.

    Fully tested on windows, assets are served correctly.