Containerization is only confusing when the example is too big. If you keep the stack small, the ideas become obvious: build an image, run it, confirm it responds. Orchestration at this scale is just a repeatable run command written down so you don’t have to remember it.

This post is a start‑to‑finish lab. Every file and command is real and copy‑paste ready. If you follow it exactly, you will end with a running container, started through Compose, responding to a health check. No placeholders. No skipped steps. No abstract explanation without a working result.

What you are building

You will create a tiny Node web service with one health endpoint, package it into a Docker image, and run it using Docker Compose. The goal is not to build an app. The goal is to understand the minimum moving parts of containerization and a simple orchestration layer.

Before touching anything, here is the end state so the steps make sense.

ItemPathPurpose
Web serviceapp/index.jsSimple HTTP server with /health endpoint
App manifestapp/package.jsonDeclares dependencies and start command
Container build fileapp/DockerfileDefines how the image is built
Orchestration filedocker-compose.ymlRuns the container with a mapped port

Once these exist, you will be able to build the image, run the container, and confirm it responds.

Prerequisites

You need Docker Desktop installed and Node.js available on your machine. No other tooling is required. If Docker is not installed, install Docker Desktop first and confirm this works before continuing:

docker version

If that command returns a version, Docker is ready.

Step 1: Create the web service

The service will expose one route: /health. This keeps verification simple. If /health returns JSON, the container is working.

Run the following commands to create the app folder and files.

mkdir -p app
cat > app/index.js <<'EOF'
const express = require("express");
const app = express();
app.get("/health", (_req, res) => {
res.json({ ok: true });
});
app.listen(3000, () => {
console.log("server on 3000");
});
EOF

Now create the package manifest.

cat > app/package.json <<'EOF'
{
"name": "lab-app",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "^4.19.2"
}
}
EOF

Install dependencies and verify the service runs locally.

cd app
npm install
npm start

Expected output:

server on 3000

In another terminal, verify the health endpoint.

curl http://localhost:3000/health

Expected response:

{"ok":true}

Stop the local server with Ctrl+C. At this point the service works outside a container.

Step 2: Build a container image

Now you will package the service into a container image using a Dockerfile.

Create the Dockerfile.

cat > app/Dockerfile <<'EOF'
FROM node:18-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
EOF

Build the image.

docker build -t lab-app ./app

If the build completes without errors, the image is ready. You can verify it exists with:

docker images | grep lab-app

Step 3: Run the image with Compose

Running containers manually is fine once. Orchestration starts when the run command becomes a file you can repeat. Docker Compose provides that layer.

Create the Compose file in the project root.

cat > docker-compose.yml <<'EOF'
services:
app:
build: ./app
ports:
- "3000:3000"
EOF

Start the service through Compose.

docker compose up --build

You should see output showing the image building and the server starting.

server on 3000

In another terminal, verify the running container.

curl http://localhost:3000/health

Expected response:

{"ok":true}

Stop Compose with Ctrl+C when finished.

What just happened

A Node service was created. A Docker image packaged it. Compose ran the image with a mapped port. The /health endpoint confirmed the container responded correctly. That is the complete containerization and basic orchestration loop in its smallest useful form.

Common failure points

SymptomLikely causeFix
Cannot find module expressDependencies not installedRun npm install in app and rebuild image
Container exits immediatelyStart command missingEnsure scripts.start exists in package.json
curl connection refusedPort mapping missing or container not runningConfirm 3000:3000 in compose and container logs
Docker build fails on lockfilepackage-lock.json missingRun npm install before building image

Closing

Containerization is just packaging a working service into an image. Orchestration is just running that image through a repeatable configuration. Keeping the example small removes the noise and makes the mechanics clear.

Once this lab runs on your machine, you have a real baseline. From here you can add databases, reverse proxies, or multiple services, but the core loop remains the same.