New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docker development environment not rebuilding after local changes. #1041

Closed
chickendinosaur opened this Issue Jan 17, 2018 · 6 comments

Comments

Projects
None yet
3 participants
@chickendinosaur
Copy link

chickendinosaur commented Jan 17, 2018

I currently have a container set up with FuseBox for development. I am able to hit localhost:4444 and load the page but when make changes on my local drive nothing rebuilds so even when I refresh the page manually nothing changes.

From past experience after pulling hair for a day or two when I was setting up a live development environment working with Docker + Webpack I eventually learned that the legacyWatch mode that invokes file polling in Webpack had to be turned on for file polling for the container in order to watch files and rebuild. I have a Docker image that works perfect for development using Webpack and it really was only that legactWatch mode if that helps.

The same goes with using nodemon with Docker. You have to create the nodemon config and set legacyWatch to true. It has to do with different file systems.

Also, I do not know how to change the port the WebSocket connects to. It seems to only like port 4444. I changed the port for the dev server and the socket continued to want 4444. No big deal but may arise in the future.

docker-compose.yml to start container and map volume to directory:

version: "3"
services:
  web:
    restart: always
    container_name: test-web
    build:
      context: .
      dockerfile: ./Dockerfile
    ports:
     - 4444:4444
    volumes:
     - .:/usr/app
     - /usr/app/node_modules

Working Dockerfile with node-sass (pain in the ass to figure out so it may help others) built in:

# Download base image.
FROM node:8.9.4-alpine

RUN apk update && \
    apk upgrade && \
    apk add git g++ gcc libgcc libstdc++ linux-headers make python

# install libsass
RUN git clone https://github.com/sass/sassc && cd sassc && \
    git clone https://github.com/sass/libsass && \
    SASS_LIBSASS_PATH=/sassc/libsass make && \
    mv bin/sassc /usr/bin/sassc && \
    cd ../ && rm -rf /sassc

# created node-sass binary
ENV SASS_BINARY_PATH=/usr/lib/node_modules/node-sass/build/Release/binding.node
RUN git clone --recursive https://github.com/sass/node-sass.git && \
    cd node-sass && \
    git submodule update --init --recursive && \
    npm install && \
    node scripts/build -f && \
    cd ../ && rm -rf node-sass

# add binary path of node-sass to .npmrc
RUN touch $HOME/.npmrc && echo "sass_binary_cache=${SASS_BINARY_PATH}" >> $HOME/.npmrc

ENV SKIP_SASS_BINARY_DOWNLOAD_FOR_CI true
ENV SKIP_NODE_SASS_TESTS true

# Create app directory.
RUN mkdir -p /usr/app

# Set initial command-line directory.
WORKDIR /usr/app

# Install app dependencies.
COPY package.json /usr/app/
RUN npm install

# Copy app source to container.
COPY . /usr/app

# Expose port.
EXPOSE 9000

VOLUME ["/usr/app"]

CMD ["npm", "run", "start"]
@nchanged

This comment has been minimized.

Copy link
Contributor

nchanged commented Jan 17, 2018

@chickendinosaur so you have a shared folder, and for some reason, it's not watchable?
You could utilise a manual reload, using watch and fuse.sendPageHMR

@devmondo devmondo added the discussion label Jan 17, 2018

@chickendinosaur

This comment has been minimized.

Copy link

chickendinosaur commented Jan 17, 2018

So instead of:

    fuse
        .bundle("app")
        .instructions("> client.tsx")
        .watch()
        .hmr();

    fuse.dev({
        port: 4444,
        httpServer: true
    });

    fuse.run();

Ditch the .watch() and utilize Sparky manually instead something like this?
Will it be the same efficient rebuild time?
Remember all of this is needs to run from the Docker container.

task("start", (context) => {
        const fuseConfig = context.getConfig();

        fuseConfig
            .bundle("app")
            .instructions("> client.tsx");

        fuseConfig.dev({
            port: 4444,
            httpServer: true
        });

        watch("src/**/*.*").file("*.*", (file) => {
            context.createBundle(fuseConfig);
            fuseConfig.sendPageHMR();
        });

        fuse.run();
    });

Then node fuse start?

Shouldn't this be an option of the dev server for much easier configuration and to ensure consistent setup?:

fuse.dev({
        watchMode: 'daemon',
        port: 4444,
        httpServer: true
    });

When and how to create/hot-update a bundle when manually watching is still confusing since I do not have a full understanding of the inner workings of FuseBox due to having only been using it for two days.

@chickendinosaur

This comment has been minimized.

Copy link

chickendinosaur commented Apr 12, 2018

I still need help getting hmr to work from a docker container.
Please help me so I can use fuse-box and then I have a reason to contribute. Thanks!

fuse config:

'use strict';

import {
    FuseBox,
    WebIndexPlugin,
    QuantumPlugin,
    PostCSSPlugin,
    CSSPlugin,
    BabelPlugin
} from 'fuse-box';

import { task, context, watch } from 'fuse-box/sparky';
import * as postcssNested from 'postcss-nested';

const isProduction = process.env.NODE_ENV === 'production';

/*
CONFIG
*/

context(
    class Context {
        constructor() {
            this.config = FuseBox.init({
                target: 'browser@es6',
                cache: !isProduction,
                hash: isProduction,
                log: true,
                useTypescriptCompiler: true,
                homeDir: 'src',
                output: `public/$name.js`,
                tsConfig: 'tsconfig.json',
                modulesFolder: 'node_modules',
                sourceMaps: {
                    project: !isProduction,
                    vendor: false
                },
                plugins: [
                    [
                        PostCSSPlugin([postcssNested], {
                            sourceMaps: false
                        }),
                        CSSPlugin({
                            outFile: (file) => {
                                return `public/${file}`;
                            },
                            inject: (file) => {
                                return `${file}`;
                            },
                            minify: isProduction
                        })
                    ],

                    // JavaScript optimazations.
                    isProduction &&
                        QuantumPlugin({
                            bakeApiIntoBundle: false,
                            ensureES5: false, // Ensures that all code that is in es5
                            extendServerImport: false, // If you are using dynamic import statements to load remote javascript files. note: Install request library beforehand
                            manifest: true, // Generates manifest.json
                            processPolyfill: false, // Removes "use strict" from the code
                            removeExportsInterop: true, // Removes Object.defineProperty(exports, '__esModule', { value: true });
                            removeUseStrict: true,
                            replaceProcessEnv: true, // Replaces process.env.NODE_ENV with a string identifier "production"
                            replaceTypeOf: true, // Replaces typeof module, typeof exports, typeof window, typeof define, typeof require keywords to corresponding values at build time
                            target: 'browser@es6', // Default value: browser Possible values server, browser, universal, electron, npm
                            treeshake: true,
                            warnings: true,
                            uglify: {
                                es6: true
                            }, // Quantum supports both uglify-js and uglify-es. Which one to use is determined by es6 option.
                            css: {
                                clean: true
                            }
                        }),

                    // Generate index.html.
                    WebIndexPlugin({
                        template: `src/templates/index.html`,
                        appendBundles: true
                    })
                ]
            });
        }

        getConfig() {
            return this.config;
        }
    }
);

/*
TASKS
*/

/* Build */

task('build', async (ctx) => {
    const fuseConfig = ctx.getConfig();

    fuseConfig.bundle('vendor').instructions('~ client.tsx');

    fuseConfig.bundle('app').instructions('!> client.tsx');

    return await fuseConfig.run();
});

task('build:dev', async (ctx) => {
    const fuseConfig = ctx.getConfig();

    fuseConfig.bundle('app').instructions('> client.tsx');

    return await fuseConfig.run();
});

/* Run */

task('start:dev', async (ctx) => {
    const fuseConfig = ctx.getConfig();

    fuseConfig.bundle('app').instructions('> client.tsx');

    fuseConfig.dev({
        port: 9000,
        httpServer: true
    });

    watch('src/**/*.*', {}, (file) => {
        fuseConfig.sendPageHMR();
    });

    return await fuseConfig.run();
});

npm script:

"start:dev": "npm run clean && cross-env NODE_ENV=development nodemon fuse.js start:dev",

Dockerfile:

# Download base image.
FROM node:9.11.1-alpine

# Create app directory.
RUN mkdir -p /usr/app

# Set initial command-line directory.
WORKDIR /usr/app

# Install app dependencies.
COPY package.json /usr/app/
RUN npm install --quiet

# Copy app source to container.
COPY . /usr/app

# Expose port.
EXPOSE 9000

# Set volumes for live editing.
VOLUME ["/usr/app"]

CMD ["npm", "run", "start:dev"]

docker-compose.yml:

version: "3"
services:
  gridplus-takehome-web:
    restart: always
    container_name: gridplus-takehome
    build:
      context: .
      dockerfile: ./Dockerfile.dev
    ports:
     - 9000:9000
    volumes:
     - .:/usr/app

docker start command:

docker-compose -f docker-compose.dev.yml up --build
@nchanged

This comment has been minimized.

Copy link
Contributor

nchanged commented Apr 12, 2018

@chickendinosaur How about if you use chokidar directly to watch the files? Or add some options to it

fuse.run({chokidar : {/*options here for sure that have something that might work*/} })
@chickendinosaur

This comment has been minimized.

Copy link

chickendinosaur commented Apr 12, 2018

@nchanged So I got it working. Thank you! However, there is a caveat. The client only connects when using port 4444 for the dev server. Even if the docker container is mapped using a different port for host:container like 9000:4444 it still requires everything to be set to port 4444 (4444:4444).

Working task:

task('start:dev', async (ctx) => {
    const fuseConfig = ctx.getConfig();

    fuseConfig
        .bundle('app')
        .instructions('> client.tsx')
        .watch()
        .hmr();

    fuseConfig.dev({
        port: 4444,
        httpServer: true,
        root: 'public'
    });

    return await fuseConfig.run({
        chokidar: {
            usePolling: true
        }
    });
});

docker-compose.yml:

version: "3"
services:
  gridplus-takehome-web:
    restart: always
    container_name: gridplus-takehome
    build:
      context: .
      dockerfile: ./Dockerfile.dev
    ports:
     - 4444:4444
    volumes:
     - .:/usr/app

For anyone else that uses bot docker and non-docker environments, here are the task that I made so that I can still take advantage of non-polling environments:

task('init:dev', async (ctx) => {
    const fuseConfig = ctx.getConfig();

    fuseConfig
        .bundle('app')
        .instructions('> client.tsx')
        .watch()
        .hmr();

    fuseConfig.dev({
        port: 4444,
        httpServer: true,
        root: 'public'
    });
});

task('start:dev', ['init:dev'], async (ctx) => {
    const fuseConfig = ctx.getConfig();

    return await fuseConfig.run();
});

task('start-docker:dev', ['init:dev'], async (ctx) => {
    const fuseConfig = ctx.getConfig();

    return await fuseConfig.run({
        // https://github.com/paulmillr/chokidar
        chokidar: {
            persistent: true,

            ignored: '*.txt',
            ignoreInitial: false,
            followSymlinks: true,
            cwd: '.',
            disableGlobbing: false,

            usePolling: true,
            interval: 75,
            binaryInterval: 300,
            alwaysStat: false,
            depth: 99,
            awaitWriteFinish: false,

            ignorePermissionErrors: false,
            atomic: true
        }
    });
});
@nchanged

This comment has been minimized.

Copy link
Contributor

nchanged commented Apr 13, 2018

Awesome, if you don't mind, i'll copy that over to our docs

UPD
59bce47

@nchanged nchanged closed this Apr 13, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment