> ## Documentation Index
> Fetch the complete documentation index at: https://docs.safedep.io/llms.txt
> Use this file to discover all available pages before exploring further.

# PMG in Docker and Shared VMs

> Install PMG system-wide on Linux so Docker images, CI runners, and shared VMs block malicious packages for every user account.

Use a system-wide install when one machine or image should protect every user account: shared VMs, CI runners, and golden Docker images. It installs PATH shims for all users and an authoritative config under `/etc`, so any user's `npm install` or `pip install` is checked before it runs.

<Info>
  For a single developer machine, use the per-user setup in the [Quickstart](/package-security/pmg/quickstart) instead. Per-user and system-wide installs can coexist.
</Info>

## Requirements

* **Linux and root.** System install is Linux-only and must run as root.
* **PMG in a system path.** Every user's shim runs PMG by absolute path, so the binary must live in a root-owned system path such as `/usr/local/bin`, executable and reachable by all users.

## What gets created

| Item                  | Path                          |
| --------------------- | ----------------------------- |
| Configuration         | `/etc/safedep/pmg/config.yml` |
| Package-manager shims | `/usr/local/lib/pmg/bin`      |
| Shell PATH snippet    | `/etc/profile.d/pmg.sh`       |

<Note>
  System install uses PATH shims only. It does not create the `~/.pmg.rc` alias layer that per-user setup installs.
</Note>

## Docker images

PMG must be installed inside the image: PMG on the Docker host cannot see installs that run during `docker build`.

Compared to a Linux VM, a Dockerfile needs two adjustments:

1. **No `sudo`.** `RUN` steps execute as root. If your base image switches to a non-root `USER`, add `USER root` before the install step and switch back after.
2. **Set `PATH` with `ENV`.** `RUN` steps do not source `/etc/profile.d`, so add the shim directory to `PATH` explicitly.

```dockerfile theme={null}
FROM node:22-bookworm

RUN curl -fsSL https://raw.githubusercontent.com/safedep/pmg/main/install.sh | sh \
 && pmg setup install --system

# Required: profile.d is not sourced during docker build
ENV PATH="/usr/local/lib/pmg/bin:$PATH"

# Optional: switch to a non-root user; the PATH above still applies
RUN mkdir -p /app && chown node:node /app
WORKDIR /app
USER node

RUN npm i safedep-test-pkg@0.1.3
```

Every `RUN npm install` or `RUN pip install` after the `ENV PATH` line now goes through PMG, and the running container inherits the protection, for any `USER`. Derived images inherit it too.

<Warning>
  If a later stage sets `ENV PATH` again, keep `/usr/local/lib/pmg/bin` ahead of the real `npm`/`pip` directories. Dropping it, or ordering it behind the toolchain, turns interception off without any error.
</Warning>

## Linux VMs

1. **Place PMG in a system path.**

   Not installed yet? The install script places it in `/usr/local/bin`, prompting for sudo for the copy:

   ```bash theme={null}
   curl -fsSL https://raw.githubusercontent.com/safedep/pmg/main/install.sh | sh
   ```

   Already installed in a user location? Copy it over:

   ```bash theme={null}
   sudo install -m 755 "$(which pmg)" /usr/local/bin/pmg
   ```

   *PMG rejects a binary in a user location (`~/.local/bin`, `~/go/bin`), since any single user could modify it. The install script also picks `~/.local/bin` when that is on your `PATH`; if it did, use the copy command above.*

2. **Install system-wide:**

   ```bash theme={null}
   sudo pmg setup install --system
   ```

3. **Activate the PATH.** The install writes `/etc/profile.d/pmg.sh`, which prepends the shim directory for login shells. New login sessions pick it up; for an already-open shell:

   ```bash theme={null}
   source /etc/profile.d/pmg.sh
   ```

<Tip>
  On a multi-user host where you do not trust every member of the directory's group, tighten the install directory: `sudo chmod g-w /usr/local/bin`. Debian and Ubuntu ship it group-writable for the `staff` group (empty by default).
</Tip>

## Verify

Works for a VM shell and inside a container (`docker run --rm -it <image> sh`):

```bash theme={null}
pmg setup doctor  # validates config, shims, PATH, and binary integrity
```

Then prove the protection end to end with a known test package. PMG must block it:

```bash theme={null}
npm install --no-cache --prefer-online safedep-test-pkg@0.1.3
```

## Configuration

The system config file is authoritative for every user. PMG ignores a per-user `config.yml` while `/etc/safedep/pmg/config.yml` exists.

`pmg config set` and `pmg config edit` are disabled under a system config. Edit the file as root, or redeploy it through your image or configuration management.

<Note>
  Runtime data (event logs, cloud-sync state, cache) stays per user under `~/.config/safedep/pmg` and `~/.cache/safedep/pmg`. The invoking user must be able to write their own config directory. Relocate it with `PMG_CONFIG_DIR` / `PMG_CACHE_DIR` if needed.
</Note>

## Cloud sync

Cloud sync reports install events to your SafeDep Cloud tenant, where they appear in [Package Guard](/governance/cloud/endpoint-hub/package-guard). To set it up, enable cloud sync and provide your tenant ID and API key from [SafeDep Cloud](/governance/cloud/quickstart) through the `SAFEDEP_API_KEY` and `SAFEDEP_TENANT_ID` environment variables.

### In a Docker image

Enable sync and set a stable endpoint name before your install steps:

```dockerfile theme={null}
ENV PMG_CLOUD_ENABLED=true
ENV PMG_CLOUD_ENDPOINT_ID="my-golden-image"
```

Containers report a generated hostname per run, so without `PMG_CLOUD_ENDPOINT_ID` every run registers as a new endpoint. A stable name is what you will recognize in Endpoint Hub.

The credentials exist only where you run `docker`, as shell exports or CI secrets; they never go in the Dockerfile.

<Warning>
  Never bake credentials into an image with `ENV SAFEDEP_API_KEY=...`. `ENV` values persist in the image metadata, and anyone who pulls the image can read them.
</Warning>

**In the running container**, pass them through at start:

```bash theme={null}
docker run -e SAFEDEP_API_KEY -e SAFEDEP_TENANT_ID my-image
```

Compose `environment:` entries and Kubernetes Secret references work the same way. If the container is short-lived, a CI job for example, run `pmg cloud sync` before it exits so buffered events are not destroyed with it.

**During `docker build`**, sync in the same `RUN` step as the install, with the `export` covering both commands: PMG records events for the cloud only when credentials resolve. Mount the credentials as BuildKit secrets, which expose them to that step only (`uid=` makes them readable by the step's non-root user):

```dockerfile theme={null}
RUN --mount=type=secret,id=safedep_api_key,uid=1000 \
    --mount=type=secret,id=safedep_tenant_id,uid=1000 \
    export SAFEDEP_API_KEY="$(cat /run/secrets/safedep_api_key)" \
           SAFEDEP_TENANT_ID="$(cat /run/secrets/safedep_tenant_id)" \
    && npm ci && pmg cloud sync
```

Each `--secret` reads the variable exported where you run the build:

```bash theme={null}
docker build \
  --secret id=safedep_api_key,env=SAFEDEP_API_KEY \
  --secret id=safedep_tenant_id,env=SAFEDEP_TENANT_ID .
```

<Note>
  A blocked package fails its `RUN` step: later commands never run, including any sync at the end of the Dockerfile, and Docker discards the failed step's changes. Syncing per step uploads every event up to the block; the block itself shows in the build output.
</Note>

<Accordion title="Complete Dockerfile with cloud sync">
  ```dockerfile theme={null}
  FROM node:22-bookworm

  RUN curl -fsSL https://raw.githubusercontent.com/safedep/pmg/main/install.sh | sh \
   && pmg setup install --system

  # Before the install steps: shim PATH, sync enabled, stable endpoint name
  ENV PATH="/usr/local/lib/pmg/bin:$PATH"
  ENV PMG_CLOUD_ENABLED=true
  ENV PMG_CLOUD_ENDPOINT_ID="my-golden-image"

  RUN mkdir -p /app && chown node:node /app
  WORKDIR /app
  USER node
  COPY --chown=node:node package*.json ./

  # uid matches the USER above so it can read the secrets
  RUN --mount=type=secret,id=safedep_api_key,uid=1000 \
      --mount=type=secret,id=safedep_tenant_id,uid=1000 \
      export SAFEDEP_API_KEY="$(cat /run/secrets/safedep_api_key)" \
             SAFEDEP_TENANT_ID="$(cat /run/secrets/safedep_tenant_id)" \
   && npm ci && pmg cloud sync
  ```

  Build it with the credentials exported in your shell:

  ```bash theme={null}
  docker build \
    --secret id=safedep_api_key,env=SAFEDEP_API_KEY \
    --secret id=safedep_tenant_id,env=SAFEDEP_TENANT_ID \
    -t my-image .
  ```
</Accordion>

### On a shared VM

Enable sync in `/etc/safedep/pmg/config.yml` as root:

```yaml theme={null}
cloud:
  enabled: true
```

Then put the credential values in a root-owned snippet such as `/etc/profile.d/safedep-cloud.sh`, so every login shell exports them:

```bash theme={null}
# /etc/profile.d/safedep-cloud.sh
export SAFEDEP_API_KEY="<your-api-key>"
export SAFEDEP_TENANT_ID="<your-tenant-id>"
```

Users who should sync with their own key can set the same variables in their own shell profile instead. One shared key does not blur the picture: every synced event records which OS user ran the install. It does mean every user on the host can read the key.

Auto-sync cadence, manual sync, and viewing events are covered in [Package Guard](/governance/cloud/endpoint-hub/package-guard); the full set of cloud keys is in the [PMG configuration reference](https://github.com/safedep/pmg/blob/main/docs/config.md).

## Certificates

System install does not set up a MITM certificate authority. For npm and pip on Linux, PMG's default ephemeral CA and environment-variable injection are sufficient. To install a persistent CA into the OS trust store, run `pmg setup cert install --system` as your normal user.

## Uninstall

```bash theme={null}
sudo pmg setup remove --system                 # remove shims and profile snippet
sudo pmg setup remove --system --config-file   # also remove /etc/safedep/pmg/config.yml
```

This leaves the PMG binary in place. To remove it too, run `sudo rm /usr/local/bin/pmg`, or uninstall it through your package manager if you installed it that way.

## Troubleshooting

Try running `pmg setup doctor` first: it detects each of these states and prints the matching fix.

<AccordionGroup>
  <Accordion title="npm or pip resolves outside the shim directory">
    Version managers like nvm, pyenv, volta, and asdf prepend their own bin directories from rc files that run after `/etc/profile.d`, putting the real `npm`/`pip` ahead of the shims.

    * Keep `/usr/local/lib/pmg/bin` first in a durable `ENV PATH` or login PATH
    * Or call `pmg npm` / `pmg pip` directly
  </Accordion>

  <Accordion title="pip inside a virtualenv bypasses PMG">
    After `source .venv/bin/activate`, bare `pip` uses the venv binary and skips the shims. Call `pmg pip` inside the venv.
  </Accordion>

  <Accordion title="Every pmg command fails with permission denied on the event log">
    Another account created or owns your config directory. Check the path in the error message.

    If the path is **inside your own home**, a root run created it as root. This happens with `su` without `-` (prefer `sudo` or `su -`) and with images that set `ENV HOME` before dropping root. Restore ownership:

    ```bash theme={null}
    sudo chown -R $(id -un) ~/.config/safedep
    ```

    If the path is **inside another user's home**, your environment leaked that user's `HOME` or `XDG_CONFIG_HOME` (common with `sudo -u` on CI runners). Fix the environment; do not `chown` another user's directory, that breaks PMG for them.

    ```bash theme={null}
    export XDG_CONFIG_HOME="$HOME/.config"
    ```
  </Accordion>
</AccordionGroup>

<CardGroup cols={2}>
  <Card title="System Install Reference" icon="github" href="https://github.com/safedep/pmg/blob/main/docs/system-install.md">
    Full reference in the PMG repo: binary validation rules, user data directories, and limitations.
  </Card>

  <Card title="PMG" icon="box" href="/package-security/pmg/overview">
    How PMG blocks malicious packages at install time.
  </Card>
</CardGroup>
