shouldenough

DevOpsLearn Docker by Dockerizing Your App

Lesson 7 of 10

Lesson 07/13 minutes/2 graded

Build your own image

Seven lines of Dockerfile turn a Node app into an image, and three of those lines are the only ones that change for another language.

So far we have run other people's images. Now the app you wrote becomes one, so it can go to a server the same way nginx came to your laptop.

To build an image, Docker needs a recipe: what to start from, what to copy in, what to install, what to run. You write that recipe in a file called Dockerfile, in the root of your project.

The app I am using

Two files, deliberately boring. A package.json with one dependency:

{
  "name": "node-app",
  "version": "1.0.0",
  "main": "src/server.js",
  "scripts": { "start": "node src/server.js" },
  "dependencies": { "express": "^5.1.0" }
}

And src/server.js, which answers one request:

const express = require("express");

const app = express();

app.get("/", (request, response) => {
  response.send("Welcome to my awesome app");
});

app.listen(3000, () => {
  console.log("app listening on port 3000");
});

Run it on your machine with npm install and node src/server.js and it works. Our job is to get that same thing happening inside a container.

The Dockerfile, line by line

FROM node:24-alpine

WORKDIR /app

COPY package.json .
COPY src ./src

RUN npm install

CMD ["node", "src/server.js"]

Seven lines, and every one of them answers a question.

FROM picks the image you build on top of, called the base image. My app needs node and npm, so I start from the official node image, and 24-alpine gives me Node 24 on Alpine, a small Linux. Java would start from a Java image, Python from a Python image. Base images are ordinary images, so you can browse their tags on Docker Hub exactly like nginx.

WORKDIR is cd for the image. It sets /app as the folder everything after this happens in, and it creates the folder if it is not there.

COPY moves files from your machine into the image. The first argument is your side, the second is inside the image. Nothing from your project is in the image unless you copy it, which is worth remembering the first time your app cannot find a file.

RUN executes a command while the image is being built. npm install reads the package.json we just copied and installs express into the image, so the container will not need to download anything when it starts.

CMD is the command that runs when a container starts from this image. Not during the build, at start up. That is the whole difference between RUN and CMD, and it is the one people trip over.

Build it

docker build -t node-app:1.0 .

-t names the image, in name:tag form, and you will see why the tag matters in the next lesson. The dot at the end is the folder holding the Dockerfile, and forgetting it is the most common mistake with this command.

Docker prints one step per line in your Dockerfile:

STEP 1/6: FROM node:24-alpine
STEP 2/6: WORKDIR /app
STEP 3/6: COPY package.json .
STEP 4/6: COPY src ./src
STEP 5/6: RUN npm install

added 67 packages, and audited 68 packages in 5s

STEP 6/6: CMD ["node", "src/server.js"]

That npm output is npm running inside the image as it is built. docker images now lists node-app with tag 1.0. Mine came out at 180 MB, and the node base image it sits on is 169 MB, so my app and express account for about 11 MB of that.

Run your own image

Nothing new here, which is the nice part. Your image behaves like any other:

docker run -d -p 3000:3000 node-app:1.0

The app listens on 3000 inside the container, because that is what server.js says, and I bound the same number on my side. Open http://localhost:3000 and there it is: "Welcome to my awesome app". Ask for the logs and you get the line the app printed on startup:

app listening on port 3000

Your code, running inside a container, on a machine where you never installed Node.

If your app is not JavaScript

Three lines change, and the shape stays identical.

LineJavaScriptPythonJava
FROMnode:24-alpinepython:3.13-alpineeclipse-temurin:21-jre
RUNnpm installpip install -r requirements.txtbuild the jar, then copy it
CMDnode src/server.jspython app.pyjava -jar app.jar

Pick the base image for your language, install with your language's tool, start with your language's command. The Dockerfile does not care which one you use.

Your turn

~

Build an image from the Dockerfile in the current folder, named node-app with tag 1.0.

Knowledge check2 questions
Question 1 of 2
What does WORKDIR do?