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:
-
Using only a Dockerfile (manually running
docker buildanddocker run) -
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
/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
❗ “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_modulesstay 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 |