NitroStack Logo
/python
/deployment
/docker

Docker Deployment (Python)

Time to read: 10 minutes

Package your Python MCP server as a container for any platform.

What You'll Learn

  • A basic Dockerfile
  • Multi-stage builds
  • Environment variables and secrets
  • Docker Compose
  • Health checks, non-root user, volumes
  • Troubleshooting

Basic Dockerfile

Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir nitrostack && pip install --no-cache-dir -r requirements.txt
ENV HOST=0.0.0.0 MCP_TRANSPORT_TYPE=http PORT=3000
EXPOSE 3000
CMD ["nitrostack-py", "start", "--port", "3000"]

The container must use the HTTP transport. stdio has no network port to expose.

Build & Run

Build Image

Bash
docker build -t my-mcp-server .

Run Container

Bash
docker run --rm -p 3000:3000 --env-file .env my-mcp-server

Check it:

Bash
curl http://localhost:3000/mcp/health

Multi-Stage Build

Build dependencies in one stage and copy only the virtualenv into a slim runtime image:

Dockerfile
# Build stage
FROM python:3.12-slim AS build
WORKDIR /app
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir nitrostack -r requirements.txt

# Production stage
FROM python:3.12-slim
WORKDIR /app
COPY --from=build /opt/venv /opt/venv
COPY . .
ENV PATH="/opt/venv/bin:$PATH" \
    PYTHONUNBUFFERED=1 \
    HOST=0.0.0.0 \
    MCP_TRANSPORT_TYPE=http \
    PORT=3000
EXPOSE 3000
CMD ["nitrostack-py", "start", "--port", "3000"]

With uv

Dockerfile
FROM python:3.12-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
COPY pyproject.toml uv.lock* ./
RUN uv sync --no-dev
COPY . .
ENV PATH="/app/.venv/bin:$PATH" HOST=0.0.0.0 MCP_TRANSPORT_TYPE=http PORT=3000
EXPOSE 3000
CMD ["nitrostack-py", "start", "--port", "3000"]

Environment Variables

Do not copy .env into the image. Pass secrets at runtime.

Bash
# From a file
docker run --rm -p 3000:3000 --env-file .env my-mcp-server

# Individually
docker run --rm -p 3000:3000 \
  -e API_KEY=$API_KEY \
  -e JWT_SECRET=$JWT_SECRET \
  my-mcp-server

Common variables: HOST, PORT, MCP_TRANSPORT_TYPE, MCP_MAX_SESSIONS, API_KEY, JWT_SECRET, OAUTH_REQUIRED.

Docker Compose

Yaml
# docker-compose.yml
services:
  mcp-server:
    build: .
    ports:
      - "3000:3000"
    env_file:
      - .env
    environment:
      HOST: 0.0.0.0
      MCP_TRANSPORT_TYPE: http
      PORT: "3000"
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:3000/mcp/health')"]
      interval: 30s
      timeout: 5s
      retries: 3
Bash
docker compose up -d
docker compose logs -f mcp-server

Production Optimizations

Health Checks

python:3-slim has no curl, so use Python itself:

Dockerfile
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:3000/mcp/health')"

Non-Root User

Dockerfile
RUN useradd --create-home --uid 1001 mcp
USER mcp

Security

Dockerfile
# Patch the base image
RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/*

Also pin the base image tag, and keep .env and .git out of the build context.

Persistent Storage

Bash
docker run --rm -p 3000:3000 -v mcp-data:/app/data my-mcp-server

Containers are disposable. Keep any state your server writes on a volume or in an external database.

Networking

Run your server next to its dependencies on one Compose network and use service names as hostnames:

Yaml
services:
  mcp-server:
    build: .
    environment:
      DATABASE_URL: postgresql://app:secret@db:5432/app
    depends_on:
      - db
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret

Best Practices

  1. Use HOST=0.0.0.0, otherwise the port is unreachable from outside the container
  2. Use the http transport in containers
  3. Never bake secrets into the image
  4. Pin Python and dependency versions
  5. Run as a non-root user
  6. Add a health check on /mcp/health
  7. Log to stderr/stdout of the container, not files, unless you mount a volume

Example .dockerignore

.git
.venv
__pycache__
*.pyc
.env
.env.*
dist
tests
.pytest_cache

Troubleshooting

Container Crashes

Bash
docker logs <container>
docker ps -a

Common causes: missing env vars, a Python version below 3.10, or the app still on stdio transport (set MCP_TRANSPORT_TYPE=http).

Port Already in Use

Bash
docker run --rm -p 8080:3000 my-mcp-server

Server Starts but Is Unreachable

HOST is probably 127.0.0.1. Set HOST=0.0.0.0.

Client Gets a Redirect on /mcp

Call /mcp without a trailing slash. The MCP Inspector requires that form.

Permission Issues

Bash
# Fix volume permissions for the non-root user
docker run --rm -v mcp-data:/data alpine chown -R 1001:1001 /data

Next Steps