Dockerfile - Node with TypeScript
Source: Dev.to

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.