Appearance
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:
- Explain containerization and distinguish containers from virtual machines.
- Identify the roles of Docker images, containers, Dockerfiles, registries, and the Docker Engine.
- Write a Dockerfile that packages a Node.js/Express application and installs only its production dependencies.
- Build and run a Docker image and make the API accessible through a published port.
- Inspect, troubleshoot, and manage running containers using Docker CLI commands.
- Apply basic containerization practices, including
.dockerignore, non-root execution, and image tagging.
What you need
| Tool or requirement | Why you need it |
|---|---|
| Docker Desktop (Windows/macOS) or Docker Engine (Linux) | Build and run containers locally |
| Docker CLI | Work with Docker images and containers |
deployment-workshop repository | Reuse the Node.js/Express API from previous lessons |
| Node.js and npm | Run the existing app and its Vitest tests before containerizing it |
| A terminal and code editor | Create the Dockerfile and execute commands |
A browser, Insomnia, or curl | Send 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 infoThe 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.jsonYou 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/healthThe 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.
| Feature | Virtual machine | Container |
|---|---|---|
| What it includes | A guest operating system and applications | An application and its userspace dependencies |
| Operating-system kernel | Uses its own guest kernel | Usually shares the host's kernel through the container runtime |
| Startup | Starts a guest operating system | Starts the container's main process |
| Typical overhead | Usually higher | Usually lower |
| Isolation | Virtualized hardware and operating system | Process, filesystem, and network isolation |
| Example | An Ubuntu VM running Node.js | A 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 kernelContainers 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
| Term | What it means | Example |
|---|---|---|
| Docker Engine | The software that builds and runs containers | The running Docker service |
| Docker CLI | Commands used to communicate with Docker | docker build, docker run |
| Dockerfile | A text file containing image-build instructions | FROM, COPY, RUN, CMD |
| Image | A packaged, read-only template used to create containers | deployment-api:1.0 |
| Container | A running or stopped instance created from an image | deployment-api |
| Registry | A service that stores and distributes images | Docker Hub |
| Tag | A readable label associated with an image | 1.0, 1.1, latest |
| Build context | Files made available to a Docker build | The . in docker build -t deployment-api:1.0 . |
| Layer | A component of an image that Docker can reuse during builds | A 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 portDeleting 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-worldDocker 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 lsYou 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 -adocker pslists running containers.docker ps -alists both running and stopped containers.
Because you used --rm, the finished hello-world container should not remain in the list.
Check your understanding
- Can you start two containers from the same image?
- Does an image contain a running Node.js process?
- What happens to an image when you stop a container?
- Is Docker Hub the same thing as the Docker Engine?
Reveal suggested answers
- Yes. Each container is a separate instance of the image.
- No. An image describes the files and configuration needed to start the process; the process runs in a container.
- Stopping the container does not remove the image.
- 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-workshopAlready 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 testYour 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
| Instruction | What it does | Why we use it |
|---|---|---|
FROM node:22-alpine | Starts with a Node.js image | Provides Node.js and npm |
WORKDIR /app | Sets the working directory inside the image | Keeps application files together |
ENV NODE_ENV=production | Sets a default runtime environment variable | Identifies the runtime as production |
COPY package.json package-lock.json ./ | Copies dependency manifests | Lets Docker reuse the dependency-install layer if only source code changes |
RUN npm ci --omit=dev | Installs dependencies during the image build | Excludes development-only tools such as Vitest |
COPY --chown=node:node src ./src | Copies the source files and assigns ownership | Adds the API code with suitable ownership |
USER node | Selects the image's unprivileged node user | Avoids running the API as root |
EXPOSE 3000 | Documents the port the app expects to listen on | Helps readers understand the image's networking requirements |
CMD ["node", "src/server.js"] | Defines the default startup command | Starts 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:
| Part | Meaning |
|---|---|
docker build | Build an image from a Dockerfile |
-t deployment-api:1.0 | Give 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 lsLook 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 checkandnpm testlocally. - [ ] I created a
Dockerfileat the project root. - [ ] I created
.dockerignoreand excluded sensitive local configuration files. - [ ]
docker build -t deployment-api:1.0 .completes successfully. - [ ]
docker image lsshowsdeployment-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.0The 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.0Here is what each option does:
| Option | Meaning |
|---|---|
--rm | Remove the container automatically after it stops |
-d | Run in the background (detached mode) |
--name deployment-api | Give the container an easy-to-use name |
-p 127.0.0.1:3000:3000 | Forward port 3000 on your computer's loopback interface to port 3000 in the container |
deployment-api:1.0 | Image 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 APIWe 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/healthYou 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 psShows running containers and their port mappings.
bash
docker logs deployment-apiShows 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-apiStreams new log output. Press Ctrl+C to stop following logs; the container continues running.
bash
docker inspect deployment-apiDisplays detailed information, including network configuration, the image reference, and the container state.
bash
docker exec deployment-api node --versionRuns 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 shOpens 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-apiThen check:
bash
docker ps -a
docker image lsBecause 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.0For PowerShell, you can again place the command on one line.
Now test:
bash
curl http://localhost:4000/healthThe 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-apiThink about it
- What happens if you run the container without
-p? - What is the difference between
3000:3000and4000:3000? - Does
EXPOSE 3000publish the port by itself? - Why can you run
docker exec deployment-api node --versioneven if Node.js is not installed directly on the host?
Reveal suggested answers
- The process can still run inside the container, but you have not published the API port to your host.
- 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. - No.
EXPOSEdocuments the container port;-ppublishes a host port. - The image contains Node.js, and
docker execruns 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 testNow 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.1Check the new route:
bash
curl http://localhost:3000/containerExpected response:
json
{"message":"Hello from Docker!"}16. Compare image versions
List the images:
bash
docker image lsYou should find both deployment-api:1.0 and deployment-api:1.1.
Stop the new container:
bash
docker stop deployment-apiYou can still run the old version:
bash
docker run --rm -d \
--name deployment-api \
-p 127.0.0.1:3000:3000 \
deployment-api:1.0Request the new /container route:
bash
curl -i http://localhost:3000/containerYou should receive 404 Not Found because the earlier image did not include the route.
Stop the container again:
bash
docker stop deployment-apiThis 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 runYou 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:
- Create a repository named
deployment-apiin Docker Hub and review whether it will be public or private. - Log in with
docker loginand follow the authentication prompts. - Replace
YOUR_DOCKER_HUB_USERNAMEbelow 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.1Another Docker environment with the appropriate access could then use:
bash
docker pull YOUR_DOCKER_HUB_USERNAME/deployment-api:1.1Only 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
| Problem | What to check |
|---|---|
Cannot connect to the Docker daemon | Start Docker Desktop or Docker Engine and check docker info. |
failed to read dockerfile | Run the build from the project root and ensure the file is named Dockerfile. |
npm ci fails during the build | Check that package-lock.json exists and matches package.json. |
Cannot find module ... in the container | Make sure runtime packages are in dependencies, not only devDependencies. |
| Port 3000 is already in use | Stop the local Node.js server or choose a different host port, such as 4000:3000. |
| Browser cannot connect to the API | Check docker ps, the -p mapping, docker logs, and the listening address 0.0.0.0. |
| Container exits immediately | Inspect docker logs and check that CMD starts a long-running process. |
| API changes do not appear | Rebuild the image and start a new container using the updated tag. |
docker exec fails | Check that the container is running with docker ps. |
/health returns 404 | Verify the API route exists and that the image contains the expected source version. |
| Image build sends too many files | Check .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.1for 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.
- Confirm that the Node.js API passes its existing Vitest tests.
- Write a
Dockerfilethat starts fromnode:22-alpine, installs the production dependencies, and launchessrc/server.jsas the non-rootnodeuser. - Add a
.dockerignorethat preventsnode_modules, Git history, tests, and environment files from being sent to the build. - Build an image tagged
deployment-api:1.0. - Run the image in a container with a localhost-only published port.
- Use Insomnia or
curlto verify that/healthreturns HTTP 200 and{"status":"ok"}. - Inspect the running container's logs and Node.js version.
- Stop the container and confirm that the image remains available.
- Add a new route, build
deployment-api:1.1, and confirm the new image contains the change. - Commit the
Dockerfileand.dockerignoreto 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 lsCommit the Docker configuration files after you verify them:
bash
git add Dockerfile .dockerignore
git commit -m "Containerize Express API with Docker"
git pushIf 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
EXPOSEand-pare 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.
- What problem does containerization help solve when working across different development machines?
- What is the difference between a Docker image and a Docker container?
- How is a container different from a virtual machine?
- What is a Dockerfile used for?
- Why do we copy
package.jsonandpackage-lock.jsonbefore copyingsrc? - Why do we use
npm ci --omit=devin the runtime image? - What is the purpose of
.dockerignore? - Does
EXPOSE 3000make the API available atlocalhost:3000? - In
-p 127.0.0.1:4000:3000, which port belongs to the container? - Why is the API listening on
0.0.0.0inside the container? - What happens to a running container after you edit
src/app.json your computer? - Where would you store an image if another machine needed to download it?
Reveal suggested answers
- It packages an application with a defined runtime and dependencies, reducing differences between machines.
- An image is a packaged template; a container is an instance created from that image.
- Containers normally share the host's kernel, whereas virtual machines run a guest operating system with its own kernel.
- A Dockerfile specifies the instructions for building an image.
- It lets Docker reuse the dependency-installation layer when only application source code changes.
- To perform a lockfile-based install while leaving out development-only tools from the runtime image.
- It excludes unwanted files from the Docker build context.
- No.
EXPOSEdocuments a port; you must publish it with-pto reach it from the host. - Port 3000 belongs to the container. Port 4000 is published on your computer's loopback interface.
- So the Node.js process accepts connections on the container's network interface, including requests forwarded by Docker.
- 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.
- 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.