Blog
Next
How to Dockerize Any App

How to Dockerize Any App

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.

What Does Dockerizing An App Mean?

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 start

But with Docker, we package this whole setup into a Docker image.

Then anyone can run the app using:

docker run

or:

docker compose up

This makes the project easier to run, share, test, and deploy.

Why Do We Need Docker?

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:

  • Node.js versions
  • package manager versions
  • operating systems
  • missing dependencies
  • environment variables
  • build setup issues

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.

Basic Docker Terms You Should Know

Before writing Docker files, these are the basic terms I understood first.

Dockerfile

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.

Image

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.

Container

A container is the running version of an image.

Simple difference:

Dockerfile = instructions
Image = packaged app
Container = running app

Docker Compose

Docker Compose is used when we want to run multiple things together.

For example:

  • frontend app
  • backend API
  • PostgreSQL database
  • Redis
  • Nginx

Instead of running all containers manually, we define them in one docker-compose.yml file.

Then run:

docker compose up

Basic Steps To Dockerize Any App

The process is mostly the same for many JavaScript apps.

Step 1: Check How Your App Runs Normally

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.

Step 2: Create A .dockerignore File

This file tells Docker what not to copy into the image.

Create a file named:

.dockerignore

Add this:

node_modules
.next
dist
build
.env
.git
.gitignore
Dockerfile
docker-compose.yml
npm-debug.log

This keeps the Docker image cleaner and smaller.

Step 3: Create A Dockerfile

Now create a file named:

Dockerfile

This is where we define how the app should be built and started.

Dockerfile For A Normal React/Vite App

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;"]

What This Does

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.

Dockerfile For An Astro App

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.

Dockerfile For A Next.js App

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.

Better Dockerfile For Next.js Production

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.

Step 4: Build The Docker Image

After creating the Dockerfile, run this command:

docker build -t my-app .

Here:

my-app = name of the Docker image
. = current folder

Docker will read the Dockerfile and create an image.

Step 5: Run The Container

For React/Astro static app served with Nginx:

docker run -p 8080:80 my-app

Now open:

http://localhost:8080

For Next.js app:

docker run -p 3000:3000 my-app

Now open:

http://localhost:3000

Step 6: Use Docker Compose

Instead of writing long Docker commands, we can use Docker Compose.

Create a file:

docker-compose.yml

For Next.js:

services:
  app:
    build: .
    ports:
      - "3000:3000"
    env_file:
      - .env

Now run:

docker compose up

To stop:

docker compose down

For React or Astro with Nginx:

services:
  app:
    build: .
    ports:
      - "8080:80"

Then run:

docker compose up

What About Environment Variables?

If 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:
      - .env

Important note:

Do not push your real .env file to GitHub.

Instead, create:

.env.example

Example:

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.

Common Issue: App Runs Locally But Not In Docker

This can happen because of:

  • wrong port
  • missing environment variables
  • wrong build command
  • missing files
  • app binding only to localhost
  • Node version mismatch
  • dependency issue

The first thing I usually check is logs:

docker logs container_name

Or if using Docker Compose:

docker compose logs

Logs are your best friend when debugging Docker issues.

Common Commands I Use

Build image:

docker build -t my-app .

Run container:

docker run -p 3000:3000 my-app

Run with Docker Compose:

docker compose up

Run in background:

docker compose up -d

Stop containers:

docker compose down

View logs:

docker compose logs

Rebuild:

docker compose up --build

Check running containers:

docker ps

When Should You Dockerize An App?

You should consider Docker when:

  • you want consistent setup across machines
  • your app has many dependencies
  • you are deploying to a VPS
  • you are working with a team
  • you want easier CI/CD
  • you want to run frontend, backend, database together
  • you want a clean production setup

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.

My Simple Mental Model

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.

Final Thoughts

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.

Website heavily inspired by Chánh Đại.

Learning as I build. Here's the code