Skip to content

Lesson 3 — Containerization with Docker ​

In Lesson 1, you deployed a static website on Netlify and a Node.js/Express API on Render. In Lesson 2, you added automated tests with Vitest and a CI workflow with GitHub Actions.

Your API now runs on your computer, can be tested automatically, and can be deployed to a managed hosting platform. But there is still an important question:

How can you package your application so that it runs with the same runtime, dependencies, and startup command on different computers?

This is where Docker comes in. In this lesson, you will package your existing Express API into a Docker image, run it as a container, and inspect and troubleshoot that container.

Continue with your existing project

Use the deployment-workshop project from Lessons 1 and 2. Keep the static Netlify site, the Render service, your Vitest tests, and your GitHub Actions workflow.

You will run Docker locally in this lesson. You do not need to change your Render deployment or create a DigitalOcean server. Multi-container applications with Docker Compose and MySQL are covered in Lesson 4; cloud deployment using Docker comes later.

Learning outcomes ​

By the end of this lesson, you should be able to:

  1. Explain containerization and distinguish containers from virtual machines.
  2. Identify the roles of Docker images, containers, Dockerfiles, registries, and the Docker Engine.
  3. Write a Dockerfile that packages a Node.js/Express application and installs only its production dependencies.
  4. Build and run a Docker image and make the API accessible through a published port.
  5. Inspect, troubleshoot, and manage running containers using Docker CLI commands.
  6. Apply basic containerization practices, including .dockerignore, non-root execution, and image tagging.

What you need ​

Tool or requirementWhy you need it
Docker Desktop (Windows/macOS) or Docker Engine (Linux)Build and run containers locally
Docker CLIWork with Docker images and containers
deployment-workshop repositoryReuse the Node.js/Express API from previous lessons
Node.js and npmRun the existing app and its Vitest tests before containerizing it
A terminal and code editorCreate the Dockerfile and execute commands
A browser, Insomnia, or curlSend requests to the containerized API
Docker Hub account (optional)Explore publishing an image to a registry

Check your Docker installation

Install Docker using the instructions for your operating system at Docker's official installation guide.

After installation, make sure Docker Desktop or the Docker Engine is running, then check:

bash
docker --version
docker info

The first command checks whether the Docker CLI is installed. The second checks whether the CLI can communicate with a running Docker Engine. If docker info reports that it cannot connect to the Docker daemon, start Docker before continuing.

On Linux, you may need to configure access to the Docker socket. Follow the Docker post-installation guide, and remember that membership in the docker group grants powerful privileges on the host.

What you will build ​

Your project will gain two files: a Dockerfile and a .dockerignore.

text
deployment-workshop/
├── .github/
│   └── workflows/
│       └── ci.yml
├── site/
│   └── index.html
├── src/
│   ├── app.js
│   └── server.js
├── tests/
│   └── app.test.js
├── .dockerignore          ← New: files to exclude from Docker builds
├── .gitignore
├── Dockerfile             ← New: instructions for building the image
├── package.json
└── package-lock.json

You will use those files to create and run your own image:

text
Your project + Dockerfile
           │
           ▼
       docker build
           │
           ▼
    Docker image
   deployment-api:1.0
           │
           ▼
        docker run
           │
           ▼
    Running container
      Node.js API
           │
           ▼
  http://localhost:3000/health

The objective is a working local container, not a new cloud deployment.


Part 1 — Understand Containerization ​

1. Why use containers? ​

Suppose your API works on your laptop but fails on another developer's machine. The problem might be an incompatible Node.js version, missing dependencies, or a different startup command.

You could give the other developer a long list of setup instructions. Or you could describe the application's runtime and setup in a Dockerfile, build an image from it, and run that image wherever a compatible container runtime is available.

Docker helps you:

  • Package your application with the runtime and production dependencies it needs.
  • Standardize the startup process across development and deployment environments.
  • Isolate processes from many details of the host system.
  • Create repeatable builds that can be versioned and distributed.
  • Run multiple application versions without installing each one's dependencies directly on the host.

A container does not guarantee identical behavior everywhere. CPU architecture, operating system compatibility, configuration, networking, storage, and external services still matter. Docker reduces important differences; it does not remove every difference.

2. Containers vs. virtual machines ​

Both virtual machines and containers help isolate workloads, but they work differently.

FeatureVirtual machineContainer
What it includesA guest operating system and applicationsAn application and its userspace dependencies
Operating-system kernelUses its own guest kernelUsually shares the host's kernel through the container runtime
StartupStarts a guest operating systemStarts the container's main process
Typical overheadUsually higherUsually lower
IsolationVirtualized hardware and operating systemProcess, filesystem, and network isolation
ExampleAn Ubuntu VM running Node.jsA Docker container running a Node.js API

A useful distinction: Docker containers in this course are Linux containers. On macOS and Windows, Docker Desktop typically runs them using a lightweight Linux virtual machine behind the scenes.

text
Virtual machines                    Containers

Host hardware                       Host hardware
      │                                   │
Hypervisor / host OS                 Host OS + container runtime
      │                                   │
  ┌───┴───────────┐                  ┌────┴───────────┐
  │               │                  │                │
Guest OS       Guest OS           Node.js API      Another app
  │               │               + dependencies   + dependencies
  App             App                 │                │
  └───────────────┘                  └────────────────┘
                                      Shared kernel

Containers are very useful for packaging applications, while virtual machines provide a different level of operating-system isolation and control. The two technologies are not mutually exclusive.

3. Essential Docker terminology ​

TermWhat it meansExample
Docker EngineThe software that builds and runs containersThe running Docker service
Docker CLICommands used to communicate with Dockerdocker build, docker run
DockerfileA text file containing image-build instructionsFROM, COPY, RUN, CMD
ImageA packaged, read-only template used to create containersdeployment-api:1.0
ContainerA running or stopped instance created from an imagedeployment-api
RegistryA service that stores and distributes imagesDocker Hub
TagA readable label associated with an image1.0, 1.1, latest
Build contextFiles made available to a Docker buildThe . in docker build -t deployment-api:1.0 .
LayerA component of an image that Docker can reuse during buildsA layer that installs npm packages

Image vs. container ​

Think of an image as a prepared template and a container as a running instance of that template.

You can create several containers from the same image. They use the same image content but have separate running processes and their own writable container filesystems.

text
     Docker image
   deployment-api:1.0
          │
    ┌─────┴─────┐
    ▼           ▼
 Container A  Container B
  API on one   API on a
  host port    different port

Deleting a container does not normally delete the image it was created from. And building an image does not automatically start a container.

4. Run a container without writing a Dockerfile ​

Start with Docker's small hello-world image:

bash
docker run --rm hello-world

Docker looks for the image locally. If it is not available, Docker attempts to download it from a registry, creates a container, runs it, and displays a confirmation message. The --rm flag removes the container after its process exits.

Now inspect your images:

bash
docker image ls

You should see a hello-world image if the command succeeded. The image can remain even though the container has been removed.

Also try:

bash
docker ps
docker ps -a
  • docker ps lists running containers.
  • docker ps -a lists both running and stopped containers.

Because you used --rm, the finished hello-world container should not remain in the list.

Check your understanding ​

  1. Can you start two containers from the same image?
  2. Does an image contain a running Node.js process?
  3. What happens to an image when you stop a container?
  4. Is Docker Hub the same thing as the Docker Engine?
Reveal suggested answers
  1. Yes. Each container is a separate instance of the image.
  2. No. An image describes the files and configuration needed to start the process; the process runs in a container.
  3. Stopping the container does not remove the image.
  4. No. Docker Hub stores and distributes images; Docker Engine builds and runs containers.

Part 2 — Package Your Express API ​

5. Check the project from Lesson 2 ​

Open the folder that contains your existing package.json:

bash
cd deployment-workshop

Already inside the project?

Skip the cd command if your terminal is already in deployment-workshop.

Confirm that your application still works before putting it into a container:

bash
npm ci
npm run check
npm test

Your test suite should pass. The API from Lesson 2 separates the Express application and its server:

src/app.js contains routes similar to:

js
import express from 'express';

const app = express();

app.use(express.json());

app.get('/', (_req, res) => {
  res.json({ message: 'Hello from Render!' });
});

app.get('/health', (_req, res) => {
  res.status(200).json({ status: 'ok' });
});

export default app;

src/server.js starts the server:

js
import app from './app.js';

const port = Number(process.env.PORT || 3000);

app.listen(port, '0.0.0.0', () => {
  console.log(`API listening on port ${port}`);
});

Keep any additional routes you added in Lesson 2. You do not need to replace your existing files with these examples.

Check the listening address

For this workshop, your server needs to listen on 0.0.0.0, not just 127.0.0.1 or localhost inside the container. That allows requests forwarded through Docker's published port to reach the Node.js process.

6. Create a Dockerfile ​

At the root of deployment-workshop, create a file named exactly Dockerfile (with no extension):

dockerfile
FROM node:22-alpine

WORKDIR /app

ENV NODE_ENV=production

COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force

COPY --chown=node:node src ./src

USER node

EXPOSE 3000

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

This is a small, production-oriented Dockerfile for the API you have already built. It packages the API without copying your Netlify static site or your test files into the runtime image.

Why Node.js 22?

Lesson 2 uses Node.js 22 in the GitHub Actions workflow, so we use the same major version here. The alpine variant is smaller than many general-purpose images, but some applications with native dependencies may need a Debian-based Node image instead.

For a long-lived production service, choose a supported Node.js version and review the base image regularly. Version tags can be updated by their publishers; pinning an image digest can make builds more reproducible.

7. Understand every Dockerfile instruction ​

InstructionWhat it doesWhy we use it
FROM node:22-alpineStarts with a Node.js imageProvides Node.js and npm
WORKDIR /appSets the working directory inside the imageKeeps application files together
ENV NODE_ENV=productionSets a default runtime environment variableIdentifies the runtime as production
COPY package.json package-lock.json ./Copies dependency manifestsLets Docker reuse the dependency-install layer if only source code changes
RUN npm ci --omit=devInstalls dependencies during the image buildExcludes development-only tools such as Vitest
COPY --chown=node:node src ./srcCopies the source files and assigns ownershipAdds the API code with suitable ownership
USER nodeSelects the image's unprivileged node userAvoids running the API as root
EXPOSE 3000Documents the port the app expects to listen onHelps readers understand the image's networking requirements
CMD ["node", "src/server.js"]Defines the default startup commandStarts your existing API

Three details deserve extra attention.

First: RUN and CMD happen at different times. RUN npm ci ... executes when Docker builds the image. CMD ... executes when Docker starts a container from the image.

Second: EXPOSE does not make your API available on your computer. You still need to publish a port using docker run -p.

Third: your runtime image does not include Vitest. That's intentional. You run automated tests locally or through GitHub Actions; the running API only needs its production dependencies. If your application unexpectedly imports something from devDependencies, the container may fail. Correct the dependency classification rather than installing every development tool in the production image.

8. Create a .dockerignore ​

Docker builds use a build context, which is the set of files made available to the build. If you send unnecessary files, builds can be slower and you risk including material that does not belong in an image.

Create a file named .dockerignore in the project root:

text
node_modules
.git
.github
.env
.env.*
coverage
.vitest
site
tests
.DS_Store
npm-debug.log*

This excludes local dependencies, Git history, environment files, test output, and the Netlify site from the build context.

We use explicit COPY instructions in our Dockerfile too. That means only the selected manifests and src directory are added to the image, even if other files exist in the repository.

Environment files and credentials

Do not copy real .env files, API keys, passwords, or other secrets into a Docker image. Images can be shared or pushed to registries, and their contents may be inspected.

In Lesson 4, you will configure environment variables when running your application rather than embedding sensitive values in an image. .dockerignore is a useful safeguard, but it is not a replacement for secret management.

9. Build your first application image ​

Make sure the terminal is in the directory containing your Dockerfile and package.json.

Run:

bash
docker build -t deployment-api:1.0 .

The parts of this command are:

PartMeaning
docker buildBuild an image from a Dockerfile
-t deployment-api:1.0Give the image a name and tag
.Use the current directory as the build context

Docker may need to download the Node.js base image the first time you build. It will then copy the dependency manifests, install production packages, and copy your API source.

Check the result:

bash
docker image ls

Look for an image named deployment-api with tag 1.0.

The build cache

Build the same image a second time:

bash
docker build -t deployment-api:1.0 .

You may see that Docker reuses cached build steps. Because we copy package.json and package-lock.json before the source code, changing only src/app.js will usually let Docker reuse the npm installation layer.

A rebuild can still download dependencies or redo steps when its inputs, base image, or cache have changed.

Your image checkpoint ​

  • [ ] Docker is installed and its Engine is running.
  • [ ] My API passes npm run check and npm test locally.
  • [ ] I created a Dockerfile at the project root.
  • [ ] I created .dockerignore and excluded sensitive local configuration files.
  • [ ] docker build -t deployment-api:1.0 . completes successfully.
  • [ ] docker image ls shows deployment-api:1.0.

Part 3 — Run, Test, and Manage Your Container ​

10. Start the API in a container ​

Before running the container, stop any local npm start process using port 3000.

Start your new image:

bash
docker run --rm -d \
  --name deployment-api \
  -p 127.0.0.1:3000:3000 \
  deployment-api:1.0

The backslashes continue the command onto the next line in Bash or a similar shell. If you use Windows PowerShell, run it on one line:

powershell
docker run --rm -d --name deployment-api -p 127.0.0.1:3000:3000 deployment-api:1.0

Here is what each option does:

OptionMeaning
--rmRemove the container automatically after it stops
-dRun in the background (detached mode)
--name deployment-apiGive the container an easy-to-use name
-p 127.0.0.1:3000:3000Forward port 3000 on your computer's loopback interface to port 3000 in the container
deployment-api:1.0Image used to create the container

Host port and container port are different concepts:

text
Your computer (host)                 Docker container

http://localhost:3000    ────────→   0.0.0.0:3000
       host port                       Node.js API

We explicitly bind the host port to 127.0.0.1 because this is a local workshop. Without that address, Docker commonly publishes the port on all host interfaces, potentially making the API reachable from other machines on your network.

11. Test the API through Docker ​

Run:

bash
curl http://localhost:3000/
curl http://localhost:3000/health

You should receive the same responses as in Lessons 1 and 2:

json
{"message":"Hello from Render!"}
json
{"status":"ok"}

You can also open Insomnia, create a GET request to http://localhost:3000/health, and confirm that the response status is 200 OK with the expected JSON.

Why does the response still say “Hello from Render!”?

That text is part of your Express route. Docker runs the same application code, so the response does not change merely because the runtime changed.

The API is currently running inside Docker on your computer, not on Render. Your existing Render service remains separate.

12. Inspect the running container ​

Try these commands while the API is running:

bash
docker ps

Shows running containers and their port mappings.

bash
docker logs deployment-api

Shows the console output from your Node.js process. You should find a line similar to API listening on port 3000.

bash
docker logs -f deployment-api

Streams new log output. Press Ctrl+C to stop following logs; the container continues running.

bash
docker inspect deployment-api

Displays detailed information, including network configuration, the image reference, and the container state.

bash
docker exec deployment-api node --version

Runs node --version inside the running container. This is a useful way to confirm that the container has its own Node.js runtime.

bash
docker exec -it deployment-api sh

Opens a shell inside the container. Try pwd and ls, then type exit to return to your terminal. Alpine-based images provide sh, but often do not include bash.

Inspect, don't modify

You can inspect a running container when troubleshooting. Avoid manually editing application files inside it: those changes are not part of your Dockerfile or Git history and usually disappear when you replace the container.

13. Stop and remove the container ​

Stop your API:

bash
docker stop deployment-api

Then check:

bash
docker ps -a
docker image ls

Because the container was created with --rm, it should have been removed when it stopped. The deployment-api:1.0 image still exists, so you can use it again.

14. Publish a different host port ​

You do not have to use the same port number outside and inside the container.

Start the image with port 4000 on your computer mapped to port 3000 in the container:

bash
docker run --rm -d \
  --name deployment-api \
  -p 127.0.0.1:4000:3000 \
  deployment-api:1.0

For PowerShell, you can again place the command on one line.

Now test:

bash
curl http://localhost:4000/health

The request reaches the same Node.js API. The host port changed, but the port the application listens on inside the container did not.

Stop this container before continuing:

bash
docker stop deployment-api

Think about it ​

  1. What happens if you run the container without -p?
  2. What is the difference between 3000:3000 and 4000:3000?
  3. Does EXPOSE 3000 publish the port by itself?
  4. Why can you run docker exec deployment-api node --version even if Node.js is not installed directly on the host?
Reveal suggested answers
  1. The process can still run inside the container, but you have not published the API port to your host.
  2. The first value is the host port and the second is the container port. With 4000:3000, requests to host port 4000 are forwarded to container port 3000.
  3. No. EXPOSE documents the container port; -p publishes a host port.
  4. The image contains Node.js, and docker exec runs the command inside the container.

Part 4 — Rebuild, Version, and Troubleshoot Images ​

15. Change the application ​

Containers are meant to be replaceable. When you change the source code, the running container does not automatically update to use your new files.

Add this route to src/app.js, before export default app:

js
app.get('/container', (_req, res) => {
  res.json({ message: 'Hello from Docker!' });
});

Keep your existing routes and their responses unchanged so the tests from Lesson 2 still work.

Run your checks:

bash
npm run check
npm test

Now create a new image tag:

bash
docker build -t deployment-api:1.1 .

Start a container from the new image:

bash
docker run --rm -d \
  --name deployment-api \
  -p 127.0.0.1:3000:3000 \
  deployment-api:1.1

Check the new route:

bash
curl http://localhost:3000/container

Expected response:

json
{"message":"Hello from Docker!"}

16. Compare image versions ​

List the images:

bash
docker image ls

You should find both deployment-api:1.0 and deployment-api:1.1.

Stop the new container:

bash
docker stop deployment-api

You can still run the old version:

bash
docker run --rm -d \
  --name deployment-api \
  -p 127.0.0.1:3000:3000 \
  deployment-api:1.0

Request the new /container route:

bash
curl -i http://localhost:3000/container

You should receive 404 Not Found because the earlier image did not include the route.

Stop the container again:

bash
docker stop deployment-api

This illustrates a basic rollback idea: if a newer image has a problem, you may be able to start a previously working image. Real production rollbacks may also involve database schema and configuration compatibility, so changing an image tag is not always enough.

What does the latest tag mean?

latest is only a tag name. Docker does not compare image versions to decide which is newest. For this workshop, explicit tags such as 1.0 and 1.1 make it easier to identify which version you are running.

17. What is Docker Hub? ​

So far, your images exist only on your computer. A registry lets you store images and make them available to another Docker environment.

Docker Hub is one such registry. A common image workflow looks like this:

text
Local source code
      │
      ▼
docker build
      │
      ▼
Local image
      │
      ▼
docker push
      │
      ▼
Image registry (for example, Docker Hub)
      │
      ▼
docker pull on another machine
      │
      ▼
docker run

You do not need a Docker Hub account to complete this lesson. You already used a registry indirectly when Docker downloaded hello-world and the node:22-alpine base image.

Optional exercise — Publish an image to Docker Hub

If you have a Docker Hub account and want to share your workshop image:

  1. Create a repository named deployment-api in Docker Hub and review whether it will be public or private.
  2. Log in with docker login and follow the authentication prompts.
  3. Replace YOUR_DOCKER_HUB_USERNAME below with your Docker Hub username.
bash
docker tag deployment-api:1.1 YOUR_DOCKER_HUB_USERNAME/deployment-api:1.1
docker push YOUR_DOCKER_HUB_USERNAME/deployment-api:1.1

Another Docker environment with the appropriate access could then use:

bash
docker pull YOUR_DOCKER_HUB_USERNAME/deployment-api:1.1

Only publish images that you are authorized to share. Do not put secrets or private application data in an image, especially one stored in a public repository.

18. Troubleshoot common Docker problems ​

ProblemWhat to check
Cannot connect to the Docker daemonStart Docker Desktop or Docker Engine and check docker info.
failed to read dockerfileRun the build from the project root and ensure the file is named Dockerfile.
npm ci fails during the buildCheck that package-lock.json exists and matches package.json.
Cannot find module ... in the containerMake sure runtime packages are in dependencies, not only devDependencies.
Port 3000 is already in useStop the local Node.js server or choose a different host port, such as 4000:3000.
Browser cannot connect to the APICheck docker ps, the -p mapping, docker logs, and the listening address 0.0.0.0.
Container exits immediatelyInspect docker logs and check that CMD starts a long-running process.
API changes do not appearRebuild the image and start a new container using the updated tag.
docker exec failsCheck that the container is running with docker ps.
/health returns 404Verify the API route exists and that the image contains the expected source version.
Image build sends too many filesCheck .dockerignore and use narrow COPY instructions.

19. Good Docker habits ​

  • Use a supported base image and update it when necessary.
  • Keep images focused. Install only what your application needs to run.
  • Use .dockerignore. Exclude local dependencies, Git history, and sensitive files from the build context.
  • Run as a non-root user when the application does not need elevated privileges.
  • Avoid hardcoding secrets in Dockerfiles or images.
  • Use explicit image tags to distinguish versions.
  • Publish only the ports you need. Bind to 127.0.0.1 for this local exercise.
  • Keep application state outside replaceable containers. In Lesson 4, you will explore this with MySQL and persistent volumes.
  • Use source control and CI. Your Dockerfile belongs in the Git repository, and your Vitest checks should still pass before you build or deploy a new application version.

Final Exercise — Containerize and Verify Your API ​

Complete these tasks using your own deployment-workshop repository.

  1. Confirm that the Node.js API passes its existing Vitest tests.
  2. Write a Dockerfile that starts from node:22-alpine, installs the production dependencies, and launches src/server.js as the non-root node user.
  3. Add a .dockerignore that prevents node_modules, Git history, tests, and environment files from being sent to the build.
  4. Build an image tagged deployment-api:1.0.
  5. Run the image in a container with a localhost-only published port.
  6. Use Insomnia or curl to verify that /health returns HTTP 200 and {"status":"ok"}.
  7. Inspect the running container's logs and Node.js version.
  8. Stop the container and confirm that the image remains available.
  9. Add a new route, build deployment-api:1.1, and confirm the new image contains the change.
  10. Commit the Dockerfile and .dockerignore to your repository, then confirm your existing GitHub Actions checks still pass.
Example verification commands

Run these from the project root, making sure the application is not already using host port 3000:

bash
npm run check
npm test

docker build -t deployment-api:1.0 .

docker run --rm -d \
  --name deployment-api \
  -p 127.0.0.1:3000:3000 \
  deployment-api:1.0

docker ps
docker logs deployment-api
docker exec deployment-api node --version
curl -i http://localhost:3000/health

docker stop deployment-api
docker image ls

Commit the Docker configuration files after you verify them:

bash
git add Dockerfile .dockerignore
git commit -m "Containerize Express API with Docker"
git push

If your changes are on a feature branch, open a pull request instead of pushing directly to main.

Final checkpoint ​

  • [ ] I can explain the difference between a Docker image and a container.
  • [ ] I can build my Express API into an image.
  • [ ] I can start the image and access the API through a host port.
  • [ ] I understand why EXPOSE and -p are different.
  • [ ] I can inspect logs, execute a command inside a container, and stop it.
  • [ ] I can build an updated image and distinguish it from an earlier version.
  • [ ] I understand why .dockerignore, non-root execution, and production-only dependencies matter.

Knowledge Check ​

Try answering these questions before revealing the suggested answers.

  1. What problem does containerization help solve when working across different development machines?
  2. What is the difference between a Docker image and a Docker container?
  3. How is a container different from a virtual machine?
  4. What is a Dockerfile used for?
  5. Why do we copy package.json and package-lock.json before copying src?
  6. Why do we use npm ci --omit=dev in the runtime image?
  7. What is the purpose of .dockerignore?
  8. Does EXPOSE 3000 make the API available at localhost:3000?
  9. In -p 127.0.0.1:4000:3000, which port belongs to the container?
  10. Why is the API listening on 0.0.0.0 inside the container?
  11. What happens to a running container after you edit src/app.js on your computer?
  12. Where would you store an image if another machine needed to download it?
Reveal suggested answers
  1. It packages an application with a defined runtime and dependencies, reducing differences between machines.
  2. An image is a packaged template; a container is an instance created from that image.
  3. Containers normally share the host's kernel, whereas virtual machines run a guest operating system with its own kernel.
  4. A Dockerfile specifies the instructions for building an image.
  5. It lets Docker reuse the dependency-installation layer when only application source code changes.
  6. To perform a lockfile-based install while leaving out development-only tools from the runtime image.
  7. It excludes unwanted files from the Docker build context.
  8. No. EXPOSE documents a port; you must publish it with -p to reach it from the host.
  9. Port 3000 belongs to the container. Port 4000 is published on your computer's loopback interface.
  10. So the Node.js process accepts connections on the container's network interface, including requests forwarded by Docker.
  11. Nothing changes automatically. You must rebuild the image and create a new container, unless you have explicitly configured a development bind mount or similar workflow.
  12. In an image registry, such as Docker Hub, if you have permission to distribute the image.

What's next? ​

In Lesson 4 — Multi-Container Applications & Configuration Management, you will connect this Dockerized Express API to MySQL, use Docker Compose to manage both services together, and learn how to configure environment variables and persistent database storage.

Keep your Dockerfile, .dockerignore, and existing GitHub Actions workflow: you will reuse them in the next lesson.

Further reading ​