Skip to content

Docker

The Linux ppee-cli needs glibc 2.38 or later, so the image is based on debian:trixie-slim (Debian 13):

  • ~82 MB image: the slim base plus ppee-cli, nothing else installed
  • respects container CPU limits (--cpus, cgroup quotas, cpusets)
  • runs as a pure function: file in, report out
  • also works as an MCP server

Build the image

Put the files from the Linux release archive (mzrst.com) in an empty folder, next to this Dockerfile:

Dockerfile
FROM debian:trixie-slim
COPY ppee-cli Suspicious.txt THIRD-PARTY-NOTICES.txt /ppee/
ENTRYPOINT ["/ppee/ppee-cli", "--no-update-check"]
chmod +x ppee-cli
docker build -t ppee-cli .
docker run --rm ppee-cli --version       # ppee-cli 2.0.0
  • Suspicious.txt is the keyword list for suspicious strings; keep it next to the binary.
  • THIRD-PARTY-NOTICES.txt holds the licence notices for the bundled libraries; keep it in redistributed images.
  • Anything after the image name is passed to ppee-cli.

Use a base with glibc 2.38+

debian:bookworm-slim (glibc 2.36) fails with GLIBC_2.38' not found. debian:trixie-slim and ubuntu:24.04 work. Alpine (musl) does not.

Running as non-root

docker run --user 65534 works for analysis. The similarity database then can't be written in /ppee, so add --no-similarity.

x86-64 hosts

The release binary is x86-64, so the image runs on x86-64 Docker hosts. It analyzes x86, x64 and ARM64 PE files alike.

Analyze a file

Mount the folder that holds your samples, read-only, and pass the path as the container sees it:

docker run --rm -v "$PWD:/data:ro" ppee-cli --json --hashes /data/sample.exe
docker run --rm -v "${PWD}:/data:ro" ppee-cli --json --hashes /data/sample.exe
docker run --rm -v "%cd%:/data:ro" ppee-cli --json --hashes /data/sample.exe

Every CLI option works the same way, including --analysis for Go, Rust, .NET and NativeAOT files, and exit codes pass through docker run.

Shell alias

alias ppee-cli='docker run --rm -v "$PWD:/data:ro" -w /data ppee-cli'
ppee-cli --imports /data/sample.exe

Read-only and hardened runs

docker run --rm --read-only --network none --cap-drop ALL --security-opt no-new-privileges \
  -v "$PWD:/data:ro" ppee-cli --json --all --no-similarity /data/sample.exe
  • --network none: PPEE needs no network (the image already passes --no-update-check).
  • --read-only: fine, as long as you add --no-similarity.

Similarity on a read-only root

Without --no-similarity, a read-only container can't create the database. --similarity and --all then print warning: similarity database '/ppee/ppee-cli.similarity.db' could not be opened … on stderr and report "available": false. The exit code is still 0. Pass --no-similarity to silence it, or persist the DB as shown below.

CPU limits

ppee-cli sizes its worker pool from the CPUs the container may actually use, from the cgroup CPU quota and affinity mask, not from the host's core count. docker run --cpus=2 therefore gets two workers. You can override it:

docker run --rm --cpus=2 -v "$PWD:/data:ro" ppee-cli --hashes /data/big.dll
docker run --rm -e PPEE_JOBS=1 -v "$PWD:/data:ro" ppee-cli --hashes /data/big.dll

Use PPEE_JOBS=1 when you already run many containers in parallel.

Persisting the similarity database

The database lives next to the binary at /ppee/ppee-cli.similarity.db. Bind-mount a file there to keep it between runs:

touch ppee-cli.similarity.db              # must exist first, or Docker creates a directory
docker run --rm -v "$PWD:/data:ro" \
  -v "$PWD/ppee-cli.similarity.db:/ppee/ppee-cli.similarity.db" \
  ppee-cli --similarity /data/a.exe
$ docker run … ppee-cli --similarity /data/b.exe
Similarity DB: 2 record(s)
Match against 1 peer(s):
  SHA256 identical - /data/a.exe

Note

Don't mount a volume over the whole /ppee directory. That hides the ppee-cli binary itself.

Batch scan a folder

The image's entry point is ppee-cli, so loop on the host:

scan.sh: one JSON Lines record per file
find samples -type f \( -iname '*.exe' -o -iname '*.dll' \) -print0 |
  xargs -0 -P 4 -I{} docker run --rm -e PPEE_JOBS=1 -v "$PWD:/data:ro" ppee-cli \
    --json --hashes --imports --security --no-similarity "/data/{}" > report.jsonl
jq -r '[.path, .fileInfo.sha256, (.security.present|tostring)] | @tsv' report.jsonl

Docker Compose

compose.yaml
services:
  ppee:
    build: .
    image: ppee-cli
    read_only: true
    network_mode: none
    cap_drop: [ALL]
    volumes:
      - ./samples:/data:ro
    command: ["--json", "--all", "--no-similarity", "/data/sample.exe"]
docker compose run --rm ppee --json --hashes /data/other.dll

Kubernetes Job

ppee-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: ppee-scan
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: ppee
          image: registry.example.com/ppee-cli:2.0.0
          args: ["--json", "--hashes", "--security", "--no-similarity", "/data/app.exe"]
          resources:
            limits: { cpu: "2", memory: "512Mi" }
          securityContext:
            readOnlyRootFilesystem: true
            allowPrivilegeEscalation: false
            runAsNonRoot: true
            runAsUser: 65534
          volumeMounts:
            - { name: samples, mountPath: /data, readOnly: true }
      volumes:
        - name: samples
          persistentVolumeClaim: { claimName: samples }

Publishing the image

docker tag ppee-cli ghcr.io/<org>/ppee-cli:2.0.0
docker push ghcr.io/<org>/ppee-cli:2.0.0

See CI/CD → GitHub Actions to automate this.

Next: CI/CD integration · MCP in Docker

References