Learn Docker With My Newest Course

Dive into Docker takes you from "What is Docker?" to confidently applying Docker to your own projects. It's packed with best practices and examples. Start Learning Docker →

Using Phoenix with Webpack and Docker

blog/cards/using-phoenix-with-webpack-and-docker.jpg

Phoenix 1.4 is set to use Webpack but Phoenix 1.3 currently uses Brunch. Here's how to get Webpack working with Phoenix 1.3.

Quick Jump: Here's What We'll Cover in This Article | Setting up Webpack with Phoenix | Final Thoughts

I recently started a new Phoenix project and I found myself wanting to use SASS along with ES6 Javascript. I’ve used Webpack before but not Brunch.

I also happen to be a big fan of using Docker, so it’s no surprise that I wanted to get everything working with Docker.

So I set out to create a development environment where I could just run 1 command and have my Phoenix server along with a Webpack watcher and PostgreSQL all start up. That part was easy enough with Docker Compose.

But it was pretty tricky wiring everything up together, so this article explains it all and even includes a working example app that you can use as a reference.

The main focus of this article will be on getting Webpack working with Phoenix. The Docker bits are all optional.

Here’s What We’ll Cover in This Article

  • Generate a Phoenix application using the --no-bunch flag
  • Create a new /asset folder to store your front-end source code
  • Use Yarn instead of NPM for a better package management experience
  • Set up a package.json file to install everything we need
  • Configure Webpack for:
    • SASS / Autoprefixer
    • ES6 Javascript
    • File loader for images and fonts
    • Minification in production
    • CSS files will be output to their own bundle
    • Bundle an app.ccs and app.js file
  • Create example SCSS and JS files to test it out
  • Configure Phoenix to start Webpack
  • Make sure everything works
  • Go over a few Docker specific things

Setting up Webpack with Phoenix

The main focus of this article is on getting Phoenix and Webpack to work. It just so happens everything is ran through Docker (which is optional by the way).

Generating a Demo Application

The only thing we’ll need to change when generating a new application is to disable Brunch, which we can do by running mix phx.new --no-brunch hello.

The Docker example app will take care of installing Phoenix’s dependencies for you, but if you’re not using Docker you’ll want to let Phoenix install the dependencies for you.

Creating a New Asset Folder for Our Source Files

Technically you can put this anywhere you want but I think an assets/ folder that is at the same depth as your lib/ folder is a reasonable spot for this.

Feel free to change this later if you want but keep in mind if you do change this you will need to update a few paths across a couple of files.

Using Yarn and Installing NodeJS

I’m not going to walk through these steps, especially since it’ll be different for every environment. This is partly why I like using Docker by the way.

Just make sure you npm install yarn in your Node environment.

Setting up a package.json File

Create a package.json file inside of your assets/ folder and then add this:

{
  "repository": {},
  "dependencies": {
    "phoenix": "~1.3",
    "phoenix_html": "~2.10"
  },
  "devDependencies": {
    "autoprefixer": "~8.5",
    "babel-core": "~6.26",
    "babel-loader": "~7.1",
    "babel-preset-es2015": "~6.24",
    "css-loader": "~0.28",
    "extract-text-webpack-plugin": "~3.0",
    "file-loader": "~1.1",
    "imports-loader": "~0.8",
    "node-sass": "~4.9",
    "postcss-loader": "~2.1",
    "precss": "~1.3",
    "sass-loader": "~7.0",
    "style-loader": "~0.21",
    "webpack": "~3.10",
    "webpack-merge": "~4.1"
  },
  "scripts": {
    "build": "NODE_ENV=production /node_modules/webpack/bin/webpack.js -p",
    "watch": "NODE_ENV=development /node_modules/webpack/bin/webpack.js --config /app/assets/webpack.config.js --progress --color --watch"
  }
}

I’ll be keeping these versions synced up to what’s included in the example repo.

The above files assumes you’ll have everything installed using my Docker example app as a reference, but if you don’t plan to use Docker then you’ll want to adjust the paths to webpack in the scripts section of the file.

If you decide not to use Docker, you would most likely end up removing the leading / since node_modules would be relative to the current directory.

If you’re not using Docker this is where you’ll want to run yarn install too.

Configuring Webpack:

Create a webpack.config.js file inside of your assets/ folder and then add this:

var ExtractTextPlugin = require("extract-text-webpack-plugin");
var merge = require("webpack-merge");
var webpack = require("webpack");

var env = process.env.NODE_ENV || "development";
var production = env === "production";

var node_modules_dir = "/node_modules"

var plugins = [
  new ExtractTextPlugin("css/app.css")
]

if (production) {
  plugins.push(
    new webpack.optimize.UglifyJsPlugin({
      compress: {warnings: false},
      output: {comments: false}
    })
  );
} else {
  plugins.push(
    new webpack.EvalSourceMapDevToolPlugin()
  );
}

var common = {
  watchOptions: {
    poll: true
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: [node_modules_dir],
        loader: "babel-loader",
        options: {
          presets: ["es2015"]
        }
      },
      {
        test: /\.scss$/,
        use: ExtractTextPlugin.extract({
          fallback: 'style-loader',
          use: [
            {
              loader: 'css-loader',
            },
            {
              loader: 'postcss-loader',
              options: {
                plugins() {
                  return [
                    require("precss"),
                    require("autoprefixer")
                  ];
                }
              }
            },
            {
              loader: 'sass-loader'
            }
          ]
        })
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        loader: "file-loader?name=/images/[name].[ext]"
      },
      {
        test: /\.(ttf|otf|eot|svg|woff2?)$/,
        loader: "file-loader?name=/fonts/[name].[ext]",
      }
    ]
  },
  plugins: plugins
};

module.exports = [
  merge(common, {
    entry: [
      __dirname + "/app/app.scss",
      __dirname + "/app/app.js"
    ],
    output: {
      path: __dirname + "/../priv/static",
      filename: "js/app.js"
    },
    resolve: {
      modules: [
        node_modules_dir,
        __dirname + "/app"
      ]
    }
  })
];

The basic idea here is, if you make a change to app.scss or app.js then Webpack is going to run its course and then produce its output in the usual priv/static directory.

That’s happening because there’s a line near the bottom that sets path: __dirname + "/../priv/static",. That ensures all of our bundles get output there. In which case, Phoenix will pickup the changes automatically because that’s where it expects assets to live.

If you’re using the Docker example app you won’t need to change anything.

If you don’t plan to use Docker then you’ll need to make a few changes:
  1. Change the node_modules_dir variable to point to your node_modules directory
  2. Change the 3 /app references near the bottom of the config to ./
  3. Remove the watchOptions poll option (we need this for Docker support)

Creating Example SCSS and JS Files

What I like to do is create an app/ folder inside of the assets/ folder and this is where all of our source assets will exist.

So inside of that app/ folder, create both an app.scss and app.js file.

We can leave them as empty files for now. Our goal is to have everything compile and run properly, and then we can change them later to test it out.

Configuring Phoenix to Start Webpack

If you use the Docker example app then you won’t need to do this step, but in case you’re not using Docker, it would be pretty nice if Phoenix started Webpack for you automatically.

That can be done by going to your config/dev.exs file and then under your app’s EndPoint config, change the watchers list to look like this:

watchers: [yarn: ["run", "watch", cd: Path.expand("../assets", __DIR__)]]

That yarn command matches up to what we have defined in our package.json file.

I don’t run Phoenix outside of Docker, so I’m not sure how well this is going to work out in practice. I like running Phoenix and Webpack as separate processes because if I need to restart the Phoenix server, I wouldn’t necessarily want to restart the Webpack watcher.

Let me know how it goes for you in the comments.

Starting Everything up

If you’re using the Docker set up you’ll just want to run docker-compose up --build and wait a few minutes while everything builds for you automatically.

If you’re not using the Docker example app you’ll want to run mix phx.server, and possibly yarn run watch in a different terminal if you don’t do the above step to integrate Webpack as a watcher with Phoenix.

What you’re looking for here would be to have both Phoenix and Webpack successfully running in the foreground.

Your Webpack output should look something like this:
10% [0] building modules 1/1 modules 0 active
Webpack is watching the files…

Compiling 13 files (.ex)

Version: webpack 3.10.0
Child
  Hash: fe7feb13ffe07e2b4bd8
  Time: 2508ms
           Asset     Size  Chunks             Chunk Names
       js/app.js   3.5 kB       0  [emitted]  main
     css/app.css  0 bytes       0  [emitted]  main
        [0] multi ./app/app.scss ./app/app.js 40 bytes {0} [built]
        [1] ./app/app.scss 41 bytes {0} [built]
        [2] ./app/app.js 13 bytes {0} [built]

I’ve removed some of the output to reduce the noise here, but the important thing is you shouldn’t see any errors. If you do, double check all of your paths.

Testing Everything Out

If you’re using the Docker example app then you won’t be able to see these changes in your browser because the default layout template just outputs a simple hello world message.

But, you should see the files get successfully compiled on the Webpack end.

Making a Javascript change:

Open up your assets/app/app.js file and then add console.log("Hello!) and save it.

You should see a Webpack message that looks similar to this:

Child
    Hash: e8374bafa3fc5db3dbb8
    Time: 15ms
          Asset     Size  Chunks             Chunk Names
      js/app.js  3.75 kB       0  [emitted]  main
      css/app.css  0 bytes       0  [emitted]  main
       [2] ./app/app.js 37 bytes {0} [built]
       + 2 hidden modules

Not too shabby. It only took 15ms for it to be updated, and in my case, that’s going through a Docker volume.

Making a SCSS change:

Open up your assets/app/app.scss file and then add body { color: #3f0; } and save it.

You should see a Webpack message that looks similar to this:

 Child
     Hash: cdcc0eb35bdf6305add7
     Time: 44ms
           Asset      Size  Chunks             Chunk Names
     css/app.css  24 bytes       0  [emitted]  main
      + 1 hidden asset
        [1] ./app/app.scss 37 bytes {0} [built]
         + 2 hidden modules

Even when I use Bootstrap 4 and a custom theme with a massive amount of SCSS it’s still quite fast. One theme that I use pulls in 10,000+ lines of SCSS and the whole thing compiles in 2.8 seconds using Docker on Windows.

Going over a Few Docker Specific Things

If you look through the example app, you might be wondering what’s up with a few design choices I made on the Webpack and Docker side of things.

Dealing with the mix.lock file:

In a previous article I wrote about handling lock files. The TL;DR is without doing a strategy that’s similar to what I did in the ENTRYPOINT script then your lock file will never be created on your Docker host.

This same approach also works with the yarn.lock file.

Polling for file changes in the Webpack config:

Unfortunately Webpack and Docker doesn’t get along for when it comes to detecting file changes through Docker for Windows. This might be different with Docker for Mac, but the Windows file system will not pass through file changes to the container.

So to combat that, the polling option was turned on in the watch options so it works for everyone out of the box. Feel free to comment it out and try it on your end.

Using the Webpack watcher instead of the dev server:

I’ve tried using the webpack-dev-server and even set up a custom Express app along with Webpack’s middleware to compare its performance to what we’re doing here which is just writing the assets to disk.

When you’re dealing with Docker volumes, it makes no difference since we’re dealing with disk access. I benchmarked all 3 strategies and the compile speed was exactly the same.

Setting up a tmpfs volume mount in the Docker Compose file:

I’ll admit, I’m not really sure if this has any extra benefits, such as creating less writes to disk (which could be handy if you have an SSD).

I benchmarked a regular volume mount and the tmpfs driven volume mount which according to Docker makes the mount live in memory instead of hitting the disk and the performance was the same with both set ups.

I left the tmpfs option in there because it doesn’t seem to hurt anything and if it does produce less disk writes, that’s a good thing. If anyone knows the real answer behind this, please let me know in the comments.

Final Thoughts

I’m really happy that Phoenix 1.4 will be using Webpack instead of Brunch by default.

I’m personally not a big fan of creating SPA style Javascript applications. I’m more of a “sprinkle JS when needed” type of person while leveraging server side templates and Turbolinks, but I do rely on a lot of SCSS and some JS to power my apps and Webpack is quite popular and well supported.

One cool thing about all of what we went over today is it should all work fine when Phoenix 1.4 is released. If anything does change, I’ll update the example app and this article.

Were you able to get everything working? Let me know below.

Free Intro to Docker Email Course

Over 5 days you'll get 1 email per day that includes video and text from the premium Dive Into Docker course. By the end of the 5 days you'll have hands on experience using Docker to serve a website.



Comments