# syntax=docker/dockerfile:1.7 # # sap-mcp-bridge # ------------------------------------------------------------------- # One container, five SAP MCP servers, all reachable through a single # external HTTP port via an internal nginx reverse proxy. This makes # the container deployable behind Traefik/Runtipi where only one port # per app can be exposed. # # External (single port — Runtipi binds it): # / landing page # /healthz aggregate health probe # /cap/mcp sap-cap (cap-js-mcp-server) # /cap/healthz # /abap/mcp sap-abap-adt (mcp-abap-adt) # /abap/healthz # /odata/mcp sap-odata (btp-sap-odata-to-mcp-server) # /odata/healthz # /ui5/mcp ui5-mcp-server (ui5mcp) # /ui5/healthz # /fiori/mcp fiori-mcp-server (fiori-mcp) # /fiori/healthz # # Internally: # 127.0.0.1:9001..9005 individual supergateway HTTP endpoints # 127.0.0.1:8080 nginx (the only port EXPOSEd) # ------------------------------------------------------------------- FROM node:24-bookworm-slim ENV NODE_ENV=production \ NPM_CONFIG_FUND=false \ NPM_CONFIG_AUDIT=false \ NPM_CONFIG_UPDATE_NOTIFIER=false # tini : PID-1 init that reaps zombies and forwards SIGTERM cleanly # nginx : in-container reverse proxy (single externally-visible port) # gosu : drop privileges from root → mcp after we've chowned /workspace # curl : healthcheck client RUN apt-get update \ && apt-get install -y --no-install-recommends \ tini ca-certificates curl nginx gosu \ && rm -rf /var/lib/apt/lists/* /var/log/nginx/* # Install all MCP servers + the stdio<->HTTP bridge globally. # All versions pinned for reproducibility — a broken supergateway or # MCP-server release would silently break every endpoint on the next # rebuild. Bump deliberately, not by accident. RUN npm install -g --omit=dev --no-audit --no-fund \ supergateway@3.4.3 \ @iflow-mcp/cap-js-mcp-server@0.0.3 \ @iflow-mcp/mcp-abap-adt@1.1.0 \ @iflow-mcp/btp-sap-odata-to-mcp-server@1.0.0 \ @ui5/mcp-server@0.2.11 \ @sap-ux/fiori-mcp-server@0.6.49 # Non-root runtime user. Container still starts as root so the # entrypoint can chown /workspace; it then drops to this user via gosu. # (No -r flag: Debian's SYS_UID_MAX is 999, and we want UID 10001 — the # warning useradd -r produces is harmless but ugly in build logs.) RUN useradd -u 10001 -g nogroup -d /home/mcp -m mcp COPY nginx.conf /etc/nginx/nginx.conf COPY entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh # /workspace is the project root the cap / ui5 / fiori servers operate on. # It's the volume mount target — host-side it's bound to a user-config path # from the Runtipi compose. Pre-creating it here makes "no volume bound" # (e.g. local docker run) still work. RUN mkdir -p /workspace && chown -R mcp:nogroup /workspace WORKDIR /workspace EXPOSE 8080 # Cheap liveness: nginx aggregates /healthz; if it answers 200, the # container is "alive enough" from Traefik's perspective. HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ CMD curl -fsS http://127.0.0.1:8080/healthz || exit 1 ENTRYPOINT ["/usr/bin/tini", "--"] CMD ["/usr/local/bin/entrypoint.sh"]