> Markdown version of [Deployment Examples](https://vaadin.com/docs/next/building-apps/agentic-development/mcp/self-hosting/deployment-examples). Section index: [llms.txt](https://vaadin.com/docs/next/building-apps/llms.txt)

# Deployment Examples

Complete definitions for a shared internal service, with the license key mounted, audit logging enabled, and health checks wired up. Both run the same image with the same settings.

Replace the image tag in each example with the build you intend to run. See [Finding a Tag](https://vaadin.com/docs/next/building-apps/agentic-development/mcp/self-hosting.md#find-a-tag).

## <a id="file-ownership"></a>File Ownership Comes First

The container runs as user ID 65532. Two mounts have to match, and getting either wrong stops the server rather than degrading it:

- **The license key** has to be **readable** by user 65532. Otherwise the server reports `no license file found` and exits.

- **The audit log directory** has to be **writable** by user 65532. Otherwise the server can’t open the file and exits.

Both defaults work against you: a Docker named volume and a Kubernetes Secret are each owned by `root` when created, so each example below sets ownership explicitly.

Docker Desktop on macOS remaps ownership for bind mounts, so you won’t hit this there. On a Linux host, you do.

## <a id="docker-compose"></a>Docker Compose

Prepare the key and the log directory next to your `compose.yaml`:

```bash
mkdir -p audit
sudo chown 65532:65532 serverKey audit
sudo chmod 400 serverKey
```

```yaml
services:
  vaadin-mcp:
    image: vaadin/mcp-server:1.0.0-20260916-114956-2b7c708
    ports:
      - "8080:8080"
    environment:
      VAADIN_AUDIT_LOG_PATH: /var/log/vaadin-mcp/audit.jsonl
    volumes:
      - ./serverKey:/home/nonroot/.vaadin/serverKey:ro
      - ./audit:/var/log/vaadin-mcp
    restart: unless-stopped
```

Mount the key as a single file, as above, rather than mounting a whole directory. Any other key in that directory may be read ahead of yours.

Don’t add a `healthcheck:` that runs a command inside the container — see [Health Checks](#health-checks) below.

## <a id="kubernetes"></a>Kubernetes

Create the key as a Secret:

```bash
kubectl create secret generic vaadin-mcp-license \
    --from-file=serverKey=/path/to/serverKey
```

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vaadin-mcp-server
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vaadin-mcp-server
  template:
    metadata:
      labels:
        app: vaadin-mcp-server
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 65532
        runAsGroup: 65532
        fsGroup: 65532            # (1)
      containers:
        - name: server
          image: vaadin/mcp-server:1.0.0-20260916-114956-2b7c708
          ports:
            - containerPort: 8080
          env:
            - name: VAADIN_AUDIT_LOG_PATH
              value: /var/log/vaadin-mcp/audit.jsonl
          volumeMounts:
            - name: license
              mountPath: /home/nonroot/.vaadin
              readOnly: true
            - name: audit
              mountPath: /var/log/vaadin-mcp
          readinessProbe:               # (3)
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 20
            periodSeconds: 30
          resources:
            requests:
              cpu: "1"
              memory: 2Gi
            limits:
              memory: 2Gi
      volumes:
        - name: license
          secret:
            secretName: vaadin-mcp-license
            defaultMode: 0440         # (2)
            items:
              - key: serverKey        # (2)
                path: serverKey
        - name: audit
          persistentVolumeClaim:
            claimName: vaadin-mcp-audit
```

1. `fsGroup` is what makes both mounts readable. It sets group ownership on the mounted volumes and adds the group to the container’s process. `runAsUser` on its own does not change file ownership, so a Secret stays owned by `root` and the server can’t read it.

2. `defaultMode: 0440` grants read access to that group. The `items` list restricts the mounted directory to your key alone, which is what keeps another key from being read first.

3. Kubernetes sends these probes as HTTP requests from outside the container, rather than running a command inside it, which is why they work against this image.

Expose it with an ordinary Service, then apply your own authentication at the ingress. The server has none of its own — see [Network Exposure](https://vaadin.com/docs/next/building-apps/agentic-development/mcp/self-hosting.md#network-exposure).

## <a id="podman"></a>Podman

`podman run` accepts the same arguments as `docker run`, so every command in this documentation works with the name substituted.

One difference matters. Rootless Podman maps container user IDs to a range of host user IDs, so a file you `chown 65532` on the host is **not** user 65532 inside the container. Either run the key through Podman’s own mapping:

```bash
podman run --rm -p 8080:8080 \
    -v "$PWD/serverKey:/home/nonroot/.vaadin/serverKey:ro,U" \
    vaadin/mcp-server:1.0.0-20260916-114956-2b7c708
```

The `U` suffix tells Podman to change the mounted file’s ownership to match the user the container runs as. Or avoid file ownership entirely and pass the key as an environment variable, as in [Licensing](https://vaadin.com/docs/next/building-apps/agentic-development/mcp/self-hosting/licensing.md).

## <a id="health-checks"></a>Health Checks

The image contains no shell and no HTTP client, so **a health check that runs a command inside the container cannot work**. This rules out a Docker or Compose `healthcheck:` using `CMD` or `CMD-SHELL`, and any `exec` probe.

Check the server from outside the container instead:

- **Kubernetes** `httpGet` probes, as above. Kubernetes sends the request from outside, so nothing has to exist inside the image.

- **Compose and plain Docker**: poll `http://<host>:8080/health` from your monitoring system or from the host.

See [Monitoring and Audit Logging](https://vaadin.com/docs/next/building-apps/agentic-development/mcp/self-hosting/monitoring-and-audit-logging.md) for what the endpoint returns and what to alert on.
