Dockerfile - Node with TypeScript

Published: (December 20, 2025 at 04:38 PM EST)
2 min read
Source: Dev.to

Source: Dev.to

Cover image for Dockerfile - Node with TypeScript

Overview

Good morning! Hope you’re all doing well. My fellow tech enthusiasts from the land of AI hype still waiting for them to take our jobs so we can finally retire, right? 🙏

While that doesn’t happen, I’d like to share a Dockerfile structure I’ve been working on. In my case, the initial image was around 1 GB, and after some tweaks it dropped to about 161 MB (according to Dive). It’s worth noting this was for a simple app built with Hono.

Note: Keep in mind that your application might not end up the exact same size. It could be larger or smaller depending on your codebase and the dependencies installed in your project.

FROM node:lts-alpine AS builder
WORKDIR /usr/src/app
COPY --chown=node:node package*.json tsconfig.json ./
COPY --chown=node:node /src ./src
RUN npm ci --silent
RUN npm run build
RUN npm ci --silent --omit=dev 

FROM node:lts-alpine AS runner
WORKDIR /usr/src/app
COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY --from=builder /usr/src/app/dist ./dist
USER node
EXPOSE 3000
ENTRYPOINT ["node", "./dist/index.js"]

Usage

Exposing the application

For your application to be accessible, it must listen on the correct network interface (e.g., 0.0.0.0). You also need to publish the container ports to the host machine.

docker run image_name:image_tag -p 3000:3000

If you are using Docker Compose, the ports field handles this for you:

services:
  node_api:
    build: .
    ports:
      - "3000:3000"

Breaking it down

1. Multi‑stage build

The first stage (the build phase) downloads dependencies and compiles the application. After transpilation, dev dependencies are removed to shrink the final image size.

2. Base image

I’m using the latest lts-alpine image for simplicity, but a best practice is to pin both the Node and Alpine versions—or even use an image digest—to make builds more predictable.

FROM node:24.12.0-alpine3.23 AS builder

or

FROM node@sha256:c921b97d4b74f51744057454b306b418cf693865e73b8100559189605f6955b8 AS builder

3. Security & execution

In the second stage, only the files required at runtime are copied. The user is switched to node (the default non‑root user in official images). Running as a non‑root user mitigates security risks.

4. Entrypoint

Port 3000 is exposed (adjust as needed) and the ENTRYPOINT runs the transpiled JavaScript directly. You could also invoke a script defined in package.json if you prefer.

Goodbye! 👏

That’s it! Thanks for reading. I hope this article was helpful. While this model might not solve every single case, it serves as a solid foundation for using Docker with Node and TypeScript.

Back to Blog

Related posts

Read more »