Build a Node.js App
So now let's dig into some more advance things you can do with a Dockerfile. Let's first make our project a real Node.js application. Make a file called index.js
and put this in there.
const http = require("http");
http
.createServer(function(request, response) {
console.log("request received");
response.end("omg hi", "utf-8");
})
.listen(3000);
console.log("server started");
This more-or-less that most barebones Node.js app you can write. It just responds to HTTP traffic on port 3000. Go ahead and try running it on your local computer (outside of Docker) by running node index.js
. Hit localhost:3000 to give it a shot.
Okay, so let's get this running inside Docker now. First thing is we have to copy this file from your local file system into the container. We'll use a new instruction, COPY
. Modify your Dockerfile to say:
FROM node:12-stretch
COPY index.js index.js
CMD ["node", "index.js"]
This will copy your index.js file from your file system into the Docker file system (the first index.js is the source and the second index.js is the destination of that file inside the container.)
We then modified the CMD
to start the server when we finally do run the container. Now run
docker build -t my-node-app .
docker run my-node-app
Now your Node.js app is running inside of a container managed by Docker! Hooray! But one problem, how do we access it? If you open locahlost:3000 now, it doesn't work! We have to tell Docker to expose the port. So let's do that now. Stop your container from running and run it again like this.
Try stopping your server now. Your normal CTRL+C won't work. Node.js itself doesn't handle SIGINT (which is what CTRL+C is) in and of itself. Instead you either have to handle it yourself inside of your Node.js code (preferable for real apps) or you can tell Docker to handle it with the --init
flag. This uses a package called tini to handle shutdown signal for you.
docker run --init --publish 3000:3000 my-node-app # or you can use -p instead of --publish
The publish
part allows you to forward a port out of a container to the host computer. In this case we're forwarding the port of 3000
(which is what the Node.js server was listening on) to port 3000
on the host machine. The 3000
represents the port on the host machine and the second 3000
represents what port is being used in the container. If you did docker run --publish 8000:3000 my-node-app
, you'd open localhost:8000
to see the server (running on port 3000
inside the container).
Next, let's organize ourselves a bit better. Right now we're putting our app into the root directory of our container and running it as the root user. This both messy and unsafe. If there's an exploit for Node.js that get released, it means that whoever uses that exploit on our Node.js server will doing so as root which means they can do whatever they want. Ungood. So let's fix that. We'll put the directory inside our home directory under a different users.
FROM node:12-stretch
USER node
COPY index.js /home/node/code/index.js
CMD ["node", "/home/node/code/index.js"]
The USER
instruction let's us switch from being the root user to a different user, one called "node" which the node:12-stretch
image has already made for us. We could make our own user too using bash commands but let's just use the one the node image gave us. (More or less you'd run RUN useradd -ms /bin/bash lolcat
to add a lolcat user.)
Notice we're now copying inside of the user's home directory. This is because they'll have proper permissions to interact with those files whereas they may not if we were outside of their home directory. You'll save yourself a lot of permission wrangling if you put it in a home directory. But we'll have to add a flag to the COPY
command to make sure the user owns those files. We'll do that with --chown=node:node
where the first node
is the user and the second node
is the user group.
It's no big deal that the "code" directory doesn't exist, COPY
will create it.
A Quick Note on COPY vs ADD
The two commands COPY
and ADD
do very similar things with a few key differences. ADD
can also accept, in addition to local files, URLs to download things off the Internet and it will also automatically unzip any tar files it downloads or adds. COPY
will just copy local files. Use COPY
unless you need to unzip something or are downloading something.
Great. Let's make everything a bit more succint by setting a working directory
FROM node:12-stretch
USER node
WORKDIR /home/node/code
COPY index.js .
CMD ["node", "index.js"]
WORKDIR
works as if you had cd
'd into that directory, so now all paths are relative to that. And again, if it doesn't exist, it will create it for you.
Now we just tell COPY
to copy the file into the same directory. Now we're giving it a directory instead of a file name, it'll just assume we want the same name. You could rename it here if you wanted.