Docker containers are a popular way to deploy applications because they provide a consistent runtime environment regardless of the host system. However, when you need to update a file inside a container, it's often necessary to rebuild the entire container, which can be time-consuming and disruptive to your deployment process.

Docker solves our problem of running an application anywhere we want without relying on external dependencies. All our dependencies are pre-packaged into the docker image, so running a container of the image without issues is easy.

Though, a limitation that we may hit as we go on is a hot-reload feature in a local environment. Normally when we save a file and open it, the latest updates can be viewed. But once an image is built in a docker system, the contents cannot be changed, and we can't view the latest update without creating a new image. This is actually the original principle of Docker. We'll circumvent this by using docker volumes, which will help us update files inside the container while it runs.

Pre-Requisites

  • Docker
  • NodeJS (V18 preferred)

New to Docker? Check out Docker for Beginners


Implementation

Using the docker command

  • For single-container workloads

Using a Docker volume is one way to update files inside a container without rebuilding it. A volume is a separate storage area mounted into a container, allowing you to share files between the host system and the container.

To use a volume, first, create a volume using the following command.

docker volume create myvolume

Then, start a container and mount the volume using the command below.

docker run -v myvolume:/path/to/mount myimage

Now, any changes you make to the files inside the mounted directory on the host system will be reflected in the container. For example, you could edit a file on the host system, and the changes would be visible inside the container.

Using the docker-compose command

  • For multi-container workloads

Another way to update files inside a container without rebuilding it is to use Docker Compose. Docker Compose is an extension of Docker itself, which is used for creating multi-container Docker applications and running them in any Docker-supported systems.

To use Docker Compose, first define your application in a YAML file. Also, specify the containers needed and any volumes or other configuration options.

For example:

version: '3'
services:
	myservice:
    	image: myimage
        volumes:
        	- myvolume:/path/to/mount
volumes:
	myvolume:

Docker Compose Files

Then, start the containers using the following command.

docker-compose up

The Unorthodox Method - Rebuilding a Container

Another rarely used way is by rebuilding a container. The volume workaround is perfectly sufficient for our uses. Rebuilding our container is also an option if, by chance, the volume method does not work.

The COPY command can be used to copy files from the host system into the container. To use the COPY command, add the following line to your Dockerfile:

COPY /path/on/host /path/in/container

When you build the container, the files from the host system will be copied into the container. If you need to update the files, you can simply rebuild the container, and the new files will be copied.

ℹ️
The rebuilding approach is less efficient than using volumes, as it requires rebuilding the entire container each time you update the files.

Also read: Using Traefik Reverse Proxy for Docker Containers


Hot Reload in Docker Container - Demo

For this demo, let's set up an express-based nodejs application.

Creating the app

Create a project directory where we will store our files.

mkdir -p projects/docker-reload
npm init -y
Wrote to /private/tmp/test/package.json:

{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Output of npm init -y

Let's install our dependencies using the command below.

npm i express nodemon

Create a file called hello.js and paste the following.

const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => {
  res.send('Hello World! :)')
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

JS code of hello.js

Running the app

Run the following command to start the app.

npm start
> hello-nodejs@1.0.0 start
> node hello.js

Example app listening on port 3000

Output of npm start

If you go to http://localhost:3000, it should look something like this:

Browser output on opening http://localhost:3000

Implementing live reload

Let's add a script in package.json, so that hello.js is constantly monitored for changes, and any new changes are reflected immediately.

Open the package.json file and the following into the scripts section.

"dev": "nodemon --watch \"./**\" --ext \"js\" --exec \"node hello.js\"",

Script for live reload to be added in package.json

After you add it, the file should look something like this:

"scripts": {
	# nodemon will watch for changes made to the js file
    "dev": "nodemon --watch \"./**\" --ext \"js\" --exec \"node hello.js\"",
    "start": "node hello.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

Complete script section of package.json

At this point, if we make any changes, they won't be reflected in the app. So, execute the following command.

npm run dev
> hello-nodejs@1.0.0 dev
> nodemon --watch "./**" --ext "js" --exec "node hello.js"

[nodemon] 2.0.20
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): **
[nodemon] watching extensions: js
[nodemon] starting `node hello.js`
Example app listening on port 3000

Output of npm run dev

Now, make a change to the hello.js file. I'll change the response message to something more reflective of the current demo.

[nodemon] restarting due to changes...
[nodemon] starting `node hello.js`
Example app listening on port 3000
Changes as seen on opening http://localhost:3000
✍️
Any changes you make on the js file will be reflected. This can be anything ranging from response messages to functions.

Dockerizing

Now that our application is prepared, we can simply dockerize it, and it will be ready for deployment.

Create a Dockerfile and paste the following.

FROM node:18.12.1-alpine
WORKDIR /src/app
COPY package.json package-lock.json .
RUN npm ci
COPY hello.js .
CMD npm run dev

Contents of Dockerfile

Run the following commands to build the image and run it.

  • To build

docker build -t hello-world-node:v1 . -f Dockerfile

  • To run

docker run -v `pwd`:/src/app/ -p 3001:3000 hello-world-node:v1

Run the above command in the project directory. The pwd command will extract our current directory path, and the -v argument will bind our project directory with the app directory inside the container.

ℹ️
The output of the above command is omitted for brevity. Please comment below if you run into any issues while performing the above.

In this article,

we learned to create a nodejs express application, implemented live reloading and subsequently deployed a docker image for it.

Dynamically updating files inside docker containers without rebuilding them is important for containerized application development and deployment. There are a few techniques you can use to accomplish this, including using volumes, Docker Compose, and the COPY command in your Dockerfile. Each technique has advantages and disadvantages, so choose the one that works best for your particular use case.

Thank you for reading! Subscribe using either of the buttons below or leave a comment if you face any difficulties in the process.