Moniruzzaman Saikat

Posted Sep 29, 2026 · 5 min read · 0 views

Report

Docker for Beginners: Containerize a Node.js App with PostgreSQL

"It works on my machine" is one of the oldest jokes in software, and one of the most expensive problems. Different Node versions, missing system packages, and mismatched database setups waste hours every week.

Docker solves this by packaging your app and everything it needs into a container that runs the same way everywhere. In this tutorial you will containerize a Node.js API, add a PostgreSQL database with Docker Compose, and learn the habits that keep your images small and secure.

What you need

  • Docker Desktop (Windows or macOS) or Docker Engine (Linux)
  • Node.js installed locally, only to create the starter project
  • A code editor and a terminal

Verify the installation:

docker --version
docker compose version

Step 1: Create a small Node.js app

Create a folder and initialize the project:

mkdir docker-demo && cd docker-demo
npm init -y
npm install express pg

Create index.js:

const express = require('express');
const { Pool } = require('pg');

const app = express();
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

app.get('/', (req, res) => {
  res.json({ message: 'Hello from Docker' });
});

app.get('/time', async (req, res) => {
  try {
    const result = await pool.query('SELECT NOW() AS now');
    res.json(result.rows[0]);
  } catch (err) {
    res.status(500).json({ error: 'Database unavailable' });
  }
});

const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`Listening on port ${port}`));

The /time route proves that the app can talk to the database, which we will add in a moment.

Step 2: Write the Dockerfile

A Dockerfile is a recipe for building your image. Create a file named Dockerfile with no extension:

FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --omit=dev

COPY . .

EXPOSE 3000
USER node
CMD ["node", "index.js"]

Here is why each part matters:

  • node:20-alpine is a small base image, so downloads and builds are faster.
  • Copying package*.json first lets Docker cache the dependency layer. If you only change your code, npm ci does not run again.
  • npm ci --omit=dev installs exact locked versions and skips dev dependencies.
  • USER node runs the app as a non-root user, which limits damage if the app is compromised.

Step 3: Add a .dockerignore file

Without this file, Docker copies everything into your image, including node_modules and secrets. Create .dockerignore:

node_modules
npm-debug.log
.git
.env
Dockerfile
docker-compose.yml

This keeps images small and stops your .env file from leaking into a published image.

Step 4: Build and run the container

docker build -t docker-demo .
docker run -p 3000:3000 docker-demo

Open http://localhost:3000 and you should see the JSON greeting. The -p 3000:3000 flag maps port 3000 on your machine to port 3000 in the container.

Press Ctrl + C to stop it. The /time route will fail for now, since no database exists yet.

Step 5: Add PostgreSQL with Docker Compose

Running several containers by hand gets messy. Docker Compose lets you describe your whole stack in one file. Create docker-compose.yml:

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://appuser:secret@db:5432/appdb
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: appdb
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  pgdata:

A few important details:

  • The host is db, not localhost. Inside Compose, services reach each other by service name.
  • The named volume pgdata keeps your database files, so data survives when containers are removed.
  • The healthcheck makes the app wait until PostgreSQL is truly ready, not just started.

Start everything:

docker compose up --build

Visit http://localhost:3000/time and you will see the current database time. Your app and database are now running together with a single command.

Useful commands you will use daily

docker compose up -d          # start in the background
docker compose logs -f app    # follow logs for one service
docker compose ps             # see running services
docker compose down           # stop and remove containers
docker compose down -v        # also delete volumes (wipes the database)
docker exec -it <container> sh  # open a shell inside a container

Be careful with down -v. It deletes your data.

Development tip: live reload with bind mounts

Rebuilding the image after every code change is slow. For local development, add a bind mount and a watcher in a separate override file, docker-compose.override.yml:

services:
  app:
    command: npx nodemon index.js
    volumes:
      - .:/app
      - /app/node_modules

Install nodemon as a dev dependency first (npm install --save-dev nodemon), and note that the Dockerfile above omits dev dependencies, so for development build with npm ci instead. Many teams keep a separate Dockerfile.dev for this reason.

Best practices checklist

  1. Pin image versions (node:20-alpine), never use latest in production
  2. Keep secrets out of images and out of Git, and use environment variables or a secrets manager
  3. Use a .dockerignore file in every project
  4. Run as a non-root user
  5. Use multi-stage builds when your app needs a compile step, such as TypeScript
  6. Add healthchecks so orchestration tools know when a service is really ready

Common mistakes

  • Using localhost for the database host. Inside a container, localhost means the container itself.
  • Forgetting to expose or map ports. The app runs but you cannot reach it.
  • Storing data without a volume. The database resets every time the container is recreated.
  • Copying node_modules from your machine. Native modules built on Windows or macOS may not work on Linux.

Final thoughts

Docker feels like extra work at first, but it pays off quickly. New teammates run one command and get a working environment, your staging server matches production, and deployments become predictable. Start with a Dockerfile and a Compose file for your next project, and add more only when you need it.

Are you already using Docker in your projects? Tell us how you set it up in the comments.

Be the first to react
0

Written by

Moniruzzaman Saikat

Software Engineer at TheSoftking Ltd

Software engineer who loves building useful things, solving hard problems, and turning ideas into scalable products. Always learning, shipping, and experimenting with new tech.

Founding MemberNew MemberProlific Writer

14 articles · Dhaka Bangladesh · Joined Sep 2026

Discussion (0)

Sign in to join the discussion.