After deploying a new version of my application in Docker,
I see my console
having the following error that break my application:
Uncaught SyntaxError: Unexpected token '<'
In this screenshot, the source that is missing is called: 10.bbfbcd9d.chunk.js
, the content of this file looks like:
(this.webpackJsonp=this.webpackJsonp||[]).push([[10],{1062:function(e,t,n){"use strict";var r=n(182);n.d(t,"a",(function(){return r.a}))},1063:function(e,t,n){var ...{source:Z[De],resizeMode:"cover",style:[Y.fixed,{zIndex:-1}]})))}))}}]);
//# sourceMappingURL=10.859374a0.chunk.js.map
This error happens because :
Docker
image that only include chunks from the latest versionChunks are
.js
file that are produced bywebpack
, see code splitting for more information
Reloading the application will update the version to latest, but it still breaks the app for all users that use an outdated version.
A possible fix I have tried consisted of refreshing the application. If the requested chunk was missing on the server, I was sending a reload signal if the request for a .js
file ended up in the wildcard route.
Wild card is serving the
index.html
of the web application, this for delegating routing to client-side routing in case of an user refreshing it's page
// Handles any requests that don't match the ones above
app.get('*', (req, res) => {
// prevent old version to download a missing old chunk and force application reload
if (req.url.slice(-3) === '.js') {
return res.send(`window.location.reload(true)`);
}
return res.sendFile(join(__dirname, '../web-build/index.html'));
});
This appeared to be a bad fix especially on Google Chrome for Android, I have seen my app being refreshed in an infinite loop. (And yes, that is also an ugly fix!)
Since it's not a reliable solution for my end users, I am looking for another way to reload the application if the user client is outdated.
My web application is build using webpack
, it's exactly as if it was a create-react-app
application, the distributed build directory is containing many .js
chunks files.
These are some possible fix I got offered on webpack issue tracker, some were offered by the webpack creator itself:
import()
errors and reload. You can also do it globally by patching __webpack_load_chunk__
somewhere. <= I don't get that patch or where to use import()
, I am not myself producing those chunks and it's just a production featurewindow.location.reload(true)
for not existing js files, but this is a really weird hack. <= it makes my application reload in loop on chrome android.js
requests, even if they don't exist, this only leads to weird errors <= that is not fixing my problemRelated issues
How can I implement a solution that would prevent this error?
If I understood the problem correctly then there are several approaches to this problem and I will list them from the simplest one to more complicated:
This is by far the simplest approach which only requires to change base image for you new version.
Consider the following Dockerfile
to build version2 of the application:
FROM version1
RUN ...
Then build it with:
docker build -t version2 .
This approach, however, has a problem - all old chunks will be accumulating in newer images. It may or may not desirable, but something to take into consideration.
Another problem is that you can't update you base image easily.
Multistage builds allow you to run multiple stages and include results from each stage into your final image. Each stage may use different Docker images with different tools, e.g. GCC to compile some native library, but you don't really need GCC in your final image.
In order to make it work with multi-stage build you would need to be able to create the very first image. Let's consider the following Dockerfile
which does exactly that:
FROM alpine
RUN mkdir -p /app/latest && touch /app/latest/$(cat /proc/sys/kernel/random/uuid).chunk.js
It creates a new new Docker image with a new chunk with random name and put's it into directory named latest
- this is important with proposed approach!
In order to create subsequent versions, we would need a Dockerfile.next
which looks like this:
FROM version2 AS previous
RUN rm -rf /app/previous && mv /app/latest/ /app/previous
FROM alpine
COPY --from=previous /app /app
RUN mkdir -p /app/latest && touch /app/latest/$(cat /proc/sys/kernel/random/uuid).chunk.js
In a first stage it rotates version by removing previous
version, and moving latest
into previous
.
During the second stage, it copies all versions there are left in the first stage, creates a new version and puts it into latest
.
Here's how to use it:
docker build -t image:1 -f Dockerfile .
>> /app/latest/99cfc0e6-3773-40a0-82d4-8c8643cc243b.chunk.js
docker build -t image:2 --build-arg PREVIOUS_VERSION=1 -f Dockerfile.next .
>> /app/previous/99cfc0e6-3773-40a0-82d4-8c8643cc243b.chunk.js
>> /app/latest/2adf34c3-c50c-446b-9e85-29fb32011463.chunk.js
docker build -t image:3 --build-arg PREVIOUS_VERSION=2 -f Dockerfile.next
>> /app/previous/2adf34c3-c50c-446b-9e85-29fb32011463.chunk.js
>> /app/latest/2e1f8aea-36bb-4b9a-ba48-db88c175cd6b.chunk.js
docker build -t image:4 --build-arg PREVIOUS_VERSION=3 -f Dockerfile.next
>> /app/previous/2e1f8aea-36bb-4b9a-ba48-db88c175cd6b.chunk.js
>> /app/latest/851dbbf2-1126-4a44-a734-d5e20ce05d86.chunk.js
Note how chunks are moved from latest
to previous
.
This solution requires your server to be able to discover static files in different directories, but that might complicate local development, thought this logic might be conditional based on environment.
Alternatively you could copy all files into a single directory when container starts. This can be done in ENTRYPOINT
script in Docker itself or in your server code - it's completely up to you, depends on what is more convenient.
Also this example looks at only one version back, but it can be scaled to multiple versions by a more complicated rotation script. For example to keep 3 last versions you could do something like this:
RUN rm -rf /app/version-0; \
[ -d /app/version-1 ] && mv /app/version-1 /app/version-0; \
[ -d /app/version-2 ] && mv /app/version-2 /app/version-1; \
mv /app/latest /app/version-2;
Or it can be parameterized using Docker ARG
with the number of versions to keep.
You can read more about multi-stage builds in the official documentation.