This guide explains how to build, run, and persist a Node.js + Express application inside Docker — with support for MongoDB, PostgreSQL, and file uploads — and also clarifies the relationship between images, containers, and volumes.
1. Big Picture: Images → Containers → Volumes
If Docker was a kitchen:
Docker Concept | Kitchen Analogy | Purpose |
---|---|---|
Image | Recipe | Instructions + ingredients for your app |
Container | Prepared Meal | A running instance of the recipe |
Volume | Fridge/Pantry | Where persistent ingredients are kept |
Key rules:
- Images are read-only.
- Containers are ephemeral — delete them, data is gone unless stored in a volume.
- Volumes survive container restarts and recreate.
Diagram: How They Work Together
Image: my-node-app Image: mongo:6 Image: postgres:16
+----------------------+ +---------------------+ +---------------------+
| code + deps | | mongo binary + init | | postgres binary + init|
+----------------------+ +---------------------+ +---------------------+
| | |
v v v
Container: app Container: mongo Container: postgres
(ephemeral FS) (/data/db) (/var/lib/postgresql)
| | |
v v v
Volume: uploads Volume: mongo_data Volume: pg_data
(user files) (MongoDB storage) (Postgres storage)
2. Why Use Docker for Node.js Apps
- Consistent environment for development, testing, and production.
- No "works on my machine" headaches.
- Easy to ship and run anywhere.
- Built-in isolation between services (app, DB, admin tools).
3. Project Structure Example
myapp/
src/
index.js
routes/
package.json
Dockerfile
docker-compose.yml
.env
uploads/ # Will be mounted to a volume in production
4. Dockerfile for Node.js + Express
FROM node:20-alpine
# Create app directory
WORKDIR /app
# Install dependencies first (layer caching)
COPY package*.json ./
RUN npm ci --omit=dev
# Copy app code
COPY . .
# Environment variables
ENV NODE_ENV=production
ENV PORT=3000
# Create uploads folder and fix permissions
RUN mkdir -p /app/uploads && chown -R node:node /app
USER node
EXPOSE 3000
CMD ["node", "src/index.js"]
5. docker-compose.yml with MongoDB, PostgreSQL & File Uploads
services:
app:
build: .
env_file: .env
ports:
- "3000:3000"
volumes:
- uploads:/app/uploads
depends_on:
- mongo
- postgres
restart: unless-stopped
mongo:
image: mongo:6
volumes:
- mongo_data:/data/db
restart: unless-stopped
mongo-express:
image: mongo-express:1
ports:
- "8081:8081"
environment:
- ME_CONFIG_MONGODB_SERVER=mongo
- ME_CONFIG_BASICAUTH=false
depends_on:
- mongo
postgres:
image: postgres:16
environment:
- POSTGRES_DB=mydb
- POSTGRES_USER=app
- POSTGRES_PASSWORD=app
volumes:
- pg_data:/var/lib/postgresql/data
restart: unless-stopped
pgadmin:
image: dpage/pgadmin4:8
ports:
- "8082:80"
environment:
- PGADMIN_DEFAULT_EMAIL=admin@example.com
- PGADMIN_DEFAULT_PASSWORD=secret
depends_on:
- postgres
volumes:
uploads:
mongo_data:
pg_data:
6. Environment File Example (.env)
NODE_ENV=production
PORT=3000
DB_CLIENT=mongo
MONGO_URI=mongodb://mongo:27017/mydb
# PostgreSQL alternative
POSTGRES_URI=postgres://app:app@postgres:5432/mydb
7. How to Run
# Build and start all services
docker compose up -d --build
# Stop everything
docker compose down
# View logs
docker compose logs -f app
# Enter app container
docker compose exec app sh
8. Volumes in Practice
- uploads → holds all user-uploaded files.
- mongo_data → stores MongoDB’s database files.
- pg_data → stores PostgreSQL’s database files.
Even if you run:
docker compose down
docker compose up -d
Your DB data and uploaded files remain intact because they’re in named volumes.
9. Security Tips
- Never commit
.env
files with passwords to Git. - Do not expose DB ports to the internet in production.
- Use specific image versions (
mongo:6.0.13
, not justmongo:latest
). - Run app as a non-root user in Dockerfile.
10. Final Takeaways
- Image = blueprint for your app.
- Container = running copy of that blueprint.
- Volume = persistent storage for data that must survive restarts.
- Use docker-compose to orchestrate app + DB + admin tools.
- Always separate dev and prod configs.
121 Docker Commands (with Explanations)
Build the image
docker build -t myapp .
-t myapp
→ tags the image name.
Run a container
docker run -d --name myapp-container -p 3000:3000 myapp
-d
→ detached mode.-p 3000:3000
→ map port 3000 host → container.
List containers
docker ps
Stop a container
docker stop myapp-container
Remove a container
docker rm myapp-container
List images
docker images
Remove an image
docker rmi myapp
List volumes
docker volume ls
Inspect a volume
docker volume inspect uploads
Remove a volume
docker volume rm uploads
12. Running with Docker Compose
Start all services
docker compose up -d --build
Stop all services
docker compose down
Stop and remove with volumes
docker compose down -v
Check logs
docker compose logs -f app
13. Migrating to Another Server
Backup volumes
docker run --rm -v uploads:/data -v $PWD:/backup alpine \
tar -czf /backup/uploads.tar.gz -C /data .
docker run --rm -v mongo_data:/data -v $PWD:/backup alpine \
tar -czf /backup/mongo_data.tar.gz -C /data .
Restore volumes
docker run --rm -v uploads:/data -v $PWD:/backup alpine \
tar -xzf /backup/uploads.tar.gz -C /data
docker run --rm -v mongo_data:/data -v $PWD:/backup alpine \
tar -xzf /backup/mongo_data.tar.gz -C /data
14. Summary of Best Practices
- Use named volumes for persistence (DB, uploads).
- Separate app, DB, and admin into their own containers.
- Keep sensitive data in
.env
, not in code. - Use
docker-compose.yml
for multi-service orchestration. - Always
.dockerignore
node_modules
if installing inside the container. - Avoid running containers as
root
in production. - Use lightweight base images (
node:alpine
).