Mounting Docker Volume On Nuxt Generate Directory

February 11th 2020

If you followed along with the tutorial from last week on Mutli Stage Docker Builds for Nuxt Generate we walked through installing a basic Nuxt.js project, and setting up a Dockerfile which could be used to build an image that would run the nuxt generate command for our project, copy the /dist files over to the root directory of our nginx web server and start our nginx server to serve the static content generated by our nuxt generate command. We where then able to build the image and run the docker images (as containers) which executed our run time command to start nginx and serve our content. Following along with the tutorial the command to run the image (aka create a container) was:

docker run --name basic_nuxt --rm -d -p 3333:80 nuxt:nginx

This starts a container named basic_nuxt and binds the host port 3333 to the exposed container port of 80 (which is the default port that nginx listen’s on). This works great, but we run into an issue when we make a change in our project and re-run the nuxt generate command, since our files are only copied over once at build time, the new files are not mounted on our nginx root directory and will not show the changes. In order to view the changes we would have to stop our container and restart the container to view the changes. This can get annoying if we are making changes, so the logical step would be to mount a volume to the nginx root directory. What’s a volume you might be wondering? According to the Docker Docs:

Volumes are the preferred mechanism for persisting data generated by and used by Docker containers.

docs.docker.com

This sounds like what we need as our container would persist the data that is generated to our /dist folder and serve the changes in our mounted directory, and in doing so we won’t need to restart our container!

At first glance I thought this would be easy. Since our nginx root directory in our container was located at /usr/share/nginx/html I thought we could just bind our volume on to this directory such as:

docker run --name basic_nuxt --rm -d -p 3333:80 -v ${pwd}/dist:/usr/share/nginx/html:ro nuxt:nginx

This works fine the first time, until we run the nuxt generate command again and if you reload the browser at http://localhost:3333 you would expect to see the changes that were generated in our /dist folder of our project but instead you see a big, blank 404 page 🤔

The reason this is happening is because the nuxt generate command removes the /dist folder and rebuilds it, and since it’s removed the reference to the docker volume is lost and nginx no longer recognizes the path to the directory so it serves a 404 error. What to do, what to do?

The solution as presented in this github issue would be to set the configuration for the /dist folder in nuxt generate to be a subdirectory of a parent docker folder (ie docker/dist ) and then bind the volume to the ${pwd}/docker folder instead of the dist folder the reference to the volume wouldn’t be lost and nginx would be able to serve the updated contents of dist. In order to do that we would need to change the server root directory in our nginx.conf file to point to /usr/share/nginx/html/dist instead of /usr/share/nginx/html/ and we would also need to change our Dockerfile to copy files from our Stage 1 build at /usr/src/app/docker to /usr/share/nginx/html/ (which copied the docker folder and all files/subdirectories into /usr/share/nginx/html). Let’s start with making edits to our nuxt.config file and then we’ll jump over to edit our Dockerfile and finally show you how to configure nginx.conf. In our nuxt.config.js file we need to add a generate property like:

export default {
  generate: {
    dir: 'docker/dist'
  },
}

We can confirm this is working by running yarn generate and you should see a docker/dist folder is generated in our project. Now onto our Dockerfile. To refresh your memory here is the old Dockerfile:

### STAGE 1: Build ###
FROM node:latest as build
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
ENV PATH /usr/src/app/node_modules/.bin:$PATH
COPY package.json /usr/src/app/package.json
RUN yarn install --silent
COPY . /usr/src/app
RUN yarn run generate

### STAGE 2: NGINX ###
FROM nginx:stable-alpine
COPY --from=build /usr/src/app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

We need to make a subtle change in our Dockerfile to make this work:

### STAGE 1: Build ###
FROM node:latest as build
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
ENV PATH /usr/src/app/node_modules/.bin:$PATH
COPY package.json /usr/src/app/package.json
RUN yarn install --silent
COPY . /usr/src/app
RUN yarn run generate

### STAGE 2: NGINX ###
FROM nginx:stable-alpine
COPY --from=build /usr/src/app/docker /usr/share/nginx/html/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Note the COPY command in STAGE 2 changed. This will copy everything under our ./docker folder (including the dist folder) into /usr/share/nginx/html/. Perfect, we’re almost there. Now we just need to edit our nginx.conf file. First, where is our nginx file and how do we edit it? Well at run time our STAGE 2 will download the nginx image from Docker Hub which includes the nginx.conf file at /etc/nginx/nginx.conf so we need to create a file in our local (host) directory and either copy that over at build time or we can mount it as a volume during run time (when a container is started). Since we’re going to mount a volume at runtime (for our ${pwd}/docker folder we can mount the volume for the nginx.conf file as well at run time. Either way works but in this tutorial we’re choosing the later. Regardless, we need to create an nginx.conf file in our host directory. I would recommend downloading the nginx.conf file from the nginx repo on github: https://github.com/nginx/nginx/blob/master/conf/nginx.conf

Now that we have that we need to make an edit in our nginx.conf. Open that up in your editor of choice and scroll down to the server block. Here we’ll edit the location / block such that it will be:

server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   /usr/share/nginx/html/dist;
            index  index.html index.htm;
        }
        ...
}

Note the line: root /usr/share/nginx/html/dist; which tells nginx that our root directory is now the dist folder on not the html folder. Perfect with that out of the way all we need to do is rebuild our image and then change the docker run command to mount our volumes. You can either remove the nuxt:nginx image that we created and regenerate it or you can create a new image name. I’m going to remove and regenerate:

docker rmi nuxt:nginx && docker image prune

Then regenerate it. To do so make sure your in the project directory with the Dockerfile in it:

docker build -t nuxt:nginx .

Now that our image is built we are ready to start our container. If you remember the previous run command was:

docker run --name basic_nuxt --rm -d -p 3333:80 nuxt:nginx

Let’s tweak that to mount our volume and nginx.conf file:

docker run --name basic_nuxt \
--rm -d -p 3333:80 \
-v $(pwd)/docker:/usr/share/nginx/html/:ro \
-v $(pwd)/nginx.conf:/etc/nginx/nginx.conf \
nuxt:nginx

I broke this into multiple lines for readability. The changes that we’re looking at are -v $(pwd)/docker:/usr/share/nginx/html/:ro and -v $(pwd)/nginx.conf:/etc/nginx/nginx.conf which are just mounting our ./docker directory to the nginx root directory and mounting the local nginx.conf to the containers nginx.conf file. We can navigate to http://localhost:3333/ and you will see our basic nuxt project. Now make some changes in pages/index.vue and save and run yarn generate then navigate back over to http://localhost:3333/ and you will see the changes persisted in our container! Hot dang, that’s cool! There you have it. Until next time, stay curious, stay creative!