Skip to content

includes:

✔ Full single-container Express setup

✔ Multi-stage Dockerfile

✔ Dev & Prod compose files

Running WITHOUT docker-compose (basic Dockerfile only)

Exact explanation: When to use Dockerfile ONLY vs Docker Compose


🐳 Dockerizing a Simple Express.js Application — Single Container, Dev & Prod, With and Without Docker Compose

Dockerizing an Express.js app can be done in two ways:

  1. Using only a Dockerfile (manually running docker build and docker run)

  2. Using Docker Compose (recommended, even for a single container)

This guide covers both approaches, explains when to use each, and gives you a production-ready setup.


📁 Project Structure

/express-docker

├── server.js

├── package.json

├── Dockerfile

├── docker-compose.dev.yml

└── docker-compose.prod.yml

🚀 1. Express Server (Single Container)

server.js

const  express  =  require("express");

const  app  =  express();



const  PORT  = process.env.PORT  ||  3000;



app.get("/", (req, res) => {

res.send("Hello from Dockerized Express App!");

});



app.listen(PORT, () => {

console.log(`Server running on port ${PORT}`);

});

📦 2. package.json

{

"name": "express-docker",

"version": "1.0.0",

"scripts": {

"start": "node server.js",

"dev": "nodemon server.js"

},

"dependencies": {

"express": "^4.19.2"

},

"devDependencies": {

"nodemon": "^3.0.0"

}

}  

🏗️ 3. Multi-Stage Dockerfile (Dev + Prod)

This same Dockerfile supports:

  • Development build

  • Production build

  • Light-weight runtime image

Dockerfile

# -------------------------

# BASE STAGE

# -------------------------

FROM node:18-alpine AS base

WORKDIR /app



# -------------------------

# DEVELOPMENT STAGE

# -------------------------

FROM base AS development

COPY package*.json ./

RUN npm install

COPY . .

CMD ["npm", "run", "dev"]



# -------------------------

# PRODUCTION BUILD STAGE

# -------------------------

FROM base AS build

COPY package*.json ./

RUN npm install --only=production

COPY . .



# -------------------------

# PRODUCTION RUNTIME

# -------------------------

FROM node:18-alpine AS production

WORKDIR /app

COPY --from=build /app /app  

EXPOSE 3000

CMD ["npm", "start"]

✅ 2. You can ignore production stages during development

    --target=development

# **“Stop building after the** `development` **stage. Don’t continue to the production stages.”**
So Docker builds only the dev environment  the one that runs `npm run dev`.

docker build --target=development -t myapp-dev .
docker run -p 3000:3000 myapp-dev

Ensures dev and prod environments stay consistent

Even though you only use the development stage locally, the Dockerfile ensures:

  • Same Node version

  • Same base image

  • Same folder structure

So you avoid the classic “works on my machine” problem.

⚙️ 4. Running Express WITHOUT Docker Compose

If you don’t want docker-compose, you can use plain Docker commands.

Build image

docker build -t express-app .

Run in production mode

docker run -p 3000:3000 express-app

Visit:

http://localhost:3000  

❗ Want Dev Mode (hot reload) WITHOUT compose?

Yes, possible: But it is ugly, hard to maintain, and breaks easily.

docker run -p 3000:3000 -v .:/app -v /app/node_modules express-app npm run dev  
### lets see why /app/node_modules is used here
when we write
-v .:/app

you are mounting your entire project folder from your host into the container. That’s good for live reload, but it has a side effect:

❗ It overwrites everything inside /app — including node_modules.

So if Docker installed node_modules inside the container, they get hidden by your host folder (which usually does NOT contain Linux-compatible node_modules).

This breaks your app.

That’s why we use Docker Compose for dev.

-v /app/node_modules
tells Docker:

❗ “Create a separate internal volume for /app/node_modules so it does NOT get overwritten by the host bind mount.”

Result:

  • Your code comes from your host (-v .:/app)

  • Your node_modules stay inside the container (-v /app/node_modules)

  • Everything works smoothly


🐳 5. Docker Compose for Development (Recommended)

✔ Hot reload

✔ Mount local code

✔ No need to re-build image every time

docker-compose.dev.yml

version: "3.8"

services:

app:

build:

context: .

target: development

container_name: express_dev

ports:

- "3000:3000"

volumes:

- .:/app

- /app/node_modules

command: ["npm", "run", "dev"]  

Run:

docker compose -f docker-compose.dev.yml up --build 

🏭 6. Docker Compose for Production

✔ Smaller image

✔ No volumes

✔ Clean immutable container

docker-compose.prod.yml

version: "3.8"  

services:

app:

build:

context: .

target: production

container_name: express_prod

ports:

- "3000:3000"  

Run:

docker compose -f docker-compose.prod.yml up --build -d 

❓ When Should You Use Only a Dockerfile?

Use a simple Dockerfile only when:

✅ You have a single container

✅ You only need production mode

✅ You deploy manually (AWS ECS, Docker Swarm, Rancher, Kubernetes)

✅ You don't need hot reload

❌ You don’t need multiple environment modes

❌ You don’t need volumes

Typical use-case:

You want to deploy a single Express app in production on a server.


❓ When Should You Use Docker Compose (even for a single container)?

Use docker-compose when:

✅ You want development mode (hot reload)

✅ You need mounting volumes

✅ You want dev & prod configs separately

✅ You don't want long docker run commands

✓ You plan to add more containers later (DB, Redis, Nginx, etc.)

Example:

docker run -v .:/app -v /app/node_modules -p 3000:3000 ...  

is replaced with a clean YAML file:

docker compose -f docker-compose.dev.yml up  

🧠 Summary Table

Feature Dockerfile Only Docker Compose
Production runs ✅ Yes ✅ Yes
Development hot reload ❌ Hard ✅ Easy
Multi-container support ❌ No ✅ Yes
Clean configs ❌ No ✅ Yes
Volumes ❌ Hard ✅ Easy
Scaling later ❌ Painful ✅ Built-in