
When I started learning platform engineering and deployment properly, Docker was one of those things that looked scary from the outside.
There were terms like containers, images, Dockerfile, Docker Compose, ports, volumes, and I was like:
Bro, I just want my app to run properly.
But after trying it practically, Docker started making sense.
In simple words, Docker helps us package our app with everything it needs to run, so it can run the same way on any machine.
Whether the app is built with React, Next.js, Astro, Express, or anything else, the basic idea is almost the same.
In this blog, I will explain how to dockerize any app in a simple beginner-friendly way.
Dockerizing an app means creating a Docker setup for your project so it can run inside a container.
A container is like a small isolated environment where your app runs with its own dependencies, runtime, and commands.
Normally, when we run a project, we do things like:
npm install
npm run build
npm run startBut with Docker, we package this whole setup into a Docker image.
Then anyone can run the app using:
docker runor:
docker compose upThis makes the project easier to run, share, test, and deploy.
The biggest reason is consistency.
Many times, an app works perfectly on our laptop but fails on another machine or server.
This usually happens because of different:
This is the classic:
"It works on my machine."
Docker helps reduce this problem.
With Docker, we define the environment once, and the app runs inside that environment everywhere.
So instead of saying:
Install Node 20, install pnpm, install dependencies, setup this, setup that...
We can say:
Run this Docker container.
Much cleaner.
Before writing Docker files, these are the basic terms I understood first.
A Dockerfile is a file where we write instructions for building our app image.
Example:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
CMD ["npm", "start"]This tells Docker how to prepare and run the app.
An image is like a ready-made package of your app.
It contains your code, dependencies, runtime, and build setup.
You create an image from a Dockerfile.
A container is the running version of an image.
Simple difference:
Dockerfile = instructions
Image = packaged app
Container = running appDocker Compose is used when we want to run multiple things together.
For example:
Instead of running all containers manually, we define them in one docker-compose.yml file.
Then run:
docker compose upThe process is mostly the same for many JavaScript apps.
Before Docker, first understand your project.
Check your package.json.
Example:
{
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview"
}
}or for Next.js:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}This tells you what command is needed to build and run the app.
.dockerignore FileThis file tells Docker what not to copy into the image.
Create a file named:
.dockerignoreAdd this:
node_modules
.next
dist
build
.env
.git
.gitignore
Dockerfile
docker-compose.yml
npm-debug.logThis keeps the Docker image cleaner and smaller.
Now create a file named:
DockerfileThis is where we define how the app should be built and started.
If your app is built with React and Vite, you can use this:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]First, it uses Node.js to install dependencies and build the React app.
Then it uses Nginx to serve the final static files.
React/Vite apps usually generate a dist folder after build.
So we copy that dist folder into Nginx.
Astro apps can also be static or server-rendered.
For a normal static Astro app, use this:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]This is almost the same as React/Vite because Astro also creates a dist folder for static output.
Next.js is a little different because it usually needs a Node.js server to run.
Use this simple Dockerfile:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]This works for a basic Next.js app.
Make sure your package.json has this:
{
"scripts": {
"build": "next build",
"start": "next start"
}
}Then Docker can build and start the app properly.
For a more optimized Next.js Docker setup, you can use a multi-stage build:
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm install
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["npm", "start"]This separates dependency installation, building, and running the app.
It keeps the final container cleaner.
After creating the Dockerfile, run this command:
docker build -t my-app .Here:
my-app = name of the Docker image
. = current folderDocker will read the Dockerfile and create an image.
For React/Astro static app served with Nginx:
docker run -p 8080:80 my-appNow open:
http://localhost:8080For Next.js app:
docker run -p 3000:3000 my-appNow open:
http://localhost:3000Instead of writing long Docker commands, we can use Docker Compose.
Create a file:
docker-compose.ymlFor Next.js:
services:
app:
build: .
ports:
- "3000:3000"
env_file:
- .envNow run:
docker compose upTo stop:
docker compose downFor React or Astro with Nginx:
services:
app:
build: .
ports:
- "8080:80"Then run:
docker compose upIf your app uses environment variables, keep them in a .env file.
Example:
DATABASE_URL=""
NEXT_PUBLIC_API_URL=""
NEXT_PUBLIC_APP_URL=""Then in Docker Compose:
services:
app:
build: .
ports:
- "3000:3000"
env_file:
- .envImportant note:
Do not push your real .env file to GitHub.
Instead, create:
.env.exampleExample:
DATABASE_URL="your_database_url_here"
NEXT_PUBLIC_API_URL="your_api_url_here"
NEXT_PUBLIC_APP_URL="your_app_url_here"This helps others understand what env variables are needed.
This can happen because of:
The first thing I usually check is logs:
docker logs container_nameOr if using Docker Compose:
docker compose logsLogs are your best friend when debugging Docker issues.
Build image:
docker build -t my-app .Run container:
docker run -p 3000:3000 my-appRun with Docker Compose:
docker compose upRun in background:
docker compose up -dStop containers:
docker compose downView logs:
docker compose logsRebuild:
docker compose up --buildCheck running containers:
docker psYou should consider Docker when:
For very small static sites, Docker may not always be needed.
For example, if you are deploying a simple Astro or React site to Vercel, Netlify, or Cloudflare Pages, Docker is optional.
But if you are learning deployment, DevOps, or platform engineering, Docker is definitely worth learning.
This is how I understand Docker now:
Without Docker:
My app depends on the machine setup.
With Docker:
My app brings its own setup.That is the main idea.
Docker is not magic.
It just makes the environment predictable.
Dockerization is not about making things complicated.
It is about making the app easier to run, easier to share, and easier to deploy.
At first, Docker feels confusing because there are new terms like images, containers, Dockerfile, and Compose.
But once you dockerize one real app, it starts making sense.
My suggestion is simple:
Pick one app you already built.
Add a Dockerfile.
Run it inside Docker.
Break it.
Fix it.
Then try Docker Compose.
That is how I am learning it too: by building, not by watching 50 tutorials and forgetting everything after two days.
Docker is one of those skills that becomes clearer only when you actually use it.