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.
| Line | JavaScript | Python | Java |
|---|---|---|---|
FROM | node:24-alpine | python:3.13-alpine | eclipse-temurin:21-jre |
RUN | npm install | pip install -r requirements.txt | build the jar, then copy it |
CMD | node src/server.js | python app.py | java -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.