DEV Community

Cover image for From Github to Container: Running a Node.js App (Wonderkid app) Locally and Packaging It with Docker.
Raphael Olaniyi
Raphael Olaniyi

Posted on

From Github to Container: Running a Node.js App (Wonderkid app) Locally and Packaging It with Docker.

Getting a project running on your machine and then packaging it in a container are two of the most common tasks in modern development and DevOps work. This guide takes a Node.js web application (wonderkid) from a fresh git clone to a running Docker container, with an explanation of why each step matters.
Prerequisites: Git, Node.js (v22 recommended) with npm, VS Code, and Docker Desktop.

Part 1: Local Application Setup

Before containerizing anything, confirm the application works on your own machine. If it doesn't run locally, it won't run in a container either, and debugging is much easier outside Docker. Lets follow the following steps to confirm this:

Step 1: Clone the repository

Open VS Code and create a dedicated workspace folder for your projects (in this case, I used the date I created the folder to name the folder ‘oct326’ ). Keeping projects in one place makes them easier to find and manage. Open the integrated terminal (Terminal → New Terminal, or Ctrl + `), navigate to that folder, and clone the repository. To do this you will need the repository url which you will get from the GitHub repository. :
git clone

git clone https://github.com/your-org/wonderkids.git

git clone

This downloads the full project, including its history, into a new folder named after the repository.

Step 2: Navigate into the project

Before navigating into the folder, type the Ls command to see that the folder exists

ls

Then use the ‘cd’ command to navigate inside the project folder.
cd

Cd

Every command from here on must run from the project root, the folder containing package.json. Running ls (macOS/Linux) or dir (Windows) should show package.json, src/ or app/, and similar files. If you don't see package.json, you're in the wrong folder.

Step 3: Install dependencies

npm install

npm install

npm reads package.json (and package-lock.json, if present) and downloads everything the project needs into a node_modules/ folder. This can take a minute or two. Some warnings about deprecated packages are normal, but red ERR! lines mean something failed, often a Node version mismatch, so check the project's README for the required version.

Step 4: Run the app locally

npm run dev

npm rundev

This starts the development server, which usually prints something like:Local: http://localhost:3000

Open that address in your browser to confirm the application loads. Dev servers typically include hot reloading, so changes to your code appear instantly.

Open

When you're finished, stop the server by pressing

Ctrl + C in the terminal.
Tip: If the port is already in use, another process is occupying it. Stop that process or run the app on a different port, depending on your framework's configuration.

Part 2: Docker Application Containerization

Containerization packages your application together with its runtime and dependencies, so it behaves the same on your laptop, a teammate's machine, a CI pipeline, or a cloud host. These are the steps I took to contanerize wonderkid application

Step 1: Check that Docker Desktop is running

Docker commands need the Docker engine running in the background. Launch Docker Desktop and wait until its status shows as running. Then verify from the terminal:

docker --version
docker info

If docker info returns an error like "Cannot connect to the Docker daemon," Docker Desktop isn't running yet.

Step 2: Create the configuration files

From the project root, create the two files Docker needs:
touch Dockerfile

touch

touch .dockerignore

Docker ignore

Alternatively, you can create it manually without using the codes.
On Windows PowerShell, use New-Item Dockerfile and New-Item .dockerignore instead, or create the files from VS Code's Explorer panel.
Dockerfile is the recipe that tells Docker how to build your image.
.dockerignore lists files that should not be sent to Docker during the build.

Step 3: Configure the .dockerignore

Docker ignore config

Without a .dockerignore, Docker copies everything in your folder into the build context, including a large node_modules directory and your Git history. That slows builds and can leak sensitive files. Add:
node_modules
npm-debug.log
.next
dist
build
.git
.gitignore
.env
.env.*
Dockerfile
.dockerignore
README.md
.vscode

Excluding node_modules is especially important. The container installs its own dependencies for its own operating system, and copying yours in can cause conflicts.

Step 4: Write the Dockerfile with a multi-stage build

Dockerfile config

A multi-stage build uses several FROM stages in one Dockerfile. Build tools and development dependencies are needed to compile your app but not to run it. By building in one stage and copying only the results into a clean final stage, you get a smaller, faster, and more secure image.
Here is a solid starting template using Node 22 on Alpine Linux, a minimal base image:

Stage 1: Install dependencies ----------

FROM node:22-alpine AS deps
WORKDIR /app

Copy only dependency manifests first to leverage Docker layer caching

COPY package.json package-lock.json ./
RUN npm ci

Stage 2: Build the application ----------

FROM node:22-alpine AS builder
WORKDIR /app

COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

Stage 3: Production runtime ----------

FROM node:22-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV PORT=3000

Run as a non-root user for better security

RUN addgroup -S appgroup && adduser -S appuser -G appgroup

Copy only what is needed to run the app

COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/package-lock.json ./package-lock.json
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist

Remove dev-only packages from the final image

RUN npm prune --omit=dev && chown -R appuser:appgroup /app

USER appuser

EXPOSE 3000

CMD ["npm", "start"]

Why it's built this way:

Choice
Reason
node:22-alpine
A current, actively supported Node version on a small base image with a reduced attack surface. Prefer a maintained LTS release.
Three stages
Separates dependency install, build, and runtime so the final image carries only what it needs.
Copying package*.json first
Docker caches layers. If your dependencies haven't changed, rebuilds skip the slow install step.
npm ci
Installs exact versions from the lockfile, giving reproducible builds (faster and stricter than npm install).
Non-root user
If the app is ever compromised, the attacker doesn't get root inside the container.
EXPOSE 3000
Documents which port the app listens on. Use 3001 if your app runs there. (just as we did in this case)

Adapt it to your project.
The template above assumes your build outputs to dist/ and npm start runs the production server. Check your package.json scripts and adjust:
Next.js: the build output is .next/, so copy .next, public, and package.json instead of dist. Next.js's output: "standalone" option can shrink the image further.
Vite/React single-page apps: these produce static files in dist/. A common pattern is a final stage using nginx:alpine that serves the static files, in which case you expose port 80 instead.
Express/NestJS APIs: copy your compiled output (often dist/) and set CMD ["node", "dist/main.js"] or similar.
Also make sure your app listens on 0.0.0.0 (not just localhost) and respects the PORT environment variable. Otherwise it won't be reachable from outside the container.

Step 5: Build the Docker image

From the project root, run:
docker build -t : .

For example I used:
docker build -t wonderkids:latest .

docker build

-t assigns a name and tag to the image.
The trailing . tells Docker to use the current directory as the build context.
Docker works through each stage and prints progress. When it finishes, confirm the image exists:
docker images

You'll see your image listed with its size. Compare it to a single-stage build and the multi-stage savings will be clear.
Tip: Use meaningful tags such as wonderkids:1.0.0 rather than relying on latest alone, so you can roll back to a known version.

Step 6: Run the container

Start a container from your image, mapping a port on your machine to the port inside the container:
docker run -p : --name :

For example:
docker run -p 3001:3001 --name wonderkids-app wonderkids:latest

Docker run

-p 3001:3001 maps host port 3001 to container port 3001. The format is always host:container. To access the app on a different host port, change only the left side: -p 8080:3000 makes it available at http://localhost:8080.
--name gives the container a friendly name instead of a random one.
Open http://localhost:3000 in your browser to confirm the containerized app is running. Press Ctrl + C to stop it if it's running in the foreground.
Useful variations:
Run in the background (detached mode)
docker run -d -p 3000:3000 --name wonderkids-app wonderkids:latest

Automatically remove the container when it stops
docker run --rm -p 3000:3000 wonderkids:latest

The container has been created on Dockerhub.

Everyday Docker Commands Useful for Every DevOps Engineer

Task
Command
List running containers
docker ps
List all containers
docker ps -a
View logs
docker logs wonderkids-app
Follow logs live
docker logs -f wonderkids-app
Stop a container
docker stop wonderkids-app
Start a stopped container
docker start wonderkids-app
Remove a container
docker rm wonderkids-app
Remove an image
docker rmi wonderkids:latest
Open a shell inside a container
docker exec -it wonderkids-app sh

Troubleshooting

"Cannot connect to the Docker daemon." Docker Desktop isn't running. Start it and wait for it to finish initializing.
"Port is already allocated." Something else is using that host port, possibly your local dev server from Part 1 or an older container. Stop it, or map to a different host port (-p 3001:3000).
"Container name already in use." A previous container with that name exists. Remove it with docker rm <container-name> or choose a new name.
Container starts then exits immediately. Check docker logs <container-name>. Common causes are a wrong CMD, a missing build output folder, or missing environment variables.
App runs in the container but the browser can't reach it. Confirm the app binds to 0.0.0.0 rather than 127.0.0.1, and that your -p mapping matches the port the app actually listens on.
Build fails at COPY --from=builder /app/dist. Your framework outputs somewhere other than dist/. Check your build script and update the path.

Conclusion

We've now taken an application through a complete workflow: cloned it, installed dependencies, verified it locally, and then packaged it as an optimized, secure Docker image that runs identically anywhere Docker does. From here, natural next steps are pushing the image to a registry (Docker Hub or Azure Container Registry), using Docker Compose for multi-service setups, and automating image builds in a CI/CD pipeline. We will complete this in the next article.

Top comments (1)

Collapse
 
realcloudprojects profile image
SKILL.SCH •

Well written!, thanks

Some comments may only be visible to logged-in visitors. Sign in to view all comments.