# portracker maps every host port, and the price is the host PID namespace

> portracker is a single container that reads host processes to build a live port map for Docker and TrueNAS machines. It needs the host PID namespace, two Linux capabilities, the Docker socket, and it ships with authentication switched off.

**mostafa-wahied/portracker** — An open source, self-hosted, real-time port monitoring and discovery tool.

- Repository: https://github.com/mostafa-wahied/portracker
- Stars: 2,466 · Forks: 105
- Language: JavaScript
- License: MIT
- Published: 2026-09-10 · Updated: 2026-09-10 · Language: en
- Canonical page: https://hysenlabs.com/projects/mostafa-wahied-portracker

## Discovery starts by joining the host PID namespace

portracker answers one question: which process on this machine owns that port. Answering it means looking outside its own container, so the shipped compose file sets `pid: "host"` and comments that as the requirement that lets portracker see all host processes for accurate port mapping. Two Linux capabilities come with it. `SYS_PTRACE` is described as what reads other processes' `/proc` entries on Linux hosts, and `SYS_ADMIN` as what enables namespace entry on Docker Desktop on macOS and Windows. The same file sets `apparmor:unconfined`, needed for system ports because AppArmor restrictions can block `/proc` access.

The stored state is small by design. The application runs as a single process, the database is a SQLite file at `/data/portracker.db` mounted from `./portracker-data`, and no external database such as PostgreSQL or Redis is involved. Peering is per machine rather than per network: you add other portracker instances as peers, and a virtual machine's instance can be nested under its physical host. A large estate means one container per host, not one central collector that has to be told everything.

## The quick start compose block never mounts the Docker socket its own file calls required

The compose snippet printed in the docs is not the compose file in the repository. The documented version defines one service, sets `container_name: portracker`, `restart: unless-stopped`, the host PID namespace, both capabilities and `apparmor:unconfined`, then lists a single volume, `./portracker-data:/data`, followed by a line reading `# Req` where the block ends.

The `docker-compose.yml` at the repository root carries more. It mounts `/var/run/docker.sock:/var/run/docker.sock:ro` under the comment that Docker socket access is required for container discovery, and instructs that the mount be commented out when the proxy setup is used instead. It also keeps two host mounts switched off, `/proc:/host/proc:ro` and `/sys/fs/cgroup:/host/sys/fs/cgroup:ro`, marked advanced and to be uncommented only if port detection misbehaves. Its environment block carries `DATABASE_PATH=/data/portracker.db`, `PORT=4999` and a commented `HOST_PROC=/host/proc` that matches the volume path.

Copy the short snippet as printed and container discovery has no socket to talk to, while the data directory and the permissions are already in place.

## The proxy path is the one that ends on a different image name

The documented alternative puts `tecnativa/docker-socket-proxy:latest` in front of the Docker API. The proxy container runs with read-only switches turned on (`CONTAINERS=1`, `IMAGES=1`, `INFO=1`, `NETWORKS=1`, `POST=0`), publishes its port 2375 on the host and takes the socket itself. portracker then reaches it over TCP, with the repository compose file showing the service-name form `DOCKER_HOST=tcp://docker-proxy:2375` where the documented command uses the loopback form.

That portracker command also ends on a shorter image reference than every other command in the docs:

```sh
# Start portracker
docker run -d \
  --name portracker \
  --restart unless-stopped \
  --pid host \
  --cap-add SYS_PTRACE \
  --cap-add SYS_ADMIN \
  --security-opt apparmor=unconfined \
  -p 4999:4999 \
  -v ./portracker-data:/data \
  -e DOCKER_HOST=tcp://localhost:2375 \
  mostafawahied/port
```

Everywhere else the reference is `mostafawahied/portracker:latest`. The mismatch sits in the hardened path, which is the one an operator is most prone to copy verbatim after reading the security note above it. Publishing 2375 on the host is also worth noticing: the proxy is bound on all addresses, and it is the component that stands between portracker and a writable Docker API.

## Authentication stays off until a variable turns it on

`ENABLE_AUTH` defaults to `false` and is documented as available from v1.2.0 onward. `SESSION_SECRET` defaults to a random value, is needed only once auth is enabled, and exists so that restarting the container does not log everyone out. `CORS_ORIGIN` is disabled as well: it takes comma-separated browser origins for a separately hosted frontend or a reverse proxy that rewrites `Host`, and same-origin access needs no setting.

Both `docker run` commands publish `-p 4999:4999` without restricting the interface, so a default install listens on every address the host holds. Put together with the default auth state, that is a live map of host processes, container names, published ports and internal container ports, protected by nothing except the host firewall. The TrueNAS path adds a second credential to the same environment block: `TRUENAS_API_KEY`, blank by default, issued from the TrueNAS interface under System Settings, API Keys. Enabling auth before the first start costs one variable; enabling it after the port is published does not undo the exposure in between.

## Two defaults decide how much of the network the map shows

`INCLUDE_UDP` is `false`, so a service listening on UDP produces no row at all until it is set to `true`. That single default hides an entire protocol from a tool whose purpose is to tell you what is listening on the host. `CACHE_TIMEOUT_MS` is `60000`, so scan results sit behind a one minute cache, and `DISABLE_CACHE` is `false` for anyone who needs that window closed. A port freed seconds ago can still be listed, and one bound seconds ago can still be missing.

The Docker-facing caches are tuned separately and only in the example environment file: `DOCKER_CACHE_CONTAINERS_TTL_MS=4000`, `DOCKER_CACHE_INSPECT_TTL_MS=5000`, `DOCKER_CACHE_STATS_TTL_MS=1500` and `DOCKER_CACHE_PORTS_TTL_MS=4000`, next to commented `DOCKER_SOCK`, `DOCKER_TLS_VERIFY=0` and `DOCKER_CERT_PATH` entries. The variable table lists `CACHE_TIMEOUT_MS`, `DISABLE_CACHE` and `INCLUDE_UDP` and then sends the reader to `.env.example` for the full set, so the per-call TTLs and the TLS switches exist in that file alone. The same applies to `DEBUG`, which turns on verbose application logging and is off by default.

## A container past one hundred internal ports loses rows without any error

The part of portracker worth keeping is the split between a container's published host ports and its internal ones, since the two answer different questions and only the first appears in a host port scan. The internal side is bounded by `MAX_INTERNAL_PORTS_PER_CONTAINER`, which defaults to `100`. Its description is direct: containers above the limit keep published ports but omit internal rows.

Nothing in that sentence is an error condition. A container with a hundred and thirty bound internal sockets is listed like any other, and the rows that would have described sockets 101 through 130 are simply absent. The dashboard therefore cannot be read as proof that a container exposes nothing further, and the same is true of the host side, where a scan reflects the moment the cache was filled rather than the moment you looked. When a service is unreachable and its port is not on screen, raising this value and clearing the cache comes first, and comparing the container's own port list against the interface is what tells you which of the two views is wrong.

## TrueNAS virtual machines arrive read-only and the key guide stops mid sentence

TrueNAS is the one collector that needs a credential. With `TRUENAS_API_KEY` set, portracker discovers running virtual machines and reports richer system information such as OS version and uptime. The footnote under the feature list draws the line: VMs found this way are shown in read-only mode, and full monitoring means running a portracker instance on each VM and adding it as a separate server. The hierarchy feature exists for exactly that arrangement.

The key instructions do not finish. The first three steps are complete, from logging into the TrueNAS web interface through System Settings to API Keys and clicking Add. The fourth begins `Give it a desc` and ends there.

The example environment file carries the rest of the TrueNAS surface that the variable table never mentions: `TRUENAS_WS_BASE`, auto-detected when unset, with the examples `ws://192.168.1.100:80` and `wss://truenas.local:443`; `TRUENAS_TIMEOUT_MS=90000` for the whole collection, with a note that systems running many apps or VMs may need 120000 to 180000; and per-call budgets of `TRUENAS_SYSTEM_INFO_TIMEOUT_MS=30000` and `TRUENAS_APP_QUERY_TIMEOUT_MS=20000`, beside a vm.query line that also stops partway. Anyone tuning TrueNAS collection is working from that file, not from the table.

## The runtime image carries ping, netcat, wget and the Docker client

The image is built in three stages. The frontend stage installs dependencies with dev included and runs the build. The backend stage repeats the install on a base carrying `python3`, `make` and `g++`, because `better-sqlite3` is a native module, and it then opens an in-memory database and creates a table to prove the binding loads before the artifact moves forward. The runtime stage is `node:22-slim` with `ca-certificates`, `iproute2`, `iputils-ping`, `docker.io`, `netcat-openbsd`, `wget`, `procps` and `util-linux` added, `package.json` and `CHANGELOG.md` copied in for a What's New panel, the compiled backend copied over, and the frontend build placed under `backend/public`.

So the published container holds ping, a netcat client, wget and the Docker CLI next to the scanner itself. That is what lets it collect and probe, and it is part of why the host PID namespace and the two capabilities are not optional decoration.

The `package.json` shows how much of this is held in place by checks. `test` runs jest, and `test:release`, `test:image` and `test:context` run contract scripts for releases, the image and the build context. `lint:strict` passes a config file named `.eslint.local.cjs`, which is not in the repository listing next to `eslint.config.cjs` and `eslint.strict.config.cjs`, and `playwright` is pinned at the exact version 1.63.0 while every other devDependency uses a caret range and no script in `scripts` invokes it. The declared version is 1.3.13, matching the newest release tag.

## Conclusion

portracker earns its place on a machine whose owner is the person running it: one process, one SQLite file, no external database, and a map that replaces the spreadsheet people keep next to the rack. The conditions are just as concrete. It reads other processes through /proc, asks for SYS_PTRACE and SYS_ADMIN and an unconfined AppArmor profile, publishes 4999 with authentication off, and keeps the Docker socket in reach unless you insert the proxy. Set ENABLE_AUTH before the first start, confirm that UDP ports show up when you expect them, and raise MAX_INTERNAL_PORTS_PER_CONTAINER before concluding that a container has nothing else bound. The quick start compose block and the proxy command ending in mostafawahied/port both need hand correction before anyone pastes them onto a real host.

## FAQ

### What does portracker need on the host before it can list ports?

The container joins the host PID namespace, adds the SYS_PTRACE and SYS_ADMIN capabilities, turns off AppArmor confinement, and mounts the Docker socket read-only for container discovery. Its own state is a single SQLite file at /data/portracker.db, so no external database server is involved.

### Does portracker need PostgreSQL or Redis to store its port map?

No. It runs as a single process with an embedded SQLite database, which defaults to /data/portracker.db inside the container, and the documentation states that external database dependencies such as PostgreSQL or Redis are not required.

### Is the portracker dashboard behind a login by default?

No. ENABLE_AUTH defaults to false and is documented as available from v1.2.0 onward. Setting it to true enables authentication, and SESSION_SECRET, which defaults to a random value, is only needed then, keeping sessions alive across a container restart.

### Why does portracker not show a UDP port that is in use?

INCLUDE_UDP defaults to false, so scans list TCP only until it is turned on. Scan results are also cached for CACHE_TIMEOUT_MS, 60000 by default, so a port bound or freed moments earlier can be absent or still listed until the cache window passes.

### dotpeek vs portracker

The repository does not compare itself with dotpeek or with any other tool, and names no alternative. What it does set out is a self-hosted port map that reads host processes through the host PID namespace, stores state in one SQLite file, keeps the Docker socket for container discovery, and adds TrueNAS integration behind an optional API key.

## Sources

- [Issues](https://github.com/mostafa-wahied/portracker/issues)
- [License: MIT](https://github.com/mostafa-wahied/portracker/blob/main/LICENSE)
- [mostafa-wahied/portracker on GitHub](https://github.com/mostafa-wahied/portracker)
- [README](https://github.com/mostafa-wahied/portracker/blob/main/README.md)
- [Releases](https://github.com/mostafa-wahied/portracker/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/mostafa-wahied-portracker
