Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Docker + Webpack (Dev Server) + Yarnpkg incomplete builds

Problem

Converting a webpack project that runs locally right now to run inside docker containers. This work takes place in two git branches: develop, and containers.

Local (No Container)

develop is the stable base, which runs locally via $ yarn install && npm run dev given the following in package.json

"scripts": {
    "start": "node .",
    "env:dev": "cross-env NODE_ENV=development",
    "env:prod": "cross-env NODE_ENV=production",
    "predev": "npm run prebuild",
    "dev": "npm run env:dev -- webpack-dev-server",
//[...]
}

The branch develop does include yarn.lock, though FWIW, $ rm yarn.lock && yarn install --force && npm run dev does start up the server correctly, i.e. GET http://localhost:3000 gives me the homepage, as I expect to see it. The above all works the same after $ git checkout containers

Docker

After shutting down the local dev server, I run $ git checkout containers, and this branch does NOT contain the yarn.lock or package.lock. I then run $ docker-compose up --build web (in a separate terminal, in a sibling directory that contains the following in the docker-compose.yaml)

   web:
      build:
       context: ../frontend/
       dockerfile: Dockerfile
      env_file: ../frontend/.env
      volumes:
        - ../frontend/src:/code/src
      ports:
        - "3001:3000"
      depends_on:
        - api
      networks:
        - backend

The frontend/Dockerfile for the service web is like so

# Dockerfile

FROM node:latest
RUN mkdir /code
ADD . /code/
WORKDIR /code/
RUN yarn cache clean && yarn install --non-interactive --force && npm rebuild node-sass

CMD npm run dev --verbose

given

#frontend/.dockerignore

node_modules
deploy
.circleci
stories
.storybook

All seems to go well, and the final line of the startup is web_1 | Server is running at http://localhost:3000/. Yet when I GET http://localhost:3001 (note port mapping in docker-compose), the page that's returned does not contain the expected <style>...</style> tag in the <head> as is supposed to be injected (as far as I understand) by webpack, given the configuration below

// https://github.com/diegohaz/arc/wiki/Webpack
const path = require('path')
const devServer = require('@webpack-blocks/dev-server2')
const splitVendor = require('webpack-blocks-split-vendor')
const happypack = require('webpack-blocks-happypack')
const serverSourceMap = require('webpack-blocks-server-source-map')
const nodeExternals = require('webpack-node-externals')
const AssetsByTypePlugin = require('webpack-assets-by-type-plugin')
const ChildConfigPlugin = require('webpack-child-config-plugin')
const SpawnPlugin = require('webpack-spawn-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')

const {
  addPlugins, createConfig, entryPoint, env, setOutput,
  sourceMaps, defineConstants, webpack, group,
} = require('@webpack-blocks/webpack2')

const host = process.env.HOST || 'localhost'
const port = (+process.env.PORT + 1) || 3001
const sourceDir = process.env.SOURCE || 'src'
const publicPath = `/${process.env.PUBLIC_PATH || ''}/`.replace('//', '/')
const sourcePath = path.join(process.cwd(), sourceDir)
const outputPath = path.join(process.cwd(), 'dist/public')
const assetsPath = path.join(process.cwd(), 'dist/assets.json')
const clientEntryPath = path.join(sourcePath, 'client.js')
const serverEntryPath = path.join(sourcePath, 'server.js')
const devDomain = `http://${host}:${port}/`

//[...]  
const sass = () => () => ({
  module: {
    rules: [
      {
        test: /\.(scss|sass)$/,
        use: [
            { loader: 'style-loader' },
            { loader: 'css-loader' },
            { loader: 'sass-loader'},
            ],
      },
    ],
  },
})

const extractSass = new ExtractTextPlugin({
  filename: 'style.css',
})

const prodSass = () => () => ({
  module: {
    rules: [
      { test: /\.(scss|sass)$/,
        use: extractSass.extract({
          use: [
            { loader: 'css-loader', options: { minimize: true } },
            { loader: 'sass-loader' },
          ],
          fallback: 'style-loader',
        }),
      },
    ],
  },
})

const babel = () => () => ({
  module: {
    rules: [
      { test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader' },
    ],
  },
})

const assets = () => () => ({
  module: {
    rules: [
      { test: /\.(png|jpe?g|svg|woff2?|ttf|eot)$/, loader: 'url-loader?limit=8000' },
    ],
  },
})

const resolveModules = modules => () => ({
  resolve: {
    modules: [].concat(modules, ['node_modules']),
  },
})

const base = () => group([
  setOutput({
    filename: '[name].js',
    path: outputPath,
    publicPath,
  }),
  defineConstants({
    'process.env.NODE_ENV': process.env.NODE_ENV,
    'process.env.PUBLIC_PATH': publicPath.replace(/\/$/, ''),
  }),
  addPlugins([
    new webpack.ProgressPlugin(),
    extractSass,
  ]),
  apiInsert(),
  happypack([
    babel(),
  ]),
  assets(),
  resolveModules(sourceDir),

  env('development', [
    setOutput({
      publicPath: devDomain,
    }),
    sass(),
  ]),

  env('production', [
    prodSass(),
  ]),
])

const server = createConfig([
  base(),
  entryPoint({ server: serverEntryPath }),
  setOutput({
    filename: '../[name].js',
    libraryTarget: 'commonjs2',
  }),
  addPlugins([
    new webpack.BannerPlugin({
      banner: 'global.assets = require("./assets.json");',
      raw: true,
    }),
  ]),
  () => ({
    target: 'node',
    externals: [nodeExternals()],
    stats: 'errors-only',
  }),

  env('development', [
    serverSourceMap(),
    addPlugins([
      new SpawnPlugin('npm', ['start']),
    ]),
    () => ({
      watch: true,
    }),
  ]),
])

const client = createConfig([
  base(),
  entryPoint({ client: clientEntryPath }),
  addPlugins([
    new AssetsByTypePlugin({ path: assetsPath }),
    new ChildConfigPlugin(server),
  ]),

  env('development', [
    devServer({
      contentBase: 'public',
      stats: 'errors-only',
      historyApiFallback: { index: publicPath },
      headers: { 'Access-Control-Allow-Origin': '*' },
      host,
      port,
    }),
    sourceMaps(),
    addPlugins([
      new webpack.NamedModulesPlugin(),
    ]),
  ]),

  env('production', [
    splitVendor(),
    addPlugins([
      new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } }),
    ]),
  ]),
])

module.exports = client

Interestingly, adding this line to package.json

 "dev-docker": "npm run predev && npm run env:dev -- webpack  --progress --watch --watch-poll",

and changing the last line of the Dockerfile to CMD npm run dev-docker does yield the desired effect...

Hypotheses

My current suspicion is that I am missing something about how the webpack dev server handles serving its loader output, and have not mapped some port properly, but that's a shot in the dark.

Alternatively, the webpack-dev-server version is a problem. Local is 4.4.2 where docker's shows 5.6.0, though this seems probably not the issue as the documentation for latest matches my own setup. I've confirmed that the package.json specification for the loader modules is the latest stable on each of them.

Apologia

Recognizing that this is a problem caused by the intersection of several technologies in a config-dependent and necessarily idiosyncratic way, I humbly ask your help in working through this dependency hell. If it seems like I do not understand how a given piece of the puzzle operates, I'm happy to hear it. Any ideas, leads, or suggestions, however tenuous, will be greatly appreciated and exploited to the best of my abilities.

like image 354
Ben Avatar asked Mar 08 '18 19:03

Ben


3 Answers

It's been a while, but coming back to this problem, I found the actual answer.

The webpack-dev-server uses two ports. Thus, in exposing only the one port (3000) I was not getting the built files, which are served in client.js on localhost:3001. The clue was right there the whole time in the JS console: a connection refused error on GET localhost:3001/client.js.

The solution is to expose both ports on the container, i.e. docker run -it -p 3000:3000 -p 3001:3001 --rm --entrypoint "npm run env:dev -- webpack-dev-server" ${CONTAINER_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}

like image 122
Ben Avatar answered Nov 12 '22 16:11

Ben


Long shot here, but I was trying to run a grails-vue app in docker containers and had issues with the port mappings of webpack-dev-server not being exposed properly.

I found this issue on github https://github.com/webpack/webpack-dev-server/issues/547 which led to me adding --host 0.0.0.0 to my dev task in package.json like so:

"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js --host 0.0.0.0"

This solved my problem, maybe this will help you find your answer.

like image 31
Derek Hobden Avatar answered Nov 12 '22 18:11

Derek Hobden


It could be possible that your locally installed packages differ from the packages in the docker container.

To be sure that you have the same packages installed, you should include yarn.lock and package.lock files. If you only use yarn yarn.lock should suffice. Even if this does not solve your specific problem, it can prevent others, because now you have a deterministic build.

like image 1
mbuechmann Avatar answered Nov 12 '22 18:11

mbuechmann