Buildchains In Docker: Advanced Setup

Having introduced the basics of running a buildchain in docker and explaining the many merits that such a system provides, I wanted to touch on some more advanced uses. Hopefully this should provide enough practical info to cover the majority of JS based buildchain requirements.

I'm going to start from where we left off in the previous article so have a read through that first if you need to get up to speed.

Gulp

Previously we built a buildchain image which used simple npm scripts to run binaries which would compile our assets. This is great until we want to start doing complicated things like generating image sprite sheets or building a pipeline of multiple tools to process our files.

A common tool to automate such build processes is Gulp, and it has been my go-to solution for CSS and JS compilation for several years now. I feel like it strikes the right balance between utility and complexity and doesn't require a PhD to be able to grok its config files.

We'll start by removing all of the watch and build stuff in docker-config/buildchain/package.json so that it looks more like this:

{
  "name": "docker-buildchain",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
  },
  "devDependencies": {
    "@babel/core": "^7.2.0",
    "babel-cli": "^6.26.0",
    "babel-preset-es2015": "^6.24.1",
    "node-sass": "^4.10.0"
  }
}

Next we'll add gulp and gulp-sass to our container and package.json:

docker-compose run buildchain yarn add gulp gulp-sass --dev

Now that that's installed we can create our base gulpfile. Add the following to docker-config/buildchain/gulpfile.js:

var gulp = require('gulp');
var sass = require('gulp-sass');

function css() {
  return gulp
    .src("./src/assets/css/app.scss")
    .pipe(sass({ outputStyle: "compressed" }))
    .pipe(gulp.dest("./src/web/assets/css/"));
}

const build = gulp.parallel(css);

exports.css = css;
exports.build = build;
exports.default = build;

We've just defined a css compilation task with appropriate inputs and outputs along with an additional wrapper task called build which will be responsible for running all of our sub-tasks in the correct order.

We need to copy this file into our image. Add the following COPY to docker-config/buildchain/Dockerfile after the yarn install command. While you're in there, let's also update our default image command:

COPY ./docker-config/buildchain/gulpfile.js gulpfile.js

CMD ./node_modules/.bin/gulp build

We're running gulp via the pre-compiled binary which is included with the package so we don't need to bother doing any global installs.

We'll also need to change the command in our docker-compose.json to run gulp. We haven't added any 'watch' capabilities yet so lets just up it to:

command: ['./node_modules/.bin/gulp', 'build']

That should be all we need to get started. Rebuild your buildchain image and then run it. It should execute the sass task and output a compiled css file in your src/web/assets/css directory.

Next we'll add our file watching capability back into the mix. Gulp supports this out of the box so all we need to do is update our gulpfile.js as follows:

var gulp = require('gulp');
var sass = require('gulp-sass');

function css() {
  return gulp
    .src("./src/assets/css/app.scss")
    .pipe(sass({ outputStyle: "compressed" }))
    .pipe(gulp.dest("./src/web/assets/css/"));
}

function watchFiles() {
  gulp.watch("./src/assets/css/**/*", css);
}

const build = gulp.parallel(css);
const watch = gulp.parallel(watchFiles);

exports.css = css;
exports.build = build;
exports.watch = watch;
exports.default = build;

We've added a watch task and an associated function which is just watching our precompiled css files.

We'll also need to update our docker-compose.yml to use our watch task as the buildchain command when we're working locally:

command: ['./node_modules/.bin/gulp', 'watch']

The gulpfile can now be worked on in order to add whatever functionality you need to compile your assets. I'd recommend starting by adding plumber to prevent errors in your compilation tasks from killing your watch tasks.

BrowserSync

With our current setup our assets are rebuilt whenever we make changes to them, but we have to press Cmd+R in order to refresh the browser in order to see what effect it's had. This is way too much work.

We can use BrowserSync to eliminate this annoyance.

docker-compose run buildchain yarn add browser-sync --dev

And update docker-config/buildchain/gulpfile.js as follows:

var gulp = require('gulp');
var sass = require('gulp-sass');
var browsersync = require("browser-sync").create();

function css() {
  return gulp
    .src("./src/assets/css/app.scss")
    .pipe(sass({ outputStyle: "compressed" }))
    .pipe(gulp.dest("./src/web/assets/css/"))
    .pipe(browsersync.stream());
}

function watchFiles() {
  gulp.watch("./src/assets/css/**/*", css);
}

function browserSync(done) {
  browsersync.init({
    proxy: {
      target: 'nginx:80',
	  proxyOptions: {
	    changeOrigin: false
	  }
    },
    port: 3000
  });
  done();
}

const build = gulp.parallel(css);
const watch = gulp.parallel(watchFiles, browserSync);

exports.css = css;
exports.build = build;
exports.watch = watch;
exports.default = build;

Here we're creating a browserSync task which is executed whenever we run our watch task. The browserSync task starts an instance of browsersync with a few configuration options which tell it to proxy any incoming requests to our nginx container. We then tell BrowserSync to listen on port 3000.

What we've done here is essentially create a reverse proxy inside our buildchain container, so now we can send requests to our buildchain and BrowserSync will proxy them to our project's nginx container whilst also adding a few additional bits of functionality.

We've also added an extra line to our css task which instructs BrowserSync to stream any modified files to any browsers which are currently connected to it.

In order to try this out we need to make sure that we can send traffic to our buildchain reverse proxy. In docker-compose.yml add these port bindings to our buildchain container:

buildchain:
      image: registry.gitlab.com/mattgrayisok/temp-test/buildchain:latest
      ports:
          - 3000:3000
          - 3001:3001
      ...

Rebuild our images and then bring up the whole project using docker-compose up. You should see BrowserSync output something like the following in the docker logs:

buildchain_1  | [17:47:24] Using gulpfile /project/gulpfile.js
buildchain_1  | [17:47:24] Starting 'watch'...
buildchain_1  | [17:47:24] Starting 'watchFiles'...
buildchain_1  | [17:47:24] Starting 'browserSync'...
buildchain_1  | [17:47:24] Finished 'browserSync' after 30 ms
buildchain_1  | [Browsersync] Proxying: http://nginx:80
buildchain_1  | [Browsersync] Access URLs:
buildchain_1  |  --------------------------------------
buildchain_1  |        Local: http://localhost:3000
buildchain_1  |     External: http://192.168.240.2:3000
buildchain_1  |  --------------------------------------
buildchain_1  |           UI: http://localhost:3001
buildchain_1  |  UI External: http://localhost:3001
buildchain_1  |  --------------------------------------

And now you can visit http://localhost:3000 to see your project which is being proxied through BrowserSync.

Ensure that your src/web/assets/css/app.css file is being included in your base Craft template file and then try editing src/assets/css/app.scss. You should see the changes reflected in your browser without having to refresh.

You can also add functionality to your gulpfile to force a browser refresh whenever a template file is edited or when javascript files are changed.

It's also worth exploring http://localhost:3001 which will show you the BrowserSync dashboard where you can play with a few settings like network throttling.

Also, because we've bound BrowserSync to our host's port 3000 you can also view our project using additional devices. You'll just need to find the local IP address of your host and connect to port 3000 on that IP on any other devices which are connected to the same LAN. For instance http://192.168.0.23:3000. This allows you to synchronise changes and interactions across multiple devices simultaneously.

Webpack

I have a confession. This isn't going to happen.

I began writing this article with every intention to cover a Gulp development workflow followed closely by a Webpack comparison. But the more I played with getting my tutorial project set up to compile css, js and images whilst integrating HMR, code splitting and all the other buzzwords in a stand-alone buildchain container, the more I hated everything about it.

Webpack added more complexity to my development environment than everything else combined.

So I threw in the towel. And tried Parcel instead 🤣 But that was worse because of its prescriptive entry and exit points. Then, when I defined more than one entry point it just refused to compile some things at all! I drew a line in the sand and stood on the side that supported my long-term mental health.

I would like, at this point, to refer you to nystudio's epic Webpack 4 post: https://nystudio107.com/blog/a...

Firstly because it has a ton of Craft+Webpack information, secondly because it gets you off my back, but also for one particular quote:

Ulti­mate­ly, you just need to decide where in the pyra­mid of fron­tend tech­nolo­gies you want to stand.

After some internal deliberation I have decided to stand at the level of "Please, no more JavaScript build tools!"

I will stick to my nice, simple Gulp workflow. Where I declare a set of easily parsable instructions and my laptop acquiesces.

Web used to be fun. IMHO JavaScript build tools have ruined it. I really feel bad for new developers enrolled in code camps these days... It must be like learning to read with only Shakespeare as reference: beautiful, but nobody knows what the fuck is going on.

</rant>

But! I am not going to leave you empty handed. The Gulp workflow above covers the general concepts you need to get set up with most build tools.

  • Mount your project files into your buildchain container.
  • Create some scripts which define your entry points, processes and destination points and copy them into the container.
  • Set your Dockerfile to perform a production build.
  • Set your docker-compose.yml to 'watch' for file changes to help with local development.
  • If you want to do HMR or live reload make sure your build tool is set to proxy requests to your nginx container or only proxy your assets through the buildchain container by referencing them on a separate port (I.E <script src="http://localhost:8888/app.js">).

I'm sorry I couldn't be of more assistance. If you need me, you'll find me erasing node_modules wherever I can find them. Maybe someone with a little more tenacity will pick up this particular torch...

✌️


Read Next



2024 Goals
Write Things