Docker & Containers

Containerizing the User Service with Docker

By the Domain India teamPublished 9 min read
Knowledge base article
Contents (10 sections)

Docker packages an application together with the exact runtime and libraries it needs, so it runs the same way on your laptop, in CI and on a server. This guide containerises a Node.js user (authentication) service from a MEAN or MERN stack: a production-ready Dockerfile, how to build and run the image, how to test it, and how to run it with MongoDB using Docker Compose. The same pattern works for any Node.js API.

Key takeaways

Start from a current Node.js LTS image such as node:24-slim, copy package.json and package-lock.json first and run npm ci --omit=dev, then copy your code, switch to the non-root node user and start the app with CMD ["node", "server.js"]. Add a .dockerignore, keep secrets out of the image by passing them at run time, and make the app read PORT and listen on 0.0.0.0. Build with docker build -t user-service:1.0 . and run with docker run -p 8080:8080 --env-file .env user-service:1.0.

1. What you need

  • Docker on your computer (Docker Desktop on Windows or macOS, Docker Engine on Linux) or on a Linux server you control.
  • A Node.js service with a package.json, a committed package-lock.json and an entry file. This guide assumes server.js and a GET /health route that returns 200.
  • A current Node.js LTS line. Use Node.js 24 or 22 for new work. Node.js 20 and older are past end of life, so move any image still based on node:14, node:16 or node:18.

2. Make the app container-friendly first

Two small code changes save hours of debugging later:

javascript
// server.js
const express = require('express');
const app = express();

app.get('/health', (req, res) => res.status(200).send('ok'));
// ... your auth routes ...

const port = process.env.PORT || 8080;
const server = app.listen(port, '0.0.0.0', () => {
  console.log(`user-service listening on ${port}`);
});

// Docker sends SIGTERM on "docker stop": finish open requests, then exit.
process.on('SIGTERM', () => server.close(() => process.exit(0)));
  • Read the port from PORT and bind to 0.0.0.0. A service bound to localhost inside a container can't be reached from outside it.
  • Handle SIGTERM so stopping or redeploying the container doesn't cut requests off mid-way.
  • Log to the console, not to files. docker logs collects standard output for you.

3. Write the Dockerfile

Create a file named Dockerfile (no extension) in the project root:

dockerfile
# syntax=docker/dockerfile:1
FROM node:24-slim

ENV NODE_ENV=production
WORKDIR /app

# Install dependencies first so this layer is cached between builds
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

# Then copy the application code, owned by the non-root user
COPY --chown=node:node . .
USER node

ENV PORT=8080
EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD node -e "fetch('http://127.0.0.1:' + process.env.PORT + '/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"

CMD ["node", "server.js"]

What each part does, and why it is better than the older FROM node:14 / RUN npm install pattern:

LineWhy
FROM node:24-slimA supported LTS release on a small Debian base. Pin a more exact tag, such as a specific 24.x release, for repeatable builds.
COPY package files, then RUN npm cinpm ci installs exactly what the lockfile says, and copying only the package files first lets Docker reuse this layer until dependencies change.
--omit=devLeaves test and build tools out of the production image.
USER nodeRuns the service as the image's built-in unprivileged user instead of root.
HEALTHCHECKLets Docker mark the container unhealthy if /health stops answering. It uses Node's built-in fetch, because the slim image has no curl.
CMD in exec formThe JSON form makes Node the main process, so it receives SIGTERM directly.

If your service is written in TypeScript or needs a build step, use a multi-stage build: install all dependencies and compile in a first stage, then copy only the compiled output and production dependencies into a clean final stage.

4. Add a .dockerignore

Without it, COPY . . sends everything in the folder to the build, including your local node_modules and secrets. Create .dockerignore next to the Dockerfile:

text
node_modules
npm-debug.log
.git
.env
*.env
coverage
Dockerfile
.dockerignore
Never bake secrets into an image

Anyone who can pull an image can read every file and environment variable in it. Keep the MongoDB connection string, JWT secret and API keys out of the Dockerfile and out of the build context, and pass them when the container starts.

5. Build and run the image

bash
# Build and tag the image
docker build -t user-service:1.0 .

# Run it in the background, publish port 8080, load secrets from .env
docker run -d --name user-service \
  -p 8080:8080 \
  --env-file .env \
  --restart unless-stopped \
  user-service:1.0
  • -d runs the container in the background and --name gives it a name you can use in later commands.
  • -p 8080:8080 maps port 8080 on the host to port 8080 in the container.
  • --env-file .env loads variables such as MONGODB_URI and JWT_SECRET from a file that stays on the host.
  • --restart unless-stopped starts the container again after a crash or a reboot of the host.

6. Test the running container

  1. Check it is running.
    docker ps lists running containers. After about 30 seconds the status should show (healthy).
  2. Call the service.
    curl -i http://localhost:8080/health should return 200 ok. Then test a real route, such as registering a test user.
  3. Read the logs.
    docker logs -f user-service follows the output. Startup errors, such as a failed database connection, appear here first.
  4. Look inside if needed.
    docker exec -it user-service sh opens a shell in the container to check files and variables.
  5. Stop and clean up.
    docker stop user-service then docker rm user-service.

If docker ps shows nothing, the container exited: run docker ps -a to see its exit status and docker logs user-service to see why.

7. Run it with MongoDB using Docker Compose

For local development, Docker Compose starts the service and its database together. Create compose.yaml:

yaml
services:
  user-service:
    build: .
    ports:
      - "8080:8080"
    env_file: .env
    environment:
      MONGODB_URI: mongodb://mongo:27017/users
    depends_on:
      - mongo
    restart: unless-stopped

  mongo:
    image: mongo:8
    volumes:
      - mongo-data:/data/db
    restart: unless-stopped

volumes:
  mongo-data:

Run docker compose up -d --build to build and start both, and docker compose down to stop them. The service reaches the database by its service name, mongo, and the named volume keeps your data when containers are recreated. MongoDB is not published to the host here, which is what you want: only the API should be reachable from outside. For production, add authentication to MongoDB or use a managed MongoDB service.

8. Keep images small and safe

  • Rebuild regularly so you pick up security fixes in the base image.
  • Scan images for known vulnerabilities with a scanner such as Docker Scout or Trivy.
  • Tag releases (user-service:1.1) instead of relying only on latest, so you can roll back.

The next step in this series is running the container on Kubernetes: see Deploying the user service with Kubernetes and, for a small cluster on one server, Lightweight Kubernetes with k3s.

9. Running this on Domain India

WhereDocker?How the service runs
cPanel or DirectAdmin shared hostingNoDocker can't run on shared hosting. See Docker on cPanel and DirectAdmin.
App PlatformYou deploy code, we build and run itNode.js apps are detected automatically; other languages build from your Dockerfile. PostgreSQL is included; MongoDB is not.
VPSYes, you install itYour own self-managed Linux server with root access, where you run Docker and Compose yourself.

On the App Platform, the same two rules apply as in section 2: read PORT and listen on 0.0.0.0. There is no SSH, and you deploy from GitHub or with a deploy token; see Getting started with the App Platform. For MongoDB, use a hosted MongoDB service and test the connection from your app.

On a VPS, you get root access and install Docker yourself. You are responsible for updates, the firewall and backups.

App Developer
₹250/mo + GST
  • 512 MB RAM per app
  • 1.5 GB RAM total
  • 2 vCPU
  • 10 GB NVMe SSD
See plan details
VPS Basic
₹1,105.30/mo + GST
  • 2 vCPU
  • 4 GB DDR4 RAM
  • 128 GB NVMe SSD Storage
  • 3 TB Monthly Bandwidth
See plan details

Frequently asked questions

Which Node.js version should my Docker image use?

A current LTS line, Node.js 24 or 22, for example node:24-slim. Node.js 20 and older are past end of life, so update images based on them.

Should I use npm install or npm ci in a Dockerfile?

Use npm ci. It installs exactly the versions in package-lock.json and fails if the lockfile and package.json disagree, which makes builds repeatable. Add --omit=dev for production images.

Why can't I reach my app on the published port?

Usually the app is listening on localhost or 127.0.0.1 inside the container. Make it listen on 0.0.0.0 and on the port in the PORT variable, and check the -p mapping in docker run.

How do I pass secrets to a Docker container?

Pass them at run time with --env-file or -e, or through your platform's secrets or environment settings. Never write them into the Dockerfile or copy a .env file into the image.

Can I run Docker on Domain India shared hosting?

No. Docker needs kernel features that shared hosting does not allow. Use the App Platform, which builds and runs your app for you, or a VPS where you install Docker yourself.

Does the App Platform need a Dockerfile for a Node.js app?

No. Node.js apps are detected automatically from package.json. Python, PHP and other languages need a Dockerfile in the project root.

Ready to deploy your container? Compare App Platform plans and VPS plans, or open a support ticket if you're not sure which fits your service.

Deploy your service on the App Platform

Deploy from GitHub or with a deploy token, with PostgreSQL and free SSL included on every plan.

See App Platform plans

Was this article helpful?

Your answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app