Showing Posts From
Docker
- 14 Apr, 2026
- 4 min read
Setting up the Penpot MCP Server with Docker and Traefik (Self-Hosted)
A walkthrough of getting the official Penpot MCP server running as a Docker container behind a Traefik reverse proxy, so Claude Code (or any MCP client) can interact with a self-hosted Penpot instance. Environment:Self-hosted Penpot 2.14 via Docker Compose Traefik v2 reverse proxy Domain: custom subdomain (mcp.example.com) MCP client: Claude Code CLIArchitecture The @penpot/mcp package runs three services inside one container:Port Purpose4400 Plugin UI (Vite dev server) — serves manifest.json and plugin.js4401 MCP HTTP server — what Claude Code connects to4402 WebSocket server — what the Penpot browser plugin connects toThe browser plugin (loaded inside Penpot) connects via WebSocket to port 4402. Claude Code connects via HTTP to port 4401. The manifest and plugin JS are served from port 4400.Hickup 1: sh: pnpm: not found Problem: The penpot-mcp binary runs corepack pnpm run bootstrap on every startup. The bootstrap script spawns a subshell running pnpm -r install && pnpm run build && pnpm run start. Even after installing pnpm, the subshell couldn't find it. Attempts that failed:npm install -g pnpm — pnpm installed but not visible in bootstrap subshell corepack enable alone — creates a shim but doesn't download the actual pnpm binaryFix: Use corepack prepare pnpm@latest --activate — this actually downloads and activates the pnpm binary, not just a shim. Also switch from node:22-alpine (busybox ash) to node:22-slim (Debian) for more predictable shell PATH behavior. FROM node:22-slim ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable && corepack prepare pnpm@latest --activate && npm install -g @penpot/mcp@latestHickup 2: Vite blocks the custom domain Problem: The plugin UI (port 4400) is a Vite dev server. Vite 6+ blocks requests with a Host header that isn't localhost. When Traefik forwarded https://mcp.example.com/manifest.json to the container, Vite returned: Blocked request. This host ("mcp.example.com") is not allowed. To allow this host, add "mcp.example.com" to `preview.allowedHosts` in vite.config.js.Fix: Add a Traefik middleware that rewrites the Host header to localhost:4400 before the request reaches Vite. Vite always allows localhost. - traefik.http.middlewares.penpot-mcp-host.headers.customrequestheaders.Host=localhost:4400 - traefik.http.routers.penpot-mcp-plugin.middlewares=penpot-mcp-hostHickup 3: Plugin manifest URL rejected by Penpot Problem: Penpot plugin manager said "The plugin doesn't exist or the URL is not correct" when entering the manifest URL. Root cause 1: Using http:// URL from an HTTPS Penpot instance. Browsers block mixed content — Penpot's frontend JS cannot fetch an HTTP resource from an HTTPS page. Always use https:// for the manifest URL. Root cause 2: The WebSocket URL (ws://localhost:4402) is burned into plugin.js at Vite build time via the WS_URI environment variable. Without setting this, the plugin tries to connect to ws://localhost:4402 — which fails as mixed content from an HTTPS origin. Fix: Set WS_URI in the container environment so Vite bakes the correct WSS URL into plugin.js during the startup build: environment: WS_URI: "wss://mcp.example.com/ws"Hickup 4: Wrong environment variable names Problem: The @penpot/mcp README documents several environment variables (PENPOT_MCP_SERVER_ADDRESS, MCP_PORT, PLUGIN_PORT, etc.) that are never actually read by the source code. Setting them has no effect. Variables that actually work: PENPOT_MCP_SERVER_HOST: "0.0.0.0" # bind address for MCP HTTP server PENPOT_MCP_SERVER_PORT: "4401" # MCP HTTP port PENPOT_MCP_WEBSOCKET_PORT: "4402" # WebSocket port PENPOT_MCP_PLUGIN_SERVER_HOST: "0.0.0.0" # bind address for Vite plugin server PENPOT_MCP_REMOTE_MODE: "true" # disables local filesystem tool WS_URI: "wss://mcp.example.com/ws" # WebSocket URL baked into plugin.jsHickup 5: WebSocket needs its own Traefik route Problem: Port 4402 (WebSocket) must be reachable as wss:// from the browser (mixed content rules apply). Simply exposing port 4402 on the host doesn't help — the browser can't use plain ws:// from an HTTPS page. Fix: Add a dedicated Traefik router for /ws that proxies to port 4402. Traefik handles WebSocket upgrades natively — no special configuration needed beyond routing the path. - traefik.http.middlewares.penpot-mcp-ws-strip.stripprefix.prefixes=/ws - traefik.http.routers.penpot-mcp-ws.rule=Host(`mcp.example.com`) && PathPrefix(`/ws`) - traefik.http.routers.penpot-mcp-ws.service=penpot-mcp-ws-svc - traefik.http.routers.penpot-mcp-ws.middlewares=penpot-mcp-ws-strip - traefik.http.routers.penpot-mcp-ws.priority=30 - traefik.http.services.penpot-mcp-ws-svc.loadbalancer.server.port=4402Final Working Docker Compose Service Replace mcp.example.com with your actual domain. penpot-mcp: build: context: . dockerfile_inline: | FROM node:22-slim RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates git python3 make g++ \ && rm -rf /var/lib/apt/lists/* RUN corepack enable && corepack prepare pnpm@latest --activate RUN npm install -g @penpot/mcp@latest WORKDIR /usr/local/lib/node_modules/@penpot/mcp RUN corepack pnpm -r install CMD ["corepack", "pnpm", "run", "bootstrap"] container_name: penpot-mcp environment: PENPOT_MCP_SERVER_HOST: "0.0.0.0" PENPOT_MCP_SERVER_PORT: "4401" PENPOT_MCP_WEBSOCKET_PORT: "4402" PENPOT_MCP_REMOTE_MODE: "true" PENPOT_MCP_PLUGIN_SERVER_HOST: "0.0.0.0" WS_URI: "wss://mcp.example.com/ws" networks: - proxy-traefik-network restart: unless-stopped labels: - traefik.enable=true - traefik.docker.network=proxy-traefik-network # Router 1: MCP HTTP/SSE API (priority 20) - traefik.http.routers.penpot-mcp-api.rule=Host(`mcp.example.com`) && (PathPrefix(`/mcp`) || PathPrefix(`/sse`) || PathPrefix(`/messages`)) - traefik.http.routers.penpot-mcp-api.entrypoints=websecure - traefik.http.routers.penpot-mcp-api.tls=true - traefik.http.routers.penpot-mcp-api.tls.certresolver=myresolver - traefik.http.routers.penpot-mcp-api.service=penpot-mcp-api-svc - traefik.http.routers.penpot-mcp-api.priority=20 - traefik.http.services.penpot-mcp-api-svc.loadbalancer.server.port=4401 # Router 2: WebSocket bridge — /ws → port 4402 (priority 30) - traefik.http.middlewares.penpot-mcp-ws-strip.stripprefix.prefixes=/ws - traefik.http.routers.penpot-mcp-ws.rule=Host(`mcp.example.com`) && PathPrefix(`/ws`) - traefik.http.routers.penpot-mcp-ws.entrypoints=websecure - traefik.http.routers.penpot-mcp-ws.tls=true - traefik.http.routers.penpot-mcp-ws.tls.certresolver=myresolver - traefik.http.routers.penpot-mcp-ws.service=penpot-mcp-ws-svc - traefik.http.routers.penpot-mcp-ws.middlewares=penpot-mcp-ws-strip - traefik.http.routers.penpot-mcp-ws.priority=30 - traefik.http.services.penpot-mcp-ws-svc.loadbalancer.server.port=4402 # Router 3: Plugin UI / manifest.json / plugin.js catch-all (priority 10) # Rewrite Host header so Vite accepts the request - traefik.http.middlewares.penpot-mcp-host.headers.customrequestheaders.Host=localhost:4400 - traefik.http.routers.penpot-mcp-plugin.rule=Host(`mcp.example.com`) - traefik.http.routers.penpot-mcp-plugin.entrypoints=websecure - traefik.http.routers.penpot-mcp-plugin.tls=true - traefik.http.routers.penpot-mcp-plugin.tls.certresolver=myresolver - traefik.http.routers.penpot-mcp-plugin.middlewares=penpot-mcp-host - traefik.http.routers.penpot-mcp-plugin.service=penpot-mcp-plugin-svc - traefik.http.routers.penpot-mcp-plugin.priority=10 - traefik.http.services.penpot-mcp-plugin-svc.loadbalancer.server.port=4400Connecting Claude Code Add the MCP server globally: claude mcp add penpot -t http https://mcp.example.com/mcpOr edit ~/.claude.json directly under .mcpServers: "penpot": { "type": "http", "url": "https://mcp.example.com/mcp" }Installing the Plugin in PenpotOpen any Penpot file Main menu → Plugins → Manage plugins Add: https://mcp.example.com/manifest.json Open the plugin panel → click Connect to MCP serverOnce connected, Claude Code can create and manipulate shapes directly on the canvas.NotesThe bootstrap script runs pnpm -r install && pnpm run build && pnpm run start on every container start. Pre-installing deps in the image (RUN corepack pnpm -r install) cuts startup time significantly — the build step still runs at startup so WS_URI gets baked into plugin.js. Build time for the Docker image is ~5–10 minutes on first run (apt + npm + pnpm deps). An alternative, simpler MCP server using the Penpot REST API (no browser plugin required): zcube/penpot-mcp-server. Limitations vs @penpot/mcp: Uses the Penpot REST API — no direct canvas manipulation (can't create/move shapes in real time like the plugin API can) Self-hosted Penpot requires the enable-access-tokens feature flag set in your Penpot config before an access token can be generated Requires generating a personal access token in Penpot Settings and passing it as PENPOT_ACCESS_TOKEN No live design interaction — operations go through HTTP round-trips to the Penpot backend, not directly into the open design file

- 01 Oct, 2018
- 2 Min. Lesezeit
Docker auf dem Raspberry Pi installieren mit nur einem Befehl - raspbian debian stretch jessie
Am schnellsten und einfachsten installierst du Docker auf einem Raspberry Pi, oder Linux Debian mit dem get-docker.sh Script von Docker. Mit nur einem Befehl, installierst du Docker auf deinem RPi. Bevor man ein fremdes Script ausführt, sollte man sich vergewissern, dass keine schädliche Software installiert wird. Detaillierte Informationen zum Script findest du auf github.com/docker/docker-install. Da das Script von Docker selbst erstellt wurde und fast 300 Sterne auf Github hat, kannst du dem Script vertrauen bzw. selbst nachschauen was drin steht. InhaltDocker via get.docker.com Script installieren Mit folgendem Befehl kannst du die aktuelle Docker Version installieren. curl -fsSL get.docker.com -o get-docker.sh && sh get-docker.sh Was macht der Befehlt: curl ist ein Befehl um Daten von einem Server zu empfangen, oder zu senden. Dieser ruft den Inhalt der Seite get.docker.com ab und speichert ihn in einer Datei namens get-docker.sh ab. Mit den Zeichen && wird ein zweiter Befehlt angehangen. Dieser startet das gespeicherte Bash Script get-docker.sh. curl Parameter -f: keine Fehlerausgabe -s: Silent/quiet mode; zeigt keine Fortschrittsanzeige und Fehlermeldungen -S: in Verbindung mit dem -s Parameter, curl zeigt eine Fehlermeldung wenn es fehlschlägt -L: wenn die Seite umgezogen ist und einen Header Response Code 3xx zurück gibt, startet curl neu mit der neuen Adresse -o: schreibt die Ausgabe von curl in eine Datei Die Beta Version (release candidates) von Docker kannst du mit folgendem Befehlen installieren. curl -fsSL test.docker.com -o test-docker.sh && sh test-docker.shDocker installation testen Ob die Docker installation erfolgreich war, kannst du mit folgendem Befehl testen. Es sollte eine Nachricht erscheinen "Hello from Docker! This message shows that your installation appears to be working correctly." sudo docker run hello-world Im Hintergrund passieren folgende Schritte:der Docker Client kommuniziert mit dem Docker Daemonlädt das Docker Image hello-world vom Docker-Hubder Docker Daemon erstellt ein neuen Container vom Image, welcher den Code für die Ausgabe ausführtDocker Daemon leitet die Ausgabe zum Docker Client weiter und dieser sendet die Ausgabe wiederum an deinen Terminal/CLIhilfreiche Docker Befehle docker images Listet alle Docker Images auf dem System auf docker ps Listet alle Docker container auf dem System auf docker run -it IMAGE-ID bash Bash/CLI in einem laufenden Docker Container nutzen (IMAGE-ID bekommst du mit docker images herraus) docker rm $(docker ps -a -q) && docker rmi $(docker images -q) All Docker Container und Docker Images löschen
- 06 Jul, 2016
- 1 min read
Install Discourse with Docker in a Subfolder with SSL and serve other content with nginx under the same domain
Discourse is a great free and open-source forum software. There is an Linux installation guide with docker, but Discourse runs with a subdomain like discourse.example.com. I want Discourse run in a subfolder like example.com/forum. There is a guide how to do this, but how to configure the nginx server to serve other content (example.com/index.html or example.com/otherContent) on the same machine?install discourse with docker on a linux server https://github.com/discourse/discourse/blob/master/docs/INSTALL-cloud.mdchange the port of the docker container and setup SSL with Let's Encrypt (don't use the subdomain like discourse.example.com use example.com) https://www.digitalocean.com/community/tutorials/how-to-install-discourse-behind-nginx-on-ubuntu-14-04create a folder for all the other content for your sitesudo mkdir -p /var/www/example.com/htmlif necessary change the owner to you or www-data sudo chown -R www-data:www-data /var/www/example.com/htmlThis is the root folder for your domain example.com Dont create a forum folder here, it will cause a conflict with the discourse docker redirect!change the /etc/nginx/sites-enabled/discourse to this and replace http://discourse.example.com with your url (use main url not subdomain)server { listen 80; server_name example.com; return 301 https://example.com$request_uri; } server { listen 443 ssl spdy; server_name example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AE$ ssl_prefer_server_ciphers on; location / { root /var/www/example.com/html; } location /forum/ { proxy_pass http://example.com:25654/; proxy_read_timeout 90; proxy_redirect http://example.com:25654/ https://example.com/; } }restart nginxsudo service nginx restartIf you go to example.com/forum your Discourse site should appear and if you go to example.com you should see your index.html (if you created one).