本セクションの内容:
How to Dockerize MCP Servers in JavaScript
MCP Servers are easily pluggable extensions to LLMs, but sometimes are a pain to set up because they require a development environment. Let’s work around that by providing a Docker image for our MCP Servers.
In a previous post, I explained how to run MCP Servers with Docker so that, as a user who wants to connect MCP Servers, you don’t need to struggle with installing the right Python version or figuring out how to get npm or npx installed for JavaScript-based MCP Servers.
In this post, we’ll focus on you as the developer of MCP Servers and look at how simple it is to provide MCP consumers with a Docker image they can use. We will achieve the following MCP development setup:
Set up a
Dockerfilefor your MCP ServerCreate a GitHub Action workflow that builds the Docker image
Create a GitHub Action workflow that pushes the Docker image to GitHub Packages
We assume the MCP Server project is built in JavaScript (or TypeScript) and is based on Node.js development environment.
As a pre-requisite, you’ll need a Node.js development environment, the MCP project you want to dockerize, and the Docker Desktop application available locally.
Set up a Dockerfile for your MCP Server
A Dockerfile is the building block for Docker images. It defines how an image is built and how it is executed when a container is created.
Create the following Dockerfile:
1FROM node:22-slim AS builder
2WORKDIR /app
3COPY . /app
4RUN --mount=type=cache,target=/root/.npm npm install
5RUN --mount=type=cache,target=/root/.npm npm run build
6RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev
7
8FROM node:22-slim AS release
9WORKDIR /app
10COPY --from=builder /app/dist /app/dist
11COPY --from=builder /app/package.json /app/package.json
12COPY --from=builder /app/package-lock.json /app/package-lock.json
13ENV NODE_ENV=production
14RUN npm ci --ignore-scripts --omit-dev
15ENTRYPOINT ["node", "/app/dist/bin/cli.cjs"]What is happening in this Dockerfile?
This is a multi-step Docker image build workflow that follows the best practice of separating the image used for building the project from the one used for instantiating a Docker container. It follows the 10 Node.js best practices to containerize Node.js applications with Docker.
It defines a
/appdirectory, which is where the MCP Server project files will exist in the containerWhen the Docker container executes, it will run the
/app/dist/bin/cli.cjsfile as an argument to thenoderuntime. Likely, you’ll want to change this to match your MCP Server entry point, such asindex.jsorserver.js.
What next? Now that we have a Dockerfile we can build it locally and execute it to ensure it actually works. Here is how to build this Docker image (you can change my-mcp-server with the name you want to tag this Docker image as):
1docker build -t my-mcp-server .Run the above command in the project’s root directory, where you should also place your Dockerfile at and a new Docker image will be built.
How does the MCP Server run from a Docker image? Use this command:
1docker run -i --rm --init -e DOCKER_CONTAINER=true my-mcp-serverWe are running a new container from the my-mcp-server Docker image that we built before. We then use the -i and --rm arguments to tell Docker that this is an interactive process that connects to STDIO, and once the container gets killed, it also gets removed in a nice, clean up process.
How do users consume an MCP Server via Docker?
Now that we have an MCP Server, how would MCP clients and host applications consume it? How do they configure the command to run if it isn’t Python’s ux, pip, or Node.js’s npx?
They do need Docker to be available, and likely many developers will have either Docker or a Docker-compatible alternative like Podman. Once they have Docker, they can configure the MCP Server as follows:
1{
2 "mcpServers": {
3 "my-mcp-server": {
4 "command": "docker",
5 "env": {},
6 "args": [
7 "run",
8 "-i",
9 "--rm",
10 "my-mcp-server"
11 ]
12 }
13}Almost! This isn’t a complete example, and it will not work for MCP users. You might have noticed that we reference the my-mcp-server in the Docker command, but how does Docker know where to get this image from, and did we even publish it there?
Right, this is the part that’s missing. We created a Dockerfile, we built a Docker image locally but we didn’t publish it anywhere. To make things easy for software developers building on GitHub we will publish the image to the GitHub Packages registry which can host Docker images. In fact, this is exactly what the official GitHub MCP Server does as well.
Create a GitHub Action workflow that builds the Docker image and publish to GitHub Packages
We will start with creating a GitHub Action workflow that creates a Docker image, and will have the following steps in the CI workflow:
Build the Docker image
Publish the Docker image to GitHub Packages
Sign the Docker image
Following is the complete workflow file that you should create in the following directory in your project’s root: .github/workflows/docker-publish-to-github.yml.
Before running and saving this file, you’ll want to modify the IMAGE_NAME environment variable to use your own Docker image name instead of the example reference we use of my-mcp-server:
1name: "Docker: GitHub Packages"
2
3on:
4 push:
5 branches:
6 - main
7
8env:
9 REGISTRY: ghcr.io
10 IMAGE_NAME: ${{ github.repository_owner }}/my-mcp-server
11
12jobs:
13 build_and_publish:
14 runs-on: ubuntu-latest
15 permissions:
16 contents: write
17 packages: write
18 id-token: write
19 attestations: write
20
21 steps:
22 - name: Checkout repository
23 uses: actions/checkout@v4
24
25 - name: Log into registry ${{ env.REGISTRY }}
26 uses: docker/login-action@v3
27 with:
28 registry: ${{ env.REGISTRY }}
29 username: ${{ github.repository_owner }}
30 password: ${{ secrets.GITHUB_TOKEN }}
31
32 - name: Extract Docker metadata
33 id: meta
34 uses: docker/metadata-action@v5
35 with:
36 images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
37 tags: |
38 type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
39 type=ref,event=pr
40 flavor: |
41 prefix=
42 suffix=
43
44 - name: Build and push Docker image
45 id: push
46 uses: docker/build-push-action@v6
47 with:
48 context: .
49 push: true
50 tags: ${{ steps.meta.outputs.tags }}
51 labels: ${{ steps.meta.outputs.labels }}
52
53 - name: Generate artifact attestation
54 uses: actions/attest-build-provenance@v2
55 with:
56 subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
57 subject-digest: ${{ steps.push.outputs.digest }}
58 push-to-registry: trueYou will also notice that the above GitHub Actions workflow file for building the Docker image assumes the trunk branch name for the branch is main.
Next, you’ll need to provide GitHub Actions runners with a write permission. To do that, go to the repository Settings -> Actions -> General and make sure you toggle read and write permissions as follows:

Building more with MCPs?
Curious where to proceed from here? I’ve lined up a few helpful resources to help guide you through productive understanding and security of MCP Servers:
A Beginner's Guide to Visually Understanding MCP Architecture
From prompt injection to other security vulnerabilities, MCP Security – What's Old is New Again
October 22, 2025
DevSecCon2025 - The AI Security Summit
Secure your spot and secure the shift to AI native