# Scanopy Documentation
Source: https://scanopy.net/docs/
import { Card, Cards } from 'fumadocs-ui/components/card';
Welcome to the Scanopy documentation. Choose your path:
## Learn More
* **[Using Scanopy](https://scanopy.net/docs/using-scanopy)** — Discovery, topology, and asset management
* **[Setting Up Daemons](https://scanopy.net/docs/setting-up-daemons)** — Install daemons and plan your deployment
* **[Self-Hosted Server](https://scanopy.net/docs/self-hosted-server)** — Install and upgrade your server
* **[Guides](https://scanopy.net/docs/guides)** — How-to guides for specific use cases
* **[Troubleshooting](https://scanopy.net/docs/troubleshooting)** — Diagnose and fix common issues
* **[Reference](https://scanopy.net/docs/reference)** — Configuration, architecture, and more
* **[API Reference](https://scanopy.net/docs/api)** — REST API documentation
# Introduction
Source: https://scanopy.net/docs/introduction/
Scanopy is a network discovery and visualization tool that automatically discovers hosts, services, and network topology across your infrastructure.
## Key Features
* **Network Discovery** — Automatically find hosts and services on your network
* **Service Detection** — Identify 200+ services including databases, web servers, and home automation
* **Topology Visualization** — View your infrastructure from multiple perspectives — network segmentation, service dependencies, workload placement, and physical wiring
* **Distributed Scanning** — Deploy daemons across multiple networks
## Cloud vs Self-Hosted
Scanopy is available in two deployment models:
### Scanopy Cloud
* **Managed service** — We handle the server infrastructure
* **Quick setup** — Create an account and deploy daemons immediately
* **Automatic updates** — Always on the latest version
With Scanopy Cloud, you only need to deploy daemons to your networks. The server, database, and UI are managed for you.
### Self-Hosted
* **Full control** — Run everything on your own infrastructure
* **Data sovereignty** — All data stays on your servers
* **Customization** — Configure server settings, OIDC, and more
Self-hosted users need to install the Scanopy server before deploying daemons. See [Installing Scanopy (Self-Hosted)](https://scanopy.net/docs/self-hosted-server/server-installation/) for setup instructions.
## How Scanopy Works
(Cloud or Self-Hosted)"]
server --> net1
server --> net2
server --> net3
subgraph net1[" "]
d1["Network 1
Daemon"]
end
subgraph net2[" "]
d2["Network 2
Daemon"]
end
subgraph net3[" "]
d3["Network 3
Daemon"]
end
`}
/>
1. **Daemons** run on your networks and perform discovery scans
2. **Server** stores data and serves the web UI
3. **UI** provides visualization, management, and configuration
## Next Steps
* **Cloud users**: Continue to [Quick Start](https://scanopy.net/docs/quick-start/)
* **Self-hosted users**: Start with [Installing Scanopy (Self-Hosted)](https://scanopy.net/docs/self-hosted-server/server-installation/), then return to Quick Start
# Quick Start
Source: https://scanopy.net/docs/quick-start/
## Cloud
### 1. Sign up
Go to [scanopy.net](https://scanopy.net) and click **Get Started**. The onboarding wizard guides you through creating your organization and network.
### 2. Install a daemon
After onboarding, you'll be prompted to install a **daemon**. A daemon is a lightweight agent that runs on your network, discovers hosts and services, and reports findings to the server. Follow the install command shown in the modal for your platform, or see [Managing Daemons](https://scanopy.net/docs/setting-up-daemons/managing-daemons/) for details.
### 3. Run your first scan
Once the daemon connects, navigate to **Discover > Scan > Scheduled** and click **Run** on the discovery. A typical /24 subnet takes around 30 minutes depending on the number of active hosts. You can watch progress in real-time on the **Discovery** page.
### 4. Explore your network
* **Topology** — Interactive visualization of your infrastructure from multiple perspectives
* **Assets > Hosts** — All discovered devices
* **Assets > Services** — Detected services across your network
The Getting Started checklist on the home page tracks your progress through these steps.
***
## Self-Hosted
**Prerequisite:** A running Scanopy server. See [Installing Scanopy (Self-Hosted)](https://scanopy.net/docs/self-hosted-server/server-installation/).
### 1. Complete onboarding
Open your server URL. The onboarding wizard walks you through initial setup.
### 2. Wait for your first scan
Docker Compose deployments include an **integrated daemon** that automatically begins scanning your network after onboarding. The first scan is a light scan that discovers hosts and open ports. A typical /24 subnet takes around 30 minutes depending on the number of active hosts. Watch progress in real-time on the **Discovery** page.
### 3. Explore your network
* **Topology** — Interactive visualization of your infrastructure from multiple perspectives
* **Assets > Hosts** — All discovered devices
* **Assets > Services** — Detected services across your network
To scan additional networks, deploy daemons on other segments. See [Planning Daemon Deployment](https://scanopy.net/docs/setting-up-daemons/planning-daemon-deployment/).
***
## Next Steps
**Learn the features:**
* [Discovery](https://scanopy.net/docs/using-scanopy/discovery/) — How scanning works and how to configure it
* [Topology](https://scanopy.net/docs/using-scanopy/topology/) — Visualize your infrastructure from multiple perspectives
* [Hosts & Subnets](https://scanopy.net/docs/using-scanopy/network-data/) — Organize your discovered data
* [Organization & Access](https://scanopy.net/docs/using-scanopy/organization/) — Users, roles, API keys, and tags
**Expand your deployment:**
* [Planning Daemon Deployment](https://scanopy.net/docs/setting-up-daemons/planning-daemon-deployment/) — Decide how many daemons you need and where to place them
* [Managing Daemons](https://scanopy.net/docs/setting-up-daemons/managing-daemons/) — Install, upgrade, and remove daemons
# Scanopy API
Source: https://scanopy.net/docs/api/
Network topology discovery and visualization API.
## Authentication
Two authentication methods are supported:
| Method | Header | Use Case |
| -------------- | --------------------------------- | --------------------------------- |
| User API key | `Authorization: Bearer scp_u_...` | Programmatic access, integrations |
| Session cookie | `Cookie: session_id=...` | Web UI (via `/api/auth/login`) |
User API keys require your organization to have API access enabled. Create keys at **Platform > API Keys**.
## Rate Limiting
Limit: 300 requests/minute
Burst: 150
Response headers: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`
When rate limited, you'll receive HTTP `429 Too Many Requests` with a `Retry-After` header.
## Pagination
List endpoints support pagination via query parameters:
| Parameter | Type | Default | Description |
| --------- | ------- | ------- | ------------------------------------------------------- |
| `limit` | integer | 50 | Maximum results to return (1-1000). Use 0 for no limit. |
| `offset` | integer | 0 | Number of results to skip |
Example: `GET /api/v1/hosts?limit=10&offset=20`
## Response Format
All responses use a standard envelope:
```json
{
"success": true,
"data": { ... },
"meta": {
"api_version": 1,
"server_version": "0.17.11"
}
}
```
**Paginated list responses** include pagination metadata:
```json
{
"success": true,
"data": [ ... ],
"meta": {
"api_version": 1,
"server_version": "0.17.11",
"pagination": {
"total_count": 142,
"limit": 50,
"offset": 0,
"has_more": true
}
}
}
```
| Field | Description |
| ------------- | ----------------------------------------------------- |
| `total_count` | Total items matching your query (ignoring pagination) |
| `limit` | Applied limit (your request or default) |
| `offset` | Applied offset |
| `has_more` | `true` if more results exist beyond this page |
**Error responses** include an `error` field instead of `data`:
```json
{
"success": false,
"error": "Resource not found",
"meta": { ... }
}
```
**Common status codes:** `400` validation error, `401` unauthorized, `403` forbidden, `404` not found, `409` conflict, `429` rate limited.
## Versioning
Endpoints are prefixed with `/api/v1/`. The API version is an integer (`api_version: 1`) returned in every response, versioned independently from the application. Check `GET /api/version` for current versions.
**While Scanopy is pre-v1.0 (current: 0.17.11)**, the API should be considered unstable. Breaking changes may be introduced in any release without incrementing the API version. We recommend pinning to a specific Scanopy release if you depend on API stability, and reviewing the [changelog](https://scanopy.net/docs/changelog) before upgrading. After Scanopy reaches v1.0, breaking API changes will only occur with an API version increment.
## Multi-Tenancy
Resources are scoped to your **organization** and **network(s)**:
* You can only access entities within your organization
* Network-level entities (hosts, services, etc.) are filtered to networks you have access to
* Use `?network_id=` to filter list endpoints to a specific network
* API keys can be scoped to a subset of your accessible networks
# Guides
Source: https://scanopy.net/docs/guides/
# Deploying the Commercial Edition
Source: https://scanopy.net/docs/self-hosted-server/commercial-deployment/
Interested in running the commercial edition? Reach out to [licensing@scanopy.net](mailto:licensing@scanopy.net) to get a license key. You can review what the commercial self-hosted edition includes on the [pricing page](https://scanopy.net/pricing).
The commercial edition of Scanopy is the standard server, unlocked by a license key, for customers on a commercial plan. This guide covers only what differs from the standard self-hosted install — see [Installing Scanopy (Self-Hosted)](https://scanopy.net/docs/self-hosted-server/server-installation/) for requirements, verifying the install, and accessing the UI.
## What's different
The commercial deployment is identical to the community deployment except for one thing: the server process reads a license key from the `SCANOPY_LICENSE_KEY` environment variable. Both editions run the same image, `ghcr.io/scanopy/scanopy/server:latest`; the key is what enables commercial mode at startup.
There is no license file to create or place anywhere. The key is a signed token verified offline at startup — nothing to import, and no outbound call to a licensing server. Anywhere you can set an environment variable for the server process — Docker Compose, a systemd unit, your shell — is a valid place to put it.
A key that is expired or fails verification puts the server into a read-only locked state until it is replaced. The bundled `docker-compose.commercial.yml` fails fast if `SCANOPY_LICENSE_KEY` is unset, so a missing key gives you a clear error at startup rather than a server that quietly runs as the community edition.
The steps below cover the Docker Compose deployment. Running the server natively instead? Skip to [Without Docker](#without-docker).
## 1. Download the compose file
```bash
curl -O https://raw.githubusercontent.com/scanopy/scanopy/refs/heads/main/docker-compose.commercial.yml
```
## 2. Set your license key
Your license key comes with your commercial plan. If you don't have one, contact [licensing@scanopy.net](mailto:licensing@scanopy.net).
Create a `.env` file next to the compose file:
```bash
SCANOPY_LICENSE_KEY=your-license-key-here
```
Docker Compose loads `.env` automatically. Alternatively, export `SCANOPY_LICENSE_KEY` in your shell before starting.
## 3. Start Scanopy
```bash
docker compose -f docker-compose.commercial.yml up -d
```
The rest of the flow — verifying the containers are running and accessing the UI — is the same as the [standard install](https://scanopy.net/docs/self-hosted-server/server-installation/#4-verify-installation).
## Without Docker
If you run the server natively — the self-contained binary under systemd rather than a container — nothing about licensing changes. Set `SCANOPY_LICENSE_KEY` in the environment of the server process, exactly as you set `SCANOPY_DATABASE_URL`. See [Running Without Docker](https://scanopy.net/docs/self-hosted-server/server-installation/#running-without-docker) for the full native install.
The recommended place is the `EnvironmentFile` your unit already reads, so the key stays out of the unit file and off the process command line:
```bash
# /etc/scanopy/server.env
SCANOPY_LICENSE_KEY=your-license-key-here
SCANOPY_DATABASE_URL=postgresql://scanopy:your-password@localhost:5432/scanopy
```
```bash
sudo chmod 600 /etc/scanopy/server.env
sudo systemctl restart scanopy-server
```
The server also loads a `.env` file from its working directory at startup, so `/opt/scanopy/.env` works too. `EnvironmentFile=` is the better default: it doesn't depend on which directory the service happens to start in.
The fail-fast check on a missing key belongs to the compose file, not the server. A native server started without `SCANOPY_LICENSE_KEY` runs as the free community edition rather than refusing to start — so verify after any change to the unit.
## Verify the license is active
The public config endpoint reports the deployment type and license state, on Docker and native installs alike:
```bash
curl -s http://localhost:60072/api/config | jq '.data | {deployment_type, license_status, license_expiry}'
```
A licensed server returns `"deployment_type": "commercial"` and `"license_status": "valid"`. `"community"` means the key never reached the process — check the environment of the running service. `"expired"` or `"invalid"` means it did reach the process but failed verification; the server is locked read-only until the key is replaced.
## The integrated daemon
The compose stack includes an integrated daemon, initialized on first start with nothing to configure. It immediately scans your local network: Layer 2 and Layer 3 host discovery, port and service detection, and container discovery through the mounted socket.
To run daemons elsewhere instead, remove the `daemon` service from the compose file and clear `SCANOPY_INTEGRATED_DAEMON_URL` on the `server` service, then point standalone daemons at your server — see [Setting Up Daemons](https://scanopy.net/docs/setting-up-daemons/).
## Recommended: configure SMTP
Scanopy uses email for account recovery, security notifications, and operational alerts. SMTP is **optional** — the app is fully usable without it (when no email service is configured, new accounts are auto-verified, so users can register and sign in normally) — but we recommend configuring it for any multi-user deployment.
**Works without SMTP**
* Registration and sign-in work normally.
* You can still invite teammates: the invite screen generates a shareable link you copy and send yourself. SMTP only adds the convenience of having Scanopy email the invite for you.
**Requires SMTP**
* **Password reset** — recovery is email-only. Without SMTP, a user who forgets their password has no self-service way back in, so this is the main reason to set it up.
* **Security notifications** — alerts when a password or email address changes, or an SSO provider is linked or unlinked.
* **Operational alerts** — warnings when a daemon becomes unreachable or is placed on standby, plus per-scan discovery summaries.
SMTP is configured with a few environment variables on the `server` service. See [SMTP Configuration](https://scanopy.net/docs/reference/server-configuration/#smtp-configuration) in the server configuration reference for the full list and an example.
## Next steps
* [Server Configuration](https://scanopy.net/docs/reference/server-configuration/) — Configure server settings
* [Configuring Single Sign-On (OIDC)](https://scanopy.net/docs/guides/operations/oidc/) — Set up enterprise authentication
* [Quick Start](https://scanopy.net/docs/quick-start/) — Deploy your first daemon
# Self-Hosted Server
Source: https://scanopy.net/docs/self-hosted-server/
# Installing Scanopy (Self-Hosted)
Source: https://scanopy.net/docs/self-hosted-server/server-installation/
> On a commercial plan? See [Deploying the Commercial Edition](https://scanopy.net/docs/self-hosted-server/commercial-deployment/) for the license-gated server image instead of the steps below.
## Requirements
**Docker Installation (Recommended)**
* Docker Engine 20.10 or later
* Docker Compose V2
**Running Without Docker** (see [below](#running-without-docker))
* Linux on x86\_64 or arm64
* PostgreSQL 17
* 4GB RAM minimum
* 20GB disk space
No Rust or Node toolchain is needed — the server binary is prebuilt and self-contained.
## Docker Installation (Recommended)
This is the easiest way to get started with self-hosted Scanopy.
### 1. Download the Docker Compose File
```bash
curl -O https://raw.githubusercontent.com/scanopy/scanopy/refs/heads/main/docker-compose.yml
```
### 2. Review Configuration
The default `docker-compose.yml` includes:
* Scanopy server on port 60072
* PostgreSQL database
* Integrated daemon for immediate network scanning
You don't need to configure anything for the integrated daemon to work; the server will auto-initialize it when the container is started and it will immediately begin scanning. If you don't want to run a daemon in the same container as the server, remove the daemon service and clear the `SCANOPY_INTEGRATED_DAEMON_URL` env var.
**Important**: The integrated daemon assumes your Docker bridge network is `172.17.0.1`. If your Docker bridge uses a different address, edit the `SCANOPY_INTEGRATED_DAEMON_URL` environment variable in the compose file.
### 3. Start Scanopy
```bash
docker compose up -d
```
### 4. Verify Installation
Check that services are running:
```bash
docker compose ps
```
All three services should show as running: `scanopy-server`, `scanopy-postgres`, and `scanopy-daemon`.
If any service fails to start, check its logs with `docker compose logs ` and [open an issue](https://github.com/scanopy/scanopy/issues) with the output.
### 5. Access the UI
Navigate to `http://:60072`
You'll see the registration page on first load.
## Platform-Specific Instructions
### Proxmox LXC Container
You can use this [helper script](https://community-scripts.github.io/ProxmoxVE/scripts?id=scanopy) to create a Scanopy LXC on your Proxmox host.
### Unraid
Scanopy is available as an Unraid community app.
**Common Issues:**
If running Scanopy directly on a Proxmox host and encountering `could not create any Unix-domain sockets`, add this to both the PostgreSQL and Scanopy services in your docker-compose:
```yaml
security_opt:
- apparmor:unconfined
```
If running in an LXC, you may need to change `SCANOPY_INTEGRATED_DAEMON_URL` to `172.31.0.1`.
See [issue #87](https://github.com/scanopy/scanopy/issues/87) for more details.
## Running Without Docker
The server ships as a single self-contained binary: migrations and the web UI are compiled in, so there is nothing to place beside it and no build step. Postgres is the only thing it needs.
### 1. Download the binary
```bash
sudo useradd --system --home /opt/scanopy --shell /usr/sbin/nologin scanopy
sudo mkdir -p /opt/scanopy
# Use scanopy-server-linux-arm64 on arm64 hosts
sudo curl -L -o /opt/scanopy/scanopy-server \
https://github.com/scanopy/scanopy/releases/latest/download/scanopy-server-linux-amd64
sudo chmod +x /opt/scanopy/scanopy-server
sudo chown -R scanopy:scanopy /opt/scanopy
```
The binaries are statically linked against musl, so they run on any glibc or musl distribution — Debian, RHEL, Alpine — with no runtime dependencies to install.
### 2. Create the database
Scanopy needs a PostgreSQL 17 database. On Debian:
```bash
sudo apt install postgresql
sudo -u postgres createuser --pwprompt scanopy
sudo -u postgres createdb --owner=scanopy scanopy
```
The server creates its own schema on first start and applies pending migrations on every start, so there is no manual schema step — on install or on upgrade.
### 3. Configure the server
Every setting is a `SCANOPY_*` [environment variable](https://scanopy.net/docs/reference/server-configuration/). There is no config file, and nothing is Docker-specific: the same variables that appear in a compose file work identically here. Set them however your setup allows — a systemd `EnvironmentFile` (recommended below), a `.env` file in the server's working directory, or a plain shell export.
Only `SCANOPY_DATABASE_URL` is strictly required. `SCANOPY_PUBLIC_URL` is used in invite and password-reset links, so set it to the address your users reach the server at.
```bash
sudo mkdir -p /etc/scanopy
sudo tee /etc/scanopy/server.env >/dev/null <<'EOF'
SCANOPY_DATABASE_URL=postgresql://scanopy:your-password@localhost:5432/scanopy
SCANOPY_PUBLIC_URL=http://your-server:60072
SCANOPY_LOG_LEVEL=info
EOF
sudo chown scanopy:scanopy /etc/scanopy/server.env
sudo chmod 600 /etc/scanopy/server.env
```
Commercial plans add `SCANOPY_LICENSE_KEY` to that same file — see [Deploying the Commercial Edition](https://scanopy.net/docs/self-hosted-server/commercial-deployment/#without-docker).
### 4. Run it under systemd
Create `/etc/systemd/system/scanopy-server.service`:
```ini
[Unit]
Description=Scanopy Server
After=network.target postgresql.service
[Service]
Type=simple
User=scanopy
WorkingDirectory=/opt/scanopy
EnvironmentFile=/etc/scanopy/server.env
ExecStart=/opt/scanopy/scanopy-server
Restart=on-failure
[Install]
WantedBy=multi-user.target
```
`EnvironmentFile` is preferred over a `.env` file because it does not depend on which directory the service happens to start in.
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now scanopy-server
sudo systemctl status scanopy-server
```
Then access the UI at `http://:60072`, same as the Docker install. Reload configuration changes with `sudo systemctl restart scanopy-server`.
### 5. Add a daemon
No daemon is bundled with the server binary, so install one separately: download the daemon release binary for your platform and enrol it against the server. See [Setting Up Daemons](https://scanopy.net/docs/setting-up-daemons/).
To reproduce the Docker stack's zero-config integrated daemon, start a daemon on the same host listening on port 60073 **before** you register the first user, and set `SCANOPY_INTEGRATED_DAEMON_URL=http://127.0.0.1:60073`. The server provisions that daemon during first-user registration — setting the variable later has no effect on a deployment that already has an account. Leave it unset otherwise.
## Uninstalling
### Docker Installation
```bash
# Stop and remove containers
docker compose down
# Remove volumes (deletes all data)
docker compose down -v
# Remove images
docker rmi ghcr.io/scanopy/scanopy/server:latest
docker rmi ghcr.io/scanopy/scanopy/daemon:latest
```
### Native Installation
```bash
sudo systemctl disable --now scanopy-server
sudo rm /etc/systemd/system/scanopy-server.service
sudo systemctl daemon-reload
sudo rm -rf /opt/scanopy /etc/scanopy
# Drop the database (deletes all data)
sudo -u postgres dropdb scanopy
```
## Next Steps
* [Server Configuration](https://scanopy.net/docs/reference/server-configuration/) — Configure server settings
* [Configuring Single Sign-On (OIDC)](https://scanopy.net/docs/guides/operations/oidc/) — Set up enterprise authentication
* [Quick Start](https://scanopy.net/docs/quick-start/) — Deploy your first daemon
# Troubleshooting
Source: https://scanopy.net/docs/self-hosted-server/troubleshooting/
Common issues and solutions for self-hosted Scanopy server deployments.
For daemon-related issues, see [Daemon Troubleshooting](https://scanopy.net/docs/setting-up-daemons/troubleshooting-setup/).
## Server Issues
### Port Already in Use
**Symptoms**: Server fails to start with "address already in use"
**Solution**: Change the port mapping in docker-compose.yml:
```yaml
ports:
- '8080:60072' # Change 60072 to any available port
```
### Browser Shows "SSL Protocol Error"
**Symptoms**: Browser displays "ERR\_SSL\_PROTOCOL\_ERROR" when accessing Scanopy
**Cause**: Using `https://` instead of `http://`. Scanopy doesn't handle TLS directly.
```
http://your-server:60072
```
**Solution**: Use `http://` to access Scanopy directly. For HTTPS, put a reverse proxy (Traefik, Nginx, Caddy) in front to handle TLS termination.
### PostgreSQL "Could not create any Unix-domain sockets" (Proxmox)
**Symptoms**: PostgreSQL container fails to start on Proxmox host with socket creation error
**Cause**: AppArmor security policy blocking socket creation.
**Solution**: Add to both PostgreSQL and Scanopy services in docker-compose.yml:
```yaml
security_opt:
- apparmor:unconfined
```
See [issue #87](https://github.com/scanopy/scanopy/issues/87) for details.
### Integrated Daemon Not Initializing
**Symptoms**: Integrated daemon shows in UI but doesn't start discovery
**Diagnosis**:
```bash
# Check daemon logs
docker logs scanopy-daemon
# Check if daemon can reach server
docker exec scanopy-daemon curl http://scanopy-server:60072/api/health
```
**Solutions**:
1. **Verify bridge network**: Check your Docker bridge IP
```bash
docker network inspect bridge | grep Gateway
```
2. **Update compose file**: If gateway isn't `172.17.0.1`, update `SCANOPY_INTEGRATED_DAEMON_URL`
3. **Check the daemon record**: Server setup [provisions](https://scanopy.net/docs/reference/daemon-identity/) the integrated daemon and its API key. Confirm the daemon appears in **Discover > Scan > Daemons** and that its `daemon-config` volume still holds the key — a wiped volume leaves the daemon with no identity to present.
## Database Issues
### How to Backup Data
Scanopy stores all data in PostgreSQL. To backup:
**Docker setup**:
```bash
# Backup
docker exec scanopy-db pg_dump -U postgres scanopy > scanopy_backup.sql
# Restore
docker exec -i scanopy-db psql -U postgres scanopy < scanopy_backup.sql
```
**Manual setup**: Use standard PostgreSQL backup tools (pg\_dump, pg\_restore).
### How to Reset Password
If SMTP is configured, use the "Forgot Password" link on the login page.
If SMTP is not configured:
1. Generate a new password hash using bcrypt
2. Update the `users` table with the new hash
3. Or, ask another Owner to delete and re-invite you
### How to Delete All Data
To start fresh:
```bash
docker compose down -v # Removes all volumes including database
docker compose up -d # Start fresh
```
## Reverse Proxy Setup
Configure your reverse proxy (Nginx, Traefik, Caddy) to forward traffic to port 60072.
For HTTPS, enable secure cookies in docker-compose.yml:
```yaml
environment:
- SCANOPY_USE_SECURE_SESSION_COOKIES=true
```
See [Server Configuration](https://scanopy.net/docs/reference/server-configuration/#session-security) for details.
## Getting Help
If your issue isn't covered here:
* **Discord**: Join our [Discord community](https://discord.gg/b7ffQr8AcZ)
* **GitHub Issues**: [Open an issue](https://github.com/scanopy/scanopy/issues/new)
# Upgrading a Self-Hosted Server
Source: https://scanopy.net/docs/self-hosted-server/upgrading/
## Upgrading with Docker Compose
Pull the latest images and recreate the containers:
```bash
docker compose pull
docker compose up -d
```
**Podman users**: If you pull the daemon image by tag (e.g., `podman pull …/daemon:v0.15.2`), add
`--platform linux/amd64` (or `linux/arm64`) to avoid pulling the wrong architecture. See
[Troubleshooting — Podman Pulls Wrong Daemon
Version](https://scanopy.net/docs/setting-up-daemons/troubleshooting-setup/#podman-pulls-wrong-daemon-version-amd64).
This upgrades both the server and any daemons defined in your compose file. Volumes are preserved, so no re-registration or reconfiguration is needed.
If you use the integrated daemon, do not delete the `daemon-config` volume during upgrades — it
stores the daemon's identity (ID, API key, and network assignment). That identity is [provisioned
once at server setup](https://scanopy.net/docs/reference/daemon-identity/) and cannot be re-derived, so deleting the
volume means provisioning the daemon again from scratch.
After restarting, the daemon reports its new version to the server and the version status should immediately show **Current** in the UI.
If you run standalone daemons on separate hosts, you'll need to upgrade those independently — see [Managing Daemons](https://scanopy.net/docs/setting-up-daemons/managing-daemons).
## Upgrading a native install
Replace the binary and restart the service:
```bash
sudo systemctl stop scanopy-server
sudo curl -L -o /opt/scanopy/scanopy-server \
https://github.com/scanopy/scanopy/releases/latest/download/scanopy-server-linux-amd64
sudo chmod +x /opt/scanopy/scanopy-server
sudo chown scanopy:scanopy /opt/scanopy/scanopy-server
sudo systemctl start scanopy-server
```
Migrations ship inside the binary and apply automatically on start, so there is no separate migration step. Your configuration lives in the environment file, which the new binary picks up unchanged.
Check that it came back up before walking away:
```bash
systemctl status scanopy-server
curl -s http://localhost:60072/api/health
```
## Daemon still shows Outdated after upgrading
If a daemon still shows [**Outdated**](https://scanopy.net/docs/reference/daemon-status#outdated) after upgrading:
### 1. Verify the daemon is running the new image
```bash
docker compose logs daemon | head -20
```
The startup log prints the daemon version. If it still shows the old version, the image wasn't actually pulled. Pull it explicitly and restart:
```bash
docker compose pull daemon
docker compose up -d daemon
```
### 2. Check that the daemon can reach the server
If the daemon can't connect to the server (for example, due to network issues between containers), it can't announce its version. Look for connection errors in the daemon logs:
```bash
docker compose logs daemon
```
### 3. Wait for the next poll cycle
The daemon version is also reported on every regular poll to the server, so even if the startup announcement fails, the version will update within a few seconds on the next successful poll.
# Architecture
Source: https://scanopy.net/docs/reference/architecture/
Technical overview of Scanopy's system design, components, and data flows.
## Components
### Server
**Purpose**: Central hub for data storage, API, and web UI serving
**Responsibilities**:
* Store network discovery data in PostgreSQL
* Serve REST API for daemons and UI
* Generate topology visualizations
* Manage user authentication and sessions
* Orchestrate scheduled discoveries
* Handle organization and user management
* Provide real-time updates via Server-Sent Events
**Implementation**:
* Language: Rust
* Framework: Axum (async web framework)
* Database: PostgreSQL 17 with sqlx
* Authentication: tower-sessions + OIDC (openidconnect crate)
* Frontend bundling: Integrated Svelte build in Docker image
**Runs as**: Docker container (recommended) or standalone binary
### Daemon
**Purpose**: Distributed discovery agent that scans networks and reports findings
**Responsibilities**:
* Scan IPv4 addresses on configured subnets
* Detect open TCP ports
* Identify services via pattern matching
* Connect to integrations it holds credentials for
* Report the subnets it has interfaces on
* Maintain server connection via periodic polling (each poll implicitly updates the daemon's health status)
* Execute scheduled discovery tasks
**Implementation**:
* Language: Rust
* Network scanning: Custom async TCP scanner with tokio
* Docker API: bollard crate for Docker socket communication
* Service detection: Pattern matching engine with 200+ definitions
* Configuration: JSON file + environment variables + CLI args
**Runs as**: Docker container (Linux only), or a native background service on Linux, macOS, Windows, and FreeBSD (installed via `scanopy-daemon install`)
### UI
**Purpose**: Web-based interface for viewing and managing network data
**Responsibilities**:
* Display interactive topology diagrams
* Provide CRUD interfaces for all entities
* Monitor discovery sessions in real-time
* Manage users and organizations
* Configure discovery schedules
* Export topology visualizations
**Implementation**:
* Framework: Svelte 5 + SvelteKit
* State management: Svelte stores with derived reactivity
* Visualization: @xyflow/svelte for topology rendering
* Forms: svelte-forms with custom validation
* Styling: Tailwind CSS
* Real-time: Native EventSource for SSE
**Runs as**: Static files served by the server (bundled in Docker image)
## Data Flows
### DaemonPoll Mode
DaemonPoll is the default mode. The daemon initiates all connections to the server, making it ideal for daemons behind NAT or firewalls.
#### Initialization (DaemonPoll)
>S: POST /api/daemons/register
{network_id, name}
S->>S: Create/update daemon record
S-->>D: {daemon_id, network_id}
Note over D: Store daemon_id for future requests
`}
/>
#### Runtime Polling (DaemonPoll)
>S: POST /api/daemons/work
{daemon_id, status, interfaces}
alt Has pending work
S-->>D: {work: DiscoverySession}
D->>D: Execute discovery
else No work
S-->>D: {work: null}
end
end
`}
/>
#### Discovery Flow (DaemonPoll)
1. User triggers a scan via the UI or API — the server creates a discovery session in **Pending** state
2. On its next polling cycle, the daemon receives the pending session as part of the `request-work` response
3. The server transitions the session to **Starting** to prevent re-dispatch to other polls
4. The daemon executes the discovery (network scan, Docker scan, or self-report depending on type)
5. The daemon sends periodic progress updates to the server, including discovered entities
6. The server processes entities immediately — deduplicating and persisting via upsert
7. The server broadcasts progress to the UI via SSE
8. The daemon sends a terminal phase (**Completed** or **Failed**) to end the session
>S: Trigger scan
S->>S: Create session (Pending)
Note over D: Next 30s poll cycle
D->>S: POST /api/daemons/work
S->>S: Transition to Starting
S-->>D: {work: DiscoverySession}
D->>D: Execute discovery
loop Progress updates
D->>S: POST progress + entities
S->>S: Deduplicate & persist
S-->>UI: SSE update
end
D->>S: POST terminal phase (Completed/Failed)
`}
/>
### ServerPoll Mode
ServerPoll mode is for DMZ deployments where the daemon cannot make outbound connections. The server initiates all connections to the daemon. Requires the daemon to be network-accessible from the server.
#### Initialization (ServerPoll)
>S: POST /api/daemons/provision
{name, network_id, url}
S->>S: Create daemon record + API key
S-->>A: {daemon_id, api_key}
Note over A: Configure daemon with API key
Note over D: Daemon starts, waits for server
S->>D: GET /api/poll (first contact)
D-->>S: {daemon_id: null, interfaces}
S->>S: Assign daemon_id, link to record
S-->>D: {daemon_id, config}
`}
/>
#### Runtime Polling (ServerPoll)
>D: GET /api/poll
D-->>S: {status, interfaces}
alt Daemon has buffered entities
S->>D: GET /api/poll/entities
D->>B: Fetch pending entities
B-->>D: Entities with Pending status
D-->>S: {entities: [...]}
S->>S: Process and store entities
S->>D: POST /api/discovery/entities-created
{entity_ids: [...]}
D->>B: Mark entities as Created
end
end
`}
/>
#### Discovery Flow (ServerPoll)
(manual or scheduled)"]
step2["2. Server creates discovery session"]
step3["3. Server polls daemon,
sends discovery instruction"]
step1 --> step2 --> step3
subgraph step4["4. Daemon executes discovery"]
scan1["Network scanning:
Scans IP range, detects ports/services"]
scan2["Docker scanning:
Queries Docker API for containers"]
scan3["Self-report:
Reports interfaced subnets"]
end
step3 --> step4
subgraph step5["5. Daemon buffers entities locally"]
buf1["Store in EntityBuffer"]
buf2["Status: Pending"]
end
step4 --> step5
subgraph step6["6. Server fetches entities on next poll"]
fetch1["GET /api/poll/entities"]
fetch2["Process and store"]
fetch3["POST /api/discovery/entities-created"]
end
step5 --> step6
step6 --> step7["7. Daemon confirms, marks Created"]
step7 --> step8["8. Server broadcasts updates via SSE"]
`}
/>
### Mode Comparison
| Aspect | DaemonPoll | ServerPoll |
| --------------------- | ------------------------- | ------------------------------ |
| Connection direction | Daemon → Server | Server → Daemon |
| Setup | Daemon self-registers | Admin provisions in UI |
| Firewall requirements | Outbound only from daemon | Inbound to daemon (port 60073) |
| Best for | NAT/firewall environments | DMZ deployments |
| Entity handling | Immediate processing | Buffered with confirmation |
## Discovery Pipeline
### Network Scanning
Network scanning runs in three phases:
1. **ARP Discovery** — The daemon sends ARP broadcasts across configured subnets to discover active hosts. This is the primary mechanism for finding devices on the local network, returning IP and MAC address pairs.
2. **Deep Scanning** — Each discovered host is scanned in detail:
* TCP and UDP port scanning on configurable port lists
* DNS reverse lookup for hostname resolution
* SNMP queries for system information (if credentials are configured)
* Service detection by matching open ports against 235 service definitions
3. **Grace Period** — A brief window after deep scanning completes to catch any late ARP responses. Newly discovered hosts receive a full deep scan before the session completes.
The UI displays real-time progress across all three phases via Server-Sent Events.
phase2 --> phase3 --> report
`}
/>
### Docker Discovery
list --> inspect --> match --> relationships --> report
`}
/>
***
**For implementation details**, see the [source code](https://github.com/scanopy/scanopy).
# Daemon Configuration
Source: https://scanopy.net/docs/reference/daemon-configuration/
## Configuration Priority
Scanopy daemons use the following priority order (highest to lowest):
1. **Command-line arguments** (highest priority)
2. **Environment variables**
3. **Configuration file**
4. **Default values** (lowest priority)
Later sources override earlier ones. For example, an environment variable overrides the config file but is overridden by a command-line argument.
## Configuration Methods
**Command-line arguments** (also accepted by `scanopy-daemon install`, which persists them to the config file — see [Running as a Service](https://scanopy.net/docs/setting-up-daemons/managing-daemons/#running-as-a-service)):
```bash
scanopy-daemon --server-url http://192.168.1.100:60072 --api-key YOUR_KEY
```
**Environment variables**:
```bash
export SCANOPY_SERVER_URL=http://192.168.1.100:60072
export SCANOPY_DAEMON_API_KEY=YOUR_KEY
scanopy-daemon
```
**Docker environment**:
```yaml
environment:
- SCANOPY_SERVER_URL=http://192.168.1.100:60072
- SCANOPY_DAEMON_API_KEY=YOUR_KEY
```
**Configuration file**:
A daemon you run yourself creates its config file under your user profile:
* **Linux**: `~/.config/scanopy/daemon/config.json`
* **macOS**: `~/Library/Application Support/com.scanopy.daemon/config.json`
* **Windows**: `%APPDATA%\scanopy\daemon\config.json`
A daemon installed as a service uses a system directory instead. A background service runs under a different profile than whoever installed it, so `install` writes the config to a system location and points the service at it explicitly. The install command prints the config and log paths it settled on — read them off the output rather than guessing.
Use `--config-dir` / `SCANOPY_CONFIG_DIR` to override the directory holding `config.json`.
The config file stores runtime state (daemon ID, host ID) alongside your settings. Command-line and environment variables take priority over the file.
## Parameter Reference
## Seeding Credentials at Registration
### `--credential-id` / `SCANOPY_CREDENTIAL_IDS`
Seeds one or more credentials for the daemon to use on its first scan, so it collects everything a [credential unlocks](https://scanopy.net/docs/using-scanopy/discovery/#what-credentials-add) before you assign anything in the UI. Provide them as a repeatable `--credential-id` flag or as a comma-separated `SCANOPY_CREDENTIAL_IDS` environment variable.
Each entry is a credential ID, optionally targeted at specific IP addresses:
| Form | Applies the credential to |
| ------------------ | --------------------------------------------------------------------------- |
| `` | The whole network (network default — tried on every host the daemon scans). |
| `@` | The host discovered at ``. |
| `@+` | Several specific hosts — join IPs with `+`. |
To target the daemon's **own** host — for example a local Docker or Podman socket credential — use its loopback address: `@127.0.0.1`.
```bash
# CLI (repeatable)
scanopy-daemon --credential-id 1a2b3c4d-...@192.168.1.10 --credential-id 5e6f7a8b-...@127.0.0.1
# Environment (comma-separated)
export SCANOPY_CREDENTIAL_IDS="1a2b3c4d-...@192.168.1.10,5e6f7a8b-...@127.0.0.1"
```
After the first scan, seeded credentials are [auto-assigned](https://scanopy.net/docs/using-scanopy/credentials/#auto-assignment) to the matching hosts and used on every subsequent scan.
Seeded credentials are applied **only at first registration**. If the daemon is already registered, they are ignored and the daemon logs a reminder to manage its credentials in the Scanopy UI instead.
## Logging
The daemon logs to stdout and stderr on all platforms; where that output lands depends on how you run it. For reading logs on each platform and capturing them to a file, see [Checking Daemon Logs](https://scanopy.net/docs/setting-up-daemons/troubleshooting-setup/#checking-daemon-logs).
Set the log level with `--log-level` or `SCANOPY_LOG_LEVEL`:
| Level | Description |
| ------- | ----------------------------------------------- |
| `error` | Errors that prevent normal operation |
| `warn` | Unexpected conditions that don't stop operation |
| `info` | General operational messages (default) |
| `debug` | Detailed diagnostic information |
| `trace` | Very verbose output, including raw network data |
```bash
# CLI
scanopy-daemon --log-level debug
# Environment
export SCANOPY_LOG_LEVEL=debug
```
# Daemon Identity
Source: https://scanopy.net/docs/reference/daemon-identity/
Every daemon has a server-side record and an API key bound to it. This page describes how that
binding is established and enforced. For the settings a daemon reads at runtime, see
[Daemon Configuration](https://scanopy.net/docs/reference/daemon-configuration/). For creating and managing daemons, see
[Managing Daemons](https://scanopy.net/docs/setting-up-daemons/managing-daemons/).
## Provisioning
Creating a daemon in **Discover > Scan > Daemons** provisions it:
1. The server creates the daemon record.
2. The server mints an API key and binds it to that record.
3. The wizard emits an install artifact carrying the key.
The key's plaintext is available only at the moment it is minted. It appears in the install
artifact the wizard shows you and is not retrievable afterwards.
You do not create daemon API keys yourself — provisioning mints them. The daemon tab under
**Platform > API Keys** is an inventory of the keys that exist, with no way to add one.
## Key binding
A provisioned key belongs to exactly one daemon, and a daemon has exactly one key.
| Property | Behavior |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Binding** | One key, one daemon, enforced by the server. |
| **Re-provisioning** | Minting a new key for a daemon deletes the key it replaces. The old key stops authenticating immediately. |
| **Plaintext storage** | ServerPoll only. The server holds the plaintext because it presents the key when it dials the daemon. DaemonPoll daemons carry their own key and the server stores only a hash. |
## How the server identifies a daemon
| Key type | Identity comes from | `X-Daemon-ID` header |
| ----------------------- | ------------------- | ------------------------------------------------------------------------------------------------- |
| Bound (1:1) | The key itself | Optional. If present and it doesn't match the key's daemon, the request is rejected as key reuse. |
| Legacy (network-shared) | The header | Required. Without it the daemon is unidentifiable and the request is rejected. |
## Version floor
Server-provisioned identity requires **daemon version 0.17.5 or later**. A 0.17.5+ daemon starts
with no identity of its own and learns which daemon it is from the key during its first handshake
with the server.
Daemons below 0.17.5 self-register against a network-shared key and identify themselves with the
`X-Daemon-ID` header. Both models are accepted concurrently — which one applies is determined by
the shape of the key presented, not by the daemon's version.
## Legacy network-shared keys
Keys minted before 1:1 provisioning are not bound to any daemon. A single such key is shared by
every daemon on its network.
* They continue to authenticate. Nothing expires them.
* They cannot be created anymore.
* They are marked as legacy in **Platform > API Keys**.
* A daemon using one can be given a key of its own without losing its host, discovery jobs, or
history — see
[Binding a key to a legacy daemon](https://scanopy.net/docs/setting-up-daemons/managing-daemons/#binding-a-key-to-a-legacy-daemon).
## Fixed and editable properties
Provisioning fixes the properties that define what a daemon *is*. The rest can be edited on the
daemon record afterwards.
| Property | |
| -------------------- | ---------------------------------------------------------------------------------------- |
| Network | Fixed at provision time |
| Polling mode | Fixed at provision time |
| Name | Fixed at provision time |
| Host | Created at provision time |
| Key binding | Maintained by provisioning |
| Version | Self-reported by the daemon on each handshake |
| Liveness and standby | Server-managed |
| URL | Editable, ServerPoll only — a DaemonPoll daemon is never dialed, so it has no URL to set |
| Maintainer, tags | Editable |
A daemon's identity is tied to the machine it runs on, so there is no way to move one to a
different host. See [Can I migrate a daemon?](https://scanopy.net/docs/setting-up-daemons/managing-daemons/#can-i-migrate-a-daemon).
# Daemon Status
Source: https://scanopy.net/docs/reference/daemon-status/
Scanopy monitors daemon health and displays a status tag next to each daemon name in the UI. Every daemon carries exactly one tag, showing the most important condition that currently applies.
## Status Reference
| Status | Meaning |
| ------------------------------------------ | ----------------------------------------------------------- |
| | Running current version, communicating normally |
| | Provisioned, but the daemon has never contacted the server |
| | No discovery session in the last 30 days |
| | Server cannot reach the daemon |
| | Version has a scheduled sunset date; still works until then |
| | Past its sunset date; the server rejects it |
| | Newer daemon version available |
| | The server has no recorded version for this daemon |
## Awaiting Connection
**Blue tag**. The daemon record exists but the daemon has never checked in.
Every daemon passes through this state. Creating a daemon [provisions](https://scanopy.net/docs/reference/daemon-identity/) its record on the server before the daemon itself exists, so the tag appears as soon as you finish the wizard and clears on first contact.
**How to resolve**:
1. Run the install artifact from the wizard on the daemon host — until you do, there is nothing to connect
2. Confirm the daemon process or service is running (`systemctl status scanopy-daemon`, `launchctl print system/com.scanopy.daemon`, `Get-Service scanopy-daemon`, or `service scanopy-daemon status`)
3. **DaemonPoll**: check the daemon can reach the server on port 60072
4. **ServerPoll**: check the daemon's URL is correct and the server can reach it on port 60073
5. Check the daemon logs for registration errors — `daemon_not_provisioned` means a 0.17.5+ daemon tried to register itself against a record that doesn't exist, which happens when it was installed by hand with a legacy shared key instead of from a provisioned daemon's install artifact
If the tag persists, see [Daemon Troubleshooting](https://scanopy.net/docs/setting-up-daemons/troubleshooting-setup).
## Standby
**Purple tag**. The daemon is registered but hasn't run a discovery session in over 30 days.
**How to resolve**:
* Run a manual discovery session to bring the daemon back to active status
* Verify the daemon process is still running on the host
* Check that scheduled discovery is configured if you expect automatic scans
## Unreachable
**Red tag**. The server cannot reach a ServerPoll daemon at its configured URL.
**How to resolve**:
1. Verify the daemon service is running on the host (`systemctl status scanopy-daemon`, `launchctl print system/com.scanopy.daemon`, `Get-Service scanopy-daemon`, or `service scanopy-daemon status`)
2. Check that the daemon's configured URL is correct and reachable from the server
3. Check firewall rules — the server must be able to connect to the daemon
4. Restart the daemon's service and retry
See [Daemon Troubleshooting](https://scanopy.net/docs/setting-up-daemons/troubleshooting-setup) for detailed connectivity debugging.
## Deprecated
**Orange tag**. The daemon is running a version with a **scheduled sunset date**. It still connects and runs discovery normally until that date, after which it becomes [Unsupported](#unsupported). The daemon card and upgrade dialog show the exact date and a countdown, and a banner appears on the Daemons view while any daemon needs updating.
When a sunset is announced, the organization owner and each affected daemon's maintainer receive **one** email listing every affected daemon and the date. You are not emailed again about the same sunset.
**How to resolve**:
* Upgrade the daemon before the sunset date — see [Managing Daemons](https://scanopy.net/docs/setting-up-daemons/managing-daemons) for installation steps. Upgrading preserves all configuration.
## Unsupported
**Red tag**. The daemon's version is past its sunset date. The server **rejects every request** from it, so it can no longer connect or run discovery. The daemon exits and, under a service manager configured to restart it, will keep restarting and failing until it is updated.
**How to resolve**:
* Upgrade the daemon to the latest version — see [Managing Daemons](https://scanopy.net/docs/setting-up-daemons/managing-daemons). Once updated it reconnects automatically.
## Outdated
**Yellow tag**. A newer daemon version is available. The current version still works but may be missing features or fixes.
**How to resolve**:
* Upgrade the daemon at your convenience — see [Managing Daemons](https://scanopy.net/docs/setting-up-daemons/managing-daemons) for installation steps
## Unknown
**Gray tag**. The server has no version on record for this daemon, because the daemon has not reported one. A version the server cannot confirm is treated as unsupported wherever a support floor applies.
**How to resolve**:
* Confirm the daemon is running a current release and has connected at least once. If it persists on a recent daemon, check the daemon logs for registration errors.
## Healthy
**Green tag**. The daemon is running a current version and communicating with the server normally.
## Status Priority
When multiple conditions apply, the UI shows the highest-priority one:
**Unsupported > Unreachable > Standby > Awaiting Connection > Deprecated > Outdated > Unknown > Healthy**
The Home tab shows all active statuses across your daemons.
## Troubleshooting
If a daemon shows an unexpected status:
* **Check daemon logs** for errors or connection failures
* **Verify connectivity** between server and daemon (for ServerPoll daemons)
* **Upgrade the daemon** if Deprecated or Outdated
* **Run a discovery session** if a daemon is in Standby
For more help, see [Daemon Troubleshooting](https://scanopy.net/docs/setting-up-daemons/troubleshooting-setup) and [Managing Daemons](https://scanopy.net/docs/setting-up-daemons/managing-daemons).
# Database Schema
Source: https://scanopy.net/docs/reference/database-schema/
Scanopy uses PostgreSQL to store all network discovery data. This page provides an overview of the database schema and entity relationships.
## Entity Relationship Diagram
## Core Tables
## Key Relationships
### Host → Services → Bindings
The discovery pipeline creates this hierarchy:
1. **Host** — A discovered device
2. **Interface** — Network interface on the host (IP + MAC)
3. **Port** — Open port on the host
4. **Service** — Identified service (e.g., PostgreSQL, Nginx)
5. **Binding** — Links a service to an interface and/or port
This structure allows a single service to be accessible on multiple interfaces (e.g., a database listening on both LAN and Docker bridge networks).
### Network Isolation
All discovery data is scoped to a network:
* Hosts, subnets, services, interfaces, ports, and bindings all have a `network_id` foreign key
* Networks belong to organizations
* Users are granted access to specific networks via `user_network_access`
### Cascade Deletes
Most foreign keys use `ON DELETE CASCADE`:
* Deleting an organization removes all its networks, users, and tags
* Deleting a network removes all hosts, services, subnets, and discovery data
* Deleting a host removes all its interfaces, ports, and services
## Full Schema Reference
For the complete schema with all columns and types, see the detailed ER diagram below. Use the zoom controls or fullscreen mode to explore.
# FAQ
Source: https://scanopy.net/docs/reference/faq/
## General
### Is IPv6 supported?
Not currently. See [Limitations](https://scanopy.net/docs/reference/limitations/) for details on what's planned.
### What's the difference between Cloud and Self-Hosted?
**Scanopy Cloud**:
* Managed server infrastructure
* Quick setup — just deploy daemons
* Automatic updates
* Subscription pricing
**Self-Hosted**:
* Run everything on your infrastructure
* Full data control
* One-time purchase or free (Community Edition)
* You manage updates and backups
Both versions use the same daemon software and have the same features for network discovery.
### How do I contribute?
We welcome contributions! See [contributing.md](https://github.com/scanopy/scanopy/blob/main/CONTRIBUTING.md) for:
* Adding service definitions (great first contribution!)
* Reporting bugs
* Requesting features
* Submitting pull requests
Join our [Discord community](https://discord.gg/b7ffQr8AcZ) for help and discussions.
### What are the four topology perspectives?
Each perspective answers a different question about your infrastructure:
* **L3 Logical** — How is my network logically segmented?
* **Application** — How do my services depend on each other?
* **Workloads** — Where does everything run, and what's inside what?
* **L2 Physical** — How is my network physically wired?
See [Topology](https://scanopy.net/docs/using-scanopy/topology/) for details on each perspective.
### Why is my Application perspective empty?
The Application perspective requires you to define application groups and assign services before it shows anything. The first time you open it, a setup wizard walks you through this. See [Application Perspective](https://scanopy.net/docs/using-scanopy/topology/application) for details.
## Discovery
### How long does discovery take?
See [Discovery Duration](https://scanopy.net/docs/using-scanopy/discovery/scans/#discovery-duration) for benchmarks and factors affecting scan speed.
### Why is my topology empty after discovery?
1. **Did discovery run at all?** Check **Discover > Scan > Sessions** for errors
2. **Wrong network selected**: Check topology options panel for network filter
3. **All services hidden**: Check if service category filters are too aggressive
4. **No hosts found**: Verify daemon can reach the network
### Can I scan networks the daemon isn't directly connected to?
Yes, but you'll miss some data. See [Layer 2 vs Layer 3](https://scanopy.net/docs/reference/limitations/#layer-2-vs-layer-3) for details on what's available for remote subnets.
For best results, deploy a daemon on each network segment. See [Planning Daemon Deployment](https://scanopy.net/docs/setting-up-daemons/planning-daemon-deployment/).
### When does a host show as stale?
When discovery hasn't observed it within its network's staleness window — **28 days** by default, adjustable per network. See [Staleness](https://scanopy.net/docs/using-scanopy/staleness/).
## Networking
### What ports does Scanopy use?
| Component | Port | Purpose |
| --------- | ----- | ------------------------------------------- |
| Server | 60072 | Web UI and API |
| Daemon | 60073 | ServerPoll mode (server connects to daemon) |
### Does the daemon need inbound firewall rules?
**DaemonPoll mode** (default): No. The daemon only makes outbound connections to the server.
**ServerPoll mode**: Yes. The server needs to reach the daemon on port 60073.
## Getting Help
* **Discord**: [discord.gg/b7ffQr8AcZ](https://discord.gg/b7ffQr8AcZ)
* **GitHub Issues**: [github.com/scanopy/scanopy/issues](https://github.com/scanopy/scanopy/issues)
# Reference
Source: https://scanopy.net/docs/reference/
# Limitations
Source: https://scanopy.net/docs/reference/limitations/
Scanopy is designed for discovering and visualizing local network infrastructure. This page documents known limitations and boundaries.
## Network Scanning
### IPv6
Scanopy currently supports IPv4 only.
**Planned**:
* Collecting and displaying IPv6 addresses during discovery
* Manual entry of IPv6 addresses when editing hosts
**Not planned**:
* Full IPv6 subnet scanning (a /64 contains 18 quintillion addresses)
### Large Subnets
Trying to scan a /8? That's 16 million IPs. Scanopy automatically skips subnets larger than /10 to prevent accidental mega-scans. Still, a /10 will probably take a couple of days to fully scan.
If you need to scan a large network, break it into smaller subnets and scan them individually.
### Layer 2 vs Layer 3
For subnets the daemon is directly connected to:
* MAC addresses are collected via ARP
* All responsive hosts are discovered
For remote subnets (routed through a gateway):
* No MAC addresses (ARP doesn't cross routers)
* Only hosts with open ports are discovered
* Some hosts may not respond to remote probes
For best results, deploy a daemon on each network segment. See [Planning Daemon Deployment](https://scanopy.net/docs/setting-up-daemons/planning-daemon-deployment/).
## Service Detection
### Pattern-Based Detection
Scanopy detects services by matching against known patterns (ports, HTTP endpoints, headers). This means:
* Services on non-standard ports may not be detected
* Services must respond to HTTP probes to match endpoint patterns
* Custom or obscure services won't be detected automatically
See [Service Detection](https://scanopy.net/docs/reference/service-detection/) for how detection works and how to improve it.
### What Can't Be Detected
* Services that don't listen on network ports
* Services behind authentication that block probing
* Internal application components (microservices behind a gateway)
* Cloud/SaaS services (Scanopy scans your network, not the internet)
## SNMP
* **SNMPv3 is AuthPriv only** — authentication *and* privacy are required. AuthNoPriv and NoAuthNoPriv modes are not supported, and MD5 authentication is not available (use SHA-1 or SHA-256). SNMPv1 and SNMPv2c (community string) are also supported.
* **No trap/inform handling** — SNMP data is collected during discovery scans only (poll-based). Scanopy does not listen for SNMP traps or informs.
* **No custom MIB loading** — only a fixed set of standard MIBs are queried. See [SNMP reference](https://scanopy.net/docs/reference/snmp/#queried-mibs) for the full list.
## Topology
### Application Perspective
The Application perspective requires manual setup — you need to define application groups and assign services before it shows anything useful. See [Application Perspective](https://scanopy.net/docs/using-scanopy/topology/application) for the setup wizard walkthrough.
## Platform-Specific
### Docker on macOS/Windows
Docker Desktop on macOS and Windows doesn't support host networking — the container can't see your actual network. Install the daemon as a native service instead (`scanopy-daemon install`).
## Requesting Features
If a limitation affects your use case:
* [Open an issue](https://github.com/scanopy/scanopy/issues/new) describing your scenario
* Join the [Discord community](https://discord.gg/b7ffQr8AcZ) to discuss
# Server Configuration
Source: https://scanopy.net/docs/reference/server-configuration/
Configuration reference for self-hosted Scanopy server deployments.
## Configuration Methods
Every setting is an environment variable prefixed with `SCANOPY_`. That is the primary interface, and it works the same however you run the server. Some settings also accept a command-line flag; the flag wins when both are set.
Sources are layered, later ones overriding earlier:
1. Built-in defaults
2. A `.env` file in the server's working directory (or any parent directory)
3. `SCANOPY_*` environment variables
4. Command-line flags
**Docker Compose** — set them on the `server` service, or point it at a `.env` file:
```yaml
environment:
- SCANOPY_SERVER_PORT=60072
- SCANOPY_DATABASE_URL=postgresql://postgres:password@db:5432/scanopy
```
**systemd** (for [native installs](https://scanopy.net/docs/self-hosted-server/server-installation/#running-without-docker)) — keep them in an `EnvironmentFile` so secrets stay out of the unit file:
```ini
[Service]
WorkingDirectory=/opt/scanopy
EnvironmentFile=/etc/scanopy/server.env
ExecStart=/opt/scanopy/scanopy-server
```
```bash
# /etc/scanopy/server.env — chmod 600
SCANOPY_DATABASE_URL=postgresql://scanopy:password@localhost:5432/scanopy
SCANOPY_PUBLIC_URL=http://your-server:60072
```
Reload after editing: `sudo systemctl restart scanopy-server`.
**Command-line** (for binary builds):
```bash
./scanopy-server --server-port 60072 --database-url postgresql://...
```
Not every setting has a CLI flag — `SCANOPY_LICENSE_KEY` and `SCANOPY_WEB_EXTERNAL_PATH`, among others, are environment-only. The table below lists `N/A` where no flag exists.
## Parameter Reference
| Parameter | CLI Flag | Environment Variable | Default | Description |
| -------------------------- | ------------------------------ | ------------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Server Public URL** | `--public-url` | `SCANOPY_PUBLIC_URL` | `http://localhost:60072` | Public URL for webhooks, email links, etc |
| **Server Port** | `--server-port` | `SCANOPY_SERVER_PORT` | `60072` | Port for server to listen on |
| **Database URL** | `--database-url` | `SCANOPY_DATABASE_URL` | *Required* | PostgreSQL connection string |
| **Web External Path** | N/A | `SCANOPY_WEB_EXTERNAL_PATH` | *Embedded UI* | Serve the web UI from this directory instead of the copy compiled into the binary. Set to `/app/static` in the Docker image; unset elsewhere unless you are replacing the bundled UI |
| **Log Level** | `--log-level` | `SCANOPY_LOG_LEVEL` | `info` | Logging verbosity: `trace`, `debug`, `info`, `warn`, `error` |
| **Secure Cookies** | `--use-secure-session-cookies` | `SCANOPY_USE_SECURE_SESSION_COOKIES` | `false` | Enable HTTPS-only cookies |
| **Integrated Daemon URL** | `--integrated-daemon-url` | `SCANOPY_INTEGRATED_DAEMON_URL` | `http://172.17.0.1:60073` | URL to reach daemon in default docker compose |
| **Disable Registration** | `--disable-registration` | `SCANOPY_DISABLE_REGISTRATION` | `false` | Disable new user registration |
| **SMTP Username** | `--smtp-username` | `SCANOPY_SMTP_USERNAME` | - | SMTP username for email features |
| **SMTP Password** | `--smtp-password` | `SCANOPY_SMTP_PASSWORD` | - | SMTP password for email authentication |
| **SMTP Relay** | `--smtp-relay` | `SCANOPY_SMTP_RELAY` | - | SMTP server address (e.g., `smtp.gmail.com`) |
| **SMTP Email** | `--smtp-email` | `SCANOPY_SMTP_EMAIL` | - | Sender email address for outgoing emails |
| **SMTP Port** | `--smtp-port` | `SCANOPY_SMTP_PORT` | `465` | Port for the SMTP relay. Selects the encryption mode: `465` is implicit TLS, any other port uses STARTTLS |
| **Client IP Source** | `--client-ip-source` | `SCANOPY_CLIENT_IP_SOURCE` | - | Source of IP address from request headers for reverse proxy setups |
| **Metrics Token** | `--metrics-token` | `SCANOPY_METRICS_TOKEN` | - | Bearer token for Prometheus metrics endpoint authentication |
| **Prometheus Allowed IPs** | N/A | `SCANOPY_EXTERNAL_SERVICE_PROMETHEUS_ALLOWED_IPS` | - | Comma-separated IPs/CIDRs allowed to access metrics endpoint |
| **Snapshot Retention** | N/A | `SCANOPY_SNAPSHOT_RETENTION_DAYS_OVERRIDE` | `90` | Days to retain topology [snapshots](https://scanopy.net/docs/using-scanopy/topology/snapshots/) before automatic pruning |
| **License Key** | N/A | `SCANOPY_LICENSE_KEY` | - | Signed key that enables the [commercial edition](https://scanopy.net/docs/self-hosted-server/commercial-deployment/). Omit for the free community edition |
## Integrated Daemon URL
The integrated daemon runs in a separate container and needs to reach the server. The default assumes Docker's bridge network gateway is `172.17.0.1`.
**Check your bridge gateway**:
```bash
docker network inspect bridge | grep Gateway
```
**If different**, update in docker-compose.yml:
```yaml
environment:
- SCANOPY_INTEGRATED_DAEMON_URL=http://YOUR_GATEWAY_IP:60073
```
## SMTP Configuration
SMTP settings enable email-based features such as password reset.
**All SMTP parameters are optional.** If not configured, email features will be disabled. Relay, username, password and email must all be set together — with any of them missing, email stays disabled.
Every SMTP connection is encrypted, and there is no setting to turn encryption on or off. `SCANOPY_SMTP_PORT` selects which kind: the default of `465` is implicit TLS, and any other port uses STARTTLS, which is what submission ports such as 587 and 25 expect. STARTTLS is required rather than opportunistic — if the upgrade fails, the message is not sent.
Set `SCANOPY_SMTP_EMAIL` to the same mailbox as `SCANOPY_SMTP_USERNAME` unless that account has been granted permission to send as another address. Most providers reject a mismatch.
**Configuration**:
```yaml
environment:
- SCANOPY_SMTP_RELAY=smtp.gmail.com
- SCANOPY_SMTP_USERNAME=your-email@gmail.com
- SCANOPY_SMTP_PASSWORD=your-app-password
- SCANOPY_SMTP_EMAIL=scanopy@yourdomain.com
- SCANOPY_SMTP_PORT=587
```
### Microsoft 365
Microsoft 365 accepts authenticated SMTP only on port 587 with STARTTLS; it does not listen on 465, so the default port fails to connect.
```yaml
environment:
- SCANOPY_SMTP_RELAY=smtp.office365.com
- SCANOPY_SMTP_PORT=587
- SCANOPY_SMTP_USERNAME=scanopy@yourdomain.com
- SCANOPY_SMTP_PASSWORD=your-password
- SCANOPY_SMTP_EMAIL=scanopy@yourdomain.com
```
Microsoft 365 also requires tenant-side configuration that Scanopy cannot perform:
* **SMTP AUTH** must be enabled for the mailbox. Microsoft disables it by default for organizations created after January 2020.
* **Security defaults** must be off, and the account must not require multi-factor authentication. Microsoft documents both as incompatible with SMTP AUTH.
* The mailbox needs a Microsoft 365 license.
Microsoft is [retiring basic authentication for SMTP AUTH](https://techcommunity.microsoft.com/blog/exchange/exchange-online-to-retire-basic-auth-for-client-submission-smtp-auth/4114750). It is disabled by default for existing tenants from the end of December 2026, though an administrator can re-enable it, and it is unavailable to tenants created after that date. Scanopy authenticates with a username and password; OAuth is not supported.
See [Microsoft's SMTP AUTH documentation](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission) for the tenant-side steps.
When a message cannot be sent, the mail server's reply is recorded in the server log. It names the specific reason — a rejected password, a tenant policy, or a sender the account may not send as — which the failure alone does not tell you.
## UI Configuration
The UI automatically uses the hostname and port from your browser's address bar to reach the API.
**No configuration needed** for standard deployments where UI and API are on the same domain.
### Advanced: API on Different Domain
If your API server is on a different hostname than where the UI is served (uncommon):
Rebuild the Docker image with build arguments:
```bash
docker build \
--build-arg PUBLIC_SERVER_HOSTNAME=api.example.com \
--build-arg PUBLIC_SERVER_PORT=8080 \
-f backend/Dockerfile \
-t scanopy-server:custom \
.
```
Then use your custom image in docker-compose:
```yaml
scanopy-server:
image: scanopy-server:custom
# ... rest of config
```
## Session Security
### Secure Cookies
**Important**: Enable secure cookies when running Scanopy behind HTTPS.
```yaml
environment:
- SCANOPY_USE_SECURE_SESSION_COOKIES=true
```
**When to enable**:
* Behind a reverse proxy with TLS (Nginx, Traefik, Caddy)
* Using a domain with HTTPS
* Production deployments
**When to disable** (default):
* Internal networks without HTTPS
* Development environments
* Accessing via IP address without TLS
**Effect**:
* `true`: Cookies marked as Secure, only sent over HTTPS
* `false`: Cookies sent over HTTP and HTTPS
## Environment Files
For easier management, use `.env` files:
**Create `.env`**:
```bash
# Database
SCANOPY_DATABASE_URL=postgresql://postgres:password@db:5432/scanopy
# Server
SCANOPY_SERVER_PORT=60072
SCANOPY_SERVER_PUBLIC_URL=http://your-domain.com:60072
SCANOPY_LOG_LEVEL=info
SCANOPY_USE_SECURE_SESSION_COOKIES=false
# SMTP (optional - for password reset and notifications)
SCANOPY_SMTP_RELAY=smtp.gmail.com
SCANOPY_SMTP_USERNAME=your-email@gmail.com
SCANOPY_SMTP_PASSWORD=your-app-password
SCANOPY_SMTP_EMAIL=scanopy@yourdomain.com
SCANOPY_SMTP_PORT=587
# Daemon
SCANOPY_INTEGRATED_DAEMON_URL=http://172.17.0.1:60073
# Metrics (optional - for Prometheus scraping)
SCANOPY_METRICS_TOKEN=your-secure-metrics-token
SCANOPY_EXTERNAL_SERVICE_PROMETHEUS_ALLOWED_IPS=192.168.1.0/24
```
**Reference in docker-compose.yml**:
```yaml
services:
scanopy-server:
image: ghcr.io/scanopy/scanopy/server:latest
env_file:
- .env
# ... rest of config
```
**Running without Docker**: the server loads a `.env` file from its working directory (or a parent) automatically, so dropping one next to the binary works with no extra wiring. For a systemd service, prefer `EnvironmentFile=` — it doesn't depend on the working directory and keeps the file's permissions explicit.
# Service Detection
Source: https://scanopy.net/docs/reference/service-detection/
Scanopy automatically identifies 200+ services during network discovery. This page explains how detection works and what to do when a service isn't detected.
For the complete list of detectable services and their match patterns, see [scanopy.net/services](https://scanopy.net/services).
## Why Wasn't My Service Detected?
First check whether the service is in the detection list at [scanopy.net/services](https://scanopy.net/services). If it isn't, [request it](#requesting-new-service-detection). If it is, one of these usually explains it:
| Cause | What to do |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| **Non-standard port** — patterns are written for the default port, so Grafana on 8080 is never probed for Grafana's endpoint | Move it to the default port, or add the service by hand |
| **Service requires authentication** — a 401/403 before the response body is readable defeats endpoint matching | Nothing, for most services. Some (Plex) have patterns that work around it |
| **Container doesn't publish a port** — containers on internal bridge networks aren't reachable by scanning | Enable [container discovery](https://scanopy.net/docs/using-scanopy/discovery/#what-credentials-add), which reads the API directly |
| **Firewall blocking the daemon** | Allow the daemon to reach the service's port |
| **Raw socket port (9100-9107)** — skipped by default, because these ports treat any TCP data as print input and probing them ghost-prints on JetDirect printers | Enable **Probe Raw Socket Ports** in [scan settings](https://scanopy.net/docs/using-scanopy/discovery/scans/#scan-settings). Affects Prometheus Node Exporter on 9100 |
## How Detection Works
Detection happens in phases:
1. **Port scan** — Find open TCP and UDP ports
2. **Endpoint probing** — Send HTTP requests to known paths
3. **Pattern matching** — Compare results against service definitions
Each service has a **detection pattern** that specifies what to look for. Patterns can include:
| Check | What It Does |
| ------------------ | ------------------------------------------------------------- |
| **Port** | Is a specific port open? |
| **Endpoint** | Does the HTTP response body contain a string? |
| **Header** | Does an HTTP header contain a value? |
| **MAC Vendor** | Does the device's MAC address belong to a vendor? |
| **Gateway** | Is this IP in the daemon's routing table? |
| **Managed Device** | Does a network controller report this device as a given type? |
Patterns can be combined with **AllOf** (all must match) or **AnyOf** (any can match).
**Managed Device** is the one check that doesn't come from probing the device. When a daemon reads a [network controller's inventory](https://scanopy.net/docs/using-scanopy/discovery/#what-credentials-add), the controller states what each adopted device is, and that statement is matched like any other evidence — which is how **UniFi Switch** and **UniFi Gateway** are identified.
### Example: Pi-hole
Pi-hole's pattern requires:
* DNS port open (53/tcp OR 53/udp) **AND**
* HTTP response from `/admin` contains "pi-hole"
Both conditions must be true for Pi-hole to be detected.
### Example: Plex
Plex's pattern accepts either:
* HTTP response from port 32400 `/web/index.html` contains "Plex" **OR**
* HTTP response has `X-Plex-Protocol` header
Either condition is sufficient.
## Confidence Levels
Each detected service has a confidence level indicating match strength.
| Level | Meaning |
| ----------- | ---------------------------------------------------- |
| **Certain** | System services only (Scanopy Daemon, Docker Daemon) |
| **High** | Strong match — endpoint content or header matched |
| **Medium** | Moderate match — MAC vendor or unique port |
| **Low** | Weak match — common port, could be multiple services |
| **N/A** | Generic service — definitional match (port 53 = DNS) |
### Generic Services
Some services are **generic** — they're defined by their port alone:
* Port 53 → DNS Server
* Port 5432 → PostgreSQL
* Port 22 → SSH
These show "N/A" confidence because there's no uncertainty. Port 53 *is* DNS by definition. If you're running a specific DNS server (Pi-hole, AdGuard), it will be detected separately with its own pattern.
## Improving Detection
**Use default ports** — Detection patterns are written for standard ports. Check [scanopy.net/services](https://scanopy.net/services) for which port each service expects.
**Use Docker scanning for containers** — Docker container discovery inspects the Docker API directly, so it works for containers on internal networks without port exposure. It's more reliable than network scanning for containerized services.
## Requesting New Service Detection
If a service isn't detected:
1. **Check existing definitions** at [scanopy.net/services](https://scanopy.net/services)
2. **Report a detection issue** if Scanopy should have detected it but didn't:
[Service detection issue](https://github.com/scanopy/scanopy/issues/new?template=service-detection-issue.md)
3. **Request a new service** if it's not in the list:
[Missing service request](https://github.com/scanopy/scanopy/issues/new?template=missing-service-detection.md)
When requesting, include:
* Service name and what it does
* Default port(s)
* Any unique HTTP endpoints or headers
* How you currently identify it
# SNMP
Source: https://scanopy.net/docs/reference/snmp/
Technical reference for Scanopy's SNMP implementation. For setup instructions, see the [SNMP discovery guide](https://scanopy.net/docs/guides/integrations/snmp/). For how credential assignment and resolution works, see [Credentials](https://scanopy.net/docs/using-scanopy/credentials/).
## Protocol support
| Property | Value |
| ------------- | ----------------------------------------------------------------------- |
| **SNMPv1** | Supported (community string) |
| **SNMPv2c** | Supported (community string) |
| **SNMPv3** | Supported — AuthPriv only (SHA-1/SHA-256 auth, AES-128/AES-256 privacy) |
| **Transport** | UDP |
| **Port** | 161, 1161 (fallback) |
## Queried MIBs
| MIB | RFC | OID prefix | Collected fields |
| ----------------------------- | ------------------ | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **System MIB** | RFC 3418 | `1.3.6.1.2.1.1` | sysDescr, sysObjectID, sysUpTime, sysContact, sysName, sysLocation |
| **IF-MIB** | RFC 2863 | `1.3.6.1.2.1.2`, `1.3.6.1.2.1.31.1.1` | ifDescr, ifType, ifSpeed, ifPhysAddress, ifAdminStatus, ifOperStatus, ifName, ifAlias, ifHighSpeed, ifMtu |
| **IP-MIB** | RFC 4293 | `1.3.6.1.2.1.4` | ipAdEntAddr, ipAdEntIfIndex, ipAdEntNetMask, ipNetToMediaPhysAddress, ipNetToMediaNetAddress |
| **LLDP-MIB** | IEEE 802.1AB | `1.0.8802.1.1.2` | lldpRemSysName, lldpRemPortId, lldpRemChassisId, lldpRemSysDesc |
| **CDP-MIB** | Cisco proprietary | `1.3.6.1.4.1.9.9.23` | cdpCacheDeviceId, cdpCachePlatform, cdpCacheDevicePort, cdpCacheAddress |
| **ENTITY-MIB** | RFC 6933 | `1.3.6.1.2.1.47` | entPhysicalMfgName, entPhysicalModelName, entPhysicalSerialNum |
| **BRIDGE-MIB / Q-BRIDGE-MIB** | RFC 4188, RFC 4363 | `1.3.6.1.2.1.17` | dot1dTpFdbAddress (MAC learning), dot1qVlanStaticName (VLAN names), dot1qPvid (per-port native VLAN), dot1qVlanCurrentEgressPorts, dot1qVlanCurrentUntaggedPorts (VLAN membership) |
## Data collected
A successful SNMP run against a host populates the following entities in Scanopy:
| Entity | Fields populated from SNMP |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Host** | hostname (from sysName when DNS lacks one), sysDescr, sysObjectID, sysLocation, sysContact, sysName, manufacturer, model, serial number, chassis ID |
| **Interface** | ifIndex, name / alias / description, type, speed, MTU, MAC, admin and oper status, native VLAN, tagged VLAN list, learned MACs, LLDP/CDP neighbor |
| **Subnet** | CIDRs inferred from ipAddrTable entries |
| **Host (remote)** | New hosts created from ARP entries on SNMP-discovered subnets |
## Timeouts and limits
| Parameter | Default | Description |
| ----------------------- | ------- | --------------------------------------------------------------------------------------------- |
| **Probe timeout** | 2s | Liveness probe per credential, used to pick a working credential before collection |
| **Request timeout** | 5s | Timeout for a single SNMP request |
| **Session timeout** | 5s | Timeout for establishing the UDP session |
| **Query timeout** | 30s | Ceiling on any single query, including a full walk of a MIB table |
| **Max entries** | 10,000 | Maximum rows returned from a single table walk |
| **Integration timeout** | 5m | Wall-clock budget for a full SNMP integration run against a single host (probe + all queries) |
Candidate credentials are probed concurrently, so a host with several assigned credentials doesn't pay for each one in sequence.
Every query is individually bounded, so a device that stops responding mid-collection costs 30 seconds rather than the whole 5-minute integration budget.
## Credential behavior
Which credential a host is queried with follows the standard [resolution order](https://scanopy.net/docs/using-scanopy/credentials/#credential-resolution), with one SNMP-specific addition: `public` is tried as a last resort, so a device left on the default needs no credential.
If every credential fails on both UDP 161 and 1161, the host is discovered without SNMP data. A query that fails inside an otherwise working session is logged and skipped rather than aborting the rest.
Partial results are kept: interfaces are saved as soon as the interface table is read, before the neighbor, forwarding-table and VLAN queries run.
# Setting Up Daemons
Source: https://scanopy.net/docs/setting-up-daemons/
# Managing Daemons
Source: https://scanopy.net/docs/setting-up-daemons/managing-daemons/
## Requirements
* **Linux**: Docker with host networking OR standalone binary (installable as a systemd service)
* **macOS**: Standalone binary — Docker Desktop doesn't allow access to host interfaces. Installs as a launchd service.
* **Windows**: Standalone binary — Docker Desktop doesn't allow access to host interfaces. Installs as a Windows service (native SCM).
* **FreeBSD**: Standalone binary, installable as an rc.d service.
On Linux, macOS, Windows, and FreeBSD the daemon installs as a native background service — see [Running as a Service](#running-as-a-service).
## Creating a Daemon
Go to **Discover > Scan > Daemons** and click **Create Daemon**. The wizard walks you through:
1. **Setup** — network, polling mode, and daemon name
2. **Integrations** — optionally add [credentials](https://scanopy.net/docs/using-scanopy/credentials/#attaching-a-credential), targeting all hosts or specific ones. They're [auto-assigned](https://scanopy.net/docs/using-scanopy/credentials/#auto-assignment) to discovered hosts after the first scan.
3. **Install** — pick your platform and install method; the wizard builds the matching artifact for you
Finishing the wizard [provisions](https://scanopy.net/docs/reference/daemon-identity/) the daemon: the server creates its record and mints an API key bound to it. You don't create a key yourself.
Choose an install method on the final step:
| Method | What you get |
| --------------------------- | --------------------------------------------------------------------------------------------------- |
| **Linux / macOS / FreeBSD** | A command that fetches the daemon binary and installs it as a native service |
| **Windows** | A PowerShell command that downloads the binary and installs it as a Windows service |
| **Docker** | A ready-to-run `docker-compose.yml` plus the `SCANOPY_*` environment variables it's configured with |
The generated artifact carries the daemon's API key, and the key is shown only here. Copy it before leaving the wizard — you can't retrieve it afterwards. If you lose it before installing, you can mint a replacement from the daemon's management modal; once the daemon has connected, re-keying it is blocked, because a fresh key would cut off the running daemon.
Any [advanced settings](https://scanopy.net/docs/reference/daemon-configuration/) you set in the wizard are baked into the artifact, so the installed daemon comes up already configured.
For help choosing a polling mode and planning where to place daemons, see [Planning Daemon Deployment](https://scanopy.net/docs/setting-up-daemons/planning-daemon-deployment/).
## Running as a Service
**`scanopy-daemon install`** places the binary, writes the daemon's `config.json`, and registers a native background service that starts immediately and on every reboot — systemd on Linux, launchd on macOS, the Service Control Manager on Windows, rc.d on FreeBSD.
The command on the wizard's **Install** step already does this. Copy it from there rather than assembling one: the wizard and the management modal build it against your server's URL and the daemon's key.
Everything except the connection flags is read from `config.json`, so no secrets appear in the service definition or process arguments. Re-running `install` is idempotent. A non-elevated invocation prints the exact `sudo`/administrator command to re-run, and on success it prints the config and log paths it settled on.
Verify the service and view its logs:
* **Linux**: `systemctl status scanopy-daemon` · logs via `journalctl -u scanopy-daemon -f`
* **macOS**: `sudo launchctl print system/com.scanopy.daemon` · logs at `/var/log/scanopy/scanopy-daemon.out.log` and `~/Library/Logs/scanopy/scanopy-daemon.log`
* **Windows**: `Get-Service scanopy-daemon` · logs at `%ProgramData%\scanopy\scanopy-daemon.log`
* **FreeBSD**: `service scanopy-daemon status`
To stop and remove the service, use `sudo scanopy-daemon uninstall` — see [Removing a Daemon](#removing-a-daemon).
Prefer to run the daemon in the foreground (e.g. for a quick test)? Run `scanopy-daemon` directly with the same flags — it runs attached to your terminal and stops when the terminal closes.
Running several daemons on one host? Each `install` takes its own numbered **slot** (`scanopy-daemon`, `scanopy-daemon-2`, …) automatically, so installs don't overwrite each other. Run `scanopy-daemon list` to see what's installed. See [Scanning Isolated Networks from One Host](https://scanopy.net/docs/guides/networking/multiple-daemons/) for the full workflow.
### Manually installed or older daemons
Daemons that predate the `scanopy-daemon install` subcommand, or that you set up by hand, are managed with the platform's native service tools directly. Use these instructions if `scanopy-daemon install` isn't available in your daemon version. (For current versions, prefer `install`/`uninstall` above.)
**Linux (systemd, manual)** — download the [systemd unit](https://github.com/scanopy/scanopy/blob/main/scanopy-daemon.service), set your flags in `ExecStart` (or rely on `config.json`), then enable it:
```bash
sudo curl -o /etc/systemd/system/scanopy-daemon.service \
https://raw.githubusercontent.com/scanopy/scanopy/main/scanopy-daemon.service
sudo systemctl daemon-reload
sudo systemctl enable --now scanopy-daemon
```
**macOS (launchd, manual)** — create a launch daemon plist and load it:
```bash
sudo tee /Library/LaunchDaemons/com.scanopy.daemon.plist > /dev/null <
Label
com.scanopy.daemon
ProgramArguments
/usr/local/bin/scanopy-daemon
RunAtLoad
KeepAlive
StandardOutPath
/var/log/scanopy/scanopy-daemon.out.log
StandardErrorPath
/var/log/scanopy/scanopy-daemon.out.log
EOF
sudo launchctl bootstrap system /Library/LaunchDaemons/com.scanopy.daemon.plist
```
**Windows (NSSM, manual)** — older setups used [NSSM](https://nssm.cc/) to run the executable as a service:
```powershell
nssm install ScanopyDaemon "C:\Path\To\scanopy-daemon.exe"
nssm set ScanopyDaemon AppStdout "C:\ProgramData\scanopy\daemon.log"
nssm set ScanopyDaemon AppStderr "C:\ProgramData\scanopy\daemon.log"
nssm start ScanopyDaemon
```
## Integrated Daemon (Self-Hosted)
Self-hosted Docker Compose deployments include an **integrated daemon** managed as part of the stack: server setup provisions it and its API key, and it upgrades with the server. Nothing to install or configure — see [Upgrading a Self-Hosted Server](https://scanopy.net/docs/self-hosted-server/upgrading/).
## Upgrading
When an update is available, the daemon card in **Discover > Scan > Daemons** shows an **Update** button, with the steps for your platform: download the latest binary, then restart the service. Configuration is preserved.
A **Deprecated** daemon has a scheduled sunset date, shown in the upgrade dialog with a countdown. Past that date it becomes **Unsupported** and is rejected until updated — see [Daemon Status](https://scanopy.net/docs/reference/daemon-status).
For upgrading self-hosted server and integrated daemon together, see [Upgrading](https://scanopy.net/docs/self-hosted-server/upgrading).
## Updating Daemon Properties
Click the edit icon on a daemon card in **Discover > Scan > Daemons** to open its management modal, which has two tabs: **Details** for the daemon record, and **API Key** for its key.
The **Details** tab edits the maintainer, tags, and — for ServerPoll daemons — the URL the server dials. Network, polling mode, and name are read-only: they're set at [provisioning](https://scanopy.net/docs/reference/daemon-identity/) and define what the daemon is. A DaemonPoll daemon has no URL to edit, since the server never dials it.
Daemon-local settings — interfaces, ports, log level, scan tuning — live in the [daemon config file](https://scanopy.net/docs/reference/daemon-configuration/) on the host, not on the server record. Change them by [reconfiguring the daemon](#reconfiguring-a-daemon) from the modal, or by editing `config.json` and restarting the service:
* **Linux**: `sudo systemctl restart scanopy-daemon`
* **macOS**: `sudo launchctl kickstart -k system/com.scanopy.daemon`
* **Windows**: `sc.exe stop scanopy-daemon` then `sc.exe start scanopy-daemon`
* **FreeBSD**: `sudo service scanopy-daemon restart`
A foreground daemon just needs the process restarted.
Each daemon also reports which subnets it has network interfaces on, which is where its default scan targets come from. That list refreshes on its next discovery, so re-run one after changing the host's network configuration.
## Reconfiguring a Daemon
Changing a daemon's advanced settings in the management modal produces a **reconfigure** artifact — the way settings you change in the app reach a daemon that's already installed.
A reconfigure differs from an install: it carries no API key and doesn't re-fetch the binary. It only re-asserts configuration, layering it over the `config.json` already on the host. For a Docker daemon you get just the environment variables that changed, to swap into your existing Compose file rather than replacing it — your own Compose customizations stay intact.
Run the artifact on the daemon host, then restart the service.
## Binding a Key to a Legacy Daemon
Daemons set up before 1:1 provisioning authenticate with a key shared across every daemon on their network. They keep working indefinitely, but a shared key can't be rotated or revoked for one daemon without affecting the rest. Binding gives the daemon a key of its own, keeping its host, discovery jobs, and history.
Open the daemon's management modal, go to the **API Key** tab, and click **Bind a Key**. What happens next depends on the polling mode:
* **DaemonPoll** — the daemon keeps running on its shared key. You get a command to run on the daemon host to switch it over, and can do that whenever it suits you.
* **ServerPoll** — a hard cutover. The server starts using the new key to connect immediately, so the daemon stops responding until you reconfigure it. Only do this when you can get to the host right away.
Scanopy asks you to confirm before a ServerPoll cutover. See [Daemon Identity](https://scanopy.net/docs/reference/daemon-identity/) for how the two key models coexist.
## Removing a Daemon
To fully remove a daemon, delete it from the UI and uninstall it from the host.
**Delete from the UI**: Go to **Discover > Scan > Daemons** and click the delete icon on the daemon card.
Deleting or uninstalling a daemon does not delete discovered data. Hosts, services, subnets, and topology remain on the server until you explicitly delete them.
**Uninstall from the host** — for a daemon installed with `scanopy-daemon install`, run:
```bash
sudo scanopy-daemon uninstall
```
This stops and deregisters the service and deletes its `config.json`. Add `--purge` to also delete the binary and log files. On Windows, run it from an elevated PowerShell.
On a host running several daemons, target one with `--name ` — its name, slot, or service id, all shown by `scanopy-daemon list` — or remove all of them with `--all`.
`uninstall` reports what it did with every item it touched, and is idempotent: uninstalling something that was never installed succeeds and says so.
Docker:
```bash
docker stop scanopy-daemon
docker rm scanopy-daemon
```
**Manually installed or older daemons** — if the daemon was set up by hand or predates the `uninstall` subcommand, remove the service and files manually.
Linux / macOS:
```bash
# systemd (Linux)
sudo systemctl stop scanopy-daemon && sudo systemctl disable scanopy-daemon
sudo rm /etc/systemd/system/scanopy-daemon.service && sudo systemctl daemon-reload
# launchd (macOS)
sudo launchctl bootout system /Library/LaunchDaemons/com.scanopy.daemon.plist
# (older setups: launchctl unload ~/Library/LaunchAgents/com.scanopy.daemon.plist)
# Remove binary and configuration
sudo rm /usr/local/bin/scanopy-daemon
rm -rf ~/.config/scanopy/ # Linux
rm -rf ~/Library/Application\ Support/com.scanopy.daemon/ # macOS
```
Windows (NSSM or manual):
1. Stop and remove the service: `nssm stop ScanopyDaemon && nssm remove ScanopyDaemon confirm` (or stop the process)
2. Delete the executable
3. Remove configuration from `%APPDATA%\scanopy\daemon\`
### Can I migrate a daemon?
No. Daemons discover host-specific data (network interfaces, Docker containers, local services) tied to the machine they run on. Moving a daemon's identity to a different host would produce inaccurate data.
If you're replacing a host, create a new daemon on the new machine and delete the old one.
# Planning Daemon Deployment
Source: https://scanopy.net/docs/setting-up-daemons/planning-daemon-deployment/
Daemons are lightweight agents that scan your network and report findings to the server. Most networks need at least one, and many need several. This guide helps you decide how many daemons to deploy and where to place them based on your network topology.
## Layer 2 vs Layer 3 discovery
This tradeoff drives every decision on this page. When a daemon has a **network interface** on a subnet, it gets Layer 2 access. When it can only route to a subnet, it's limited to Layer 3.
| | Layer 2 (interfaced) | Layer 3 (routed) |
| ---------------------------------- | ------------------------------------------ | ------------------------------------ |
| **How it works** | ARP scanning on the local segment | TCP port scanning through a gateway |
| **Finds hosts without open ports** | Yes | No |
| **Collects MAC addresses** | Yes | No |
| **Discovery speed** | Fast — ARP is lightweight | Slower — must probe individual ports |
| **Requires** | Daemon host has an interface on the subnet | Daemon host can route to the subnet |
**Layer 2 is always preferred.** It's faster, finds more devices, and gives you complete visibility. Layer 3 is a fallback for subnets where you can't place a daemon — it will miss hosts that don't have open ports and won't collect MAC addresses. When planning your deployment, aim for Layer 2 coverage on every segment you care about.
See [Layer 2 vs Layer 3](https://scanopy.net/docs/reference/limitations/#layer-2-vs-layer-3) for more detail.
## Deployment strategies
### Single daemon with routing
**Start with one daemon and extend its reach before adding more.** A single daemon automatically scans every subnet its host has an interface on (Layer 2). It can also scan remote subnets it can route to, but those fall back to Layer 3 — a good way to get initial visibility while you plan where to place additional daemons for full coverage.
(Cloud or Self-Hosted)"]
server --> host
subgraph host["Single Host"]
d1["Daemon"]
end
d1 -- "L2 (interfaced)" --> lan["LAN
192.168.1.0/24"]
d1 -. "L3 (routed)" .-> remote1["Remote Subnet
10.0.1.0/24"]
d1 -. "L3 (routed)" .-> remote2["Remote Subnet
10.0.2.0/24"]
`}
/>
**Extending what a single daemon scans:**
* **Remote subnets**: The daemon can scan subnets it can route to but doesn't have an interface on (Layer 3 only). See [Managing Scan Targets](https://scanopy.net/docs/guides/networking/scanning-remote-subnets/).
* **VPN networks**: If the daemon's host is connected to a VPN, it can scan that network with full Layer 2 access. See [Scanning VPN Networks](https://scanopy.net/docs/guides/networking/scanning-vpn-networks/).
**Best for:** Getting started quickly. For subnets the daemon can only route to, plan to deploy additional daemons for full Layer 2 coverage when you can.
### Daemon per network segment
**Deploy a daemon on each VLAN or network segment for full Layer 2 access everywhere.**
(Cloud or Self-Hosted)"]
server --> vlan1
server --> vlan2
server --> vlan3
subgraph vlan1["VLAN 1 - Production"]
d1["192.168.1.0/24
Daemon 1"]
end
subgraph vlan2["VLAN 2 - IoT"]
d2["192.168.2.0/24
Daemon 2"]
end
subgraph vlan3["VLAN 3 - Guest"]
d3["192.168.3.0/24
Daemon 3"]
end
`}
/>
Each daemon runs on a host within its segment and reports full Layer 2 data. Install each one through **Discover > Daemons** — see [Managing Daemons](https://scanopy.net/docs/setting-up-daemons/managing-daemons/) for setup instructions.
When a device appears on multiple VLANs, it may show up as separate hosts. Use [host consolidation](https://scanopy.net/docs/using-scanopy/network-data/#consolidating-duplicate-hosts) to merge them.
**Best for:** Any multi-segment network. This is the ideal deployment — every segment gets full discovery. Required for isolated networks with no routing between segments.
### Multiple daemons on one host
**Run several daemon instances on a single machine that bridges multiple isolated networks.** This is common with hypervisors, routers, or servers that have interfaces on VLANs that don't route to each other.
(Cloud or Self-Hosted)"]
server --> host
subgraph host["Multi-NIC Host (Hypervisor / Router)"]
d1["Daemon 1
--interfaces eth0"]
d2["Daemon 2
--interfaces eth1"]
end
d1 -- "L2" --> vlan1["VLAN 1 - Production
192.168.1.0/24"]
d2 -- "L2" --> vlan2["VLAN 2 - IoT
192.168.2.0/24"]
`}
/>
Restrict each instance to specific interfaces so they don't overlap. Each `install` takes its own slot automatically, so you don't pass any per-instance identity — and since every daemon binds an HTTP server, the installer picks a non-conflicting port for each after the first:
```bash
sudo scanopy-daemon install --interfaces eth0
sudo scanopy-daemon install --interfaces eth1
```
Set `--bind-address` only to pin a daemon's listener to one interface, and `--daemon-port` only when you need a specific port (for a ServerPoll daemon it's already in the install command). See [Ports and bind addresses](https://scanopy.net/docs/guides/networking/multiple-daemons/#ports-and-bind-addresses) for details.
Create a separate daemon entry in **Discover > Daemons** for each instance. Each is provisioned with its own API key and can target the same or different Scanopy networks.
This gives you Layer 2 discovery on each segment without needing a separate physical host per VLAN. See [Scanning Isolated Networks from One Host](https://scanopy.net/docs/guides/networking/multiple-daemons/) for the full setup guide, including how slots work, systemd templates, and Docker configuration.
**Best for:** Hypervisors, routers, or multi-NIC servers bridging isolated VLANs where deploying separate hosts isn't practical.
### Hybrid
Use a central daemon for routable subnets and add dedicated daemons only where you need Layer 2 access or where networks are truly isolated. Most real-world deployments end up here — one daemon covers the majority of the network, with one or two more for specific segments.
(Cloud or Self-Hosted)"]
server --> d1
server --> d2
subgraph main["Main Network"]
d1["Daemon 1
(central)"]
end
d1 -- "L2" --> lan["LAN
192.168.1.0/24"]
d1 -. "L3" .-> routed["Routed Subnet
10.0.1.0/24"]
subgraph isolated["Isolated Segment"]
d2["Daemon 2
(dedicated)"]
end
d2 -- "L2" --> iot["IoT VLAN
172.16.0.0/24"]
`}
/>
## Choosing a polling mode
Each daemon uses a polling mode that determines which side initiates the connection.
| | DaemonPoll (default) | ServerPoll |
| ------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| **Connection direction** | Daemon → Server | Server → Daemon |
| **Firewall requirements** | Daemon needs outbound access to server | Server needs inbound access to daemon (port 60073) |
| **Setup** | Create the daemon in the UI, then run its install artifact on the host | Same, plus a reachable URL for the server to dial |
| **Best for** | Most deployments — works behind NAT and firewalls | DMZ or restricted environments where the daemon cannot make outbound connections |
**Use DaemonPoll unless your daemon cannot reach the server.** It's simpler to set up and requires no inbound firewall rules on the daemon host.
## Network requirements
### Firewall rules for DaemonPoll mode (default)
Daemon initiates all connections. Minimal firewall changes needed.
| Source | Destination | Port | Protocol | Purpose |
| ------ | ------------ | ----- | -------- | ------------------- |
| Daemon | Server | 60072 | TCP | Server API requests |
| Daemon | Local subnet | \* | TCP | Network scanning |
### Firewall rules for ServerPoll mode
Server initiates scan requests. Daemon must be reachable.
| Source | Destination | Port | Protocol | Purpose |
| ------ | ------------ | ----- | -------- | ------------------- |
| Server | Daemon | 60073 | TCP | Daemon API requests |
| Daemon | Local subnet | \* | TCP | Network scanning |
# Troubleshooting Setup
Source: https://scanopy.net/docs/setting-up-daemons/troubleshooting-setup/
Issues getting the daemon connected and running. For problems that occur during scans, see [Troubleshooting Scans](https://scanopy.net/docs/setting-up-daemons/troubleshooting-scans/).
## Checking Daemon Logs
**Symptoms**: Daemon appears to start but nothing happens, scans don't run, or you need to share logs with support
First, enable debug logging for more detail:
```bash
scanopy-daemon --log-level debug
```
For Docker Compose deployments, set the `SCANOPY_LOG_LEVEL` environment variable on the daemon service instead:
```yaml
services:
scanopy-daemon:
environment:
- SCANOPY_LOG_LEVEL=debug
```
Then recreate the container to apply the change:
```bash
docker compose up -d scanopy-daemon
```
Then check logs for your platform:
* **Linux (systemd)**: `journalctl -u scanopy-daemon -f`
* **macOS (launchd)**: `tail -f /var/log/scanopy/scanopy-daemon.out.log` (service output) or `~/Library/Logs/scanopy/scanopy-daemon.log`
* **Windows (service)**: `Get-Content "$env:ProgramData\scanopy\scanopy-daemon.log" -Wait`
* **FreeBSD (rc.d)**: `tail -f /var/log/scanopy/scanopy-daemon.log`
* **Docker**: `docker logs -f scanopy-daemon`
* **Foreground** (`scanopy-daemon` run directly): logs appear in your terminal
To capture logs to a file for sharing with support:
```bash
# Linux / macOS
./scanopy-daemon --log-level debug 2>&1 | tee ~/scanopy-daemon.log
```
```powershell
# Windows (PowerShell)
.\scanopy-daemon.exe --log-level debug 2>&1 | Tee-Object -FilePath "$env:USERPROFILE\scanopy-daemon.log"
```
For full details on log levels and output destinations, see [Logging](https://scanopy.net/docs/reference/daemon-configuration/#logging).
## Daemon Not Connecting to Server
**Symptoms**: Daemon shows as offline in the UI, or logs show connection errors
**Diagnosis**: Test connectivity to the server from the daemon host:
```bash
curl https://your-server-url/api/health
```
```powershell
# Windows (PowerShell)
Test-NetConnection -ComputerName your-server-hostname -Port 443
```
**Solutions**:
1. **Verify server URL**: Ensure `SCANOPY_SERVER_URL` is correct and reachable from the daemon host
2. **Check the API key**: Confirm the daemon's `config.json` holds the key from its install artifact. A daemon rejected with `daemon_not_provisioned` was installed by hand against a record that doesn't exist — create it in the UI and use the artifact that produces. See [Daemon Identity](https://scanopy.net/docs/reference/daemon-identity/).
3. **Firewall rules**: Ensure outbound HTTPS (port 443) is allowed from the daemon host. On Windows, verify the daemon executable is allowed through **Windows Defender Firewall > Allow an app through firewall**.
## Permission Denied Errors (Linux)
**Symptoms**: "Permission denied" when accessing Docker socket
**Solution**: Add user to docker group:
```bash
sudo usermod -aG docker $USER
newgrp docker
```
Log out and back in for changes to take effect.
If you're using a Docker socket proxy and getting permission errors, see [Securing Docker Discovery with a Socket Proxy — Troubleshooting](https://scanopy.net/docs/guides/integrations/docker/#troubleshooting).
## Daemon Stops When Terminal Closes
**Symptoms**: Daemon runs in foreground and stops when SSH session or terminal window ends
**Solution**: Install the daemon as a background service with `sudo scanopy-daemon install …` so it runs independently of your terminal session and survives reboots. See [Running as a Service](https://scanopy.net/docs/setting-up-daemons/managing-daemons/#running-as-a-service).
## Podman Pulls Wrong Daemon Version (amd64)
**Symptoms**: After pulling the daemon image by tag with Podman, the container runs an unexpected version or architecture
**Cause**: Docker buildx provenance attestations add `unknown/unknown` entries to the OCI manifest index. Podman can select these instead of the correct `linux/amd64` image when resolving a multi-platform tag.
**Solution**: Specify the platform explicitly when pulling:
```bash
podman pull --platform linux/amd64 ghcr.io/scanopy/scanopy/daemon:v0.15.2
```
For ARM hosts, use `--platform linux/arm64` instead.
This only affects the daemon image. The server image uses explicit manifest creation and is not
affected.
## Getting Help
If your issue isn't covered here:
* **Discord**: Join our [Discord community](https://discord.gg/b7ffQr8AcZ)
* **GitHub Issues**: [Open an issue](https://github.com/scanopy/scanopy/issues/new)
# Credentials
Source: https://scanopy.net/docs/using-scanopy/credentials/
Credentials provide daemons with the authentication details they need to gather data during discovery. Without credentials, discovery is limited to port scanning and DNS lookups. With credentials configured, a daemon can also query devices for their details, interfaces and neighbors, connect to container APIs, or read a network controller's inventory.
## Credential Types
Scanopy supports these credential types, grouped by the integration they enable. Each type can be pointed at one or more **targets** — see [Where a credential applies](#where-a-credential-applies).
Each integration's guide covers what's specific to it — what the other side needs configured, which transport to choose, and how to verify it: .
## Creating a credential
Creating a credential works the same way for every integration:
1. Go to **Assets > Credentials**
2. Click **Create Credential**
3. Select the credential type — the transport, not just the integration (e.g. **SNMP v2c**, **Docker Proxy**, **UniFi API Key**)
4. Enter a name that will make sense later ("Core switches", "HQ controller")
5. Fill in the type's fields — each integration's guide lists them
6. Click **Save**
Secrets — community strings, passwords, API keys, private keys — are not shown again in the UI or in API responses once saved.
A credential does nothing until it's pointed at something. That's the next step.
## Where a credential applies
Every credential is pointed at one or more **targets**, which determine the hosts the daemon tries it on:
| Target | Meaning |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| **Network** | Every host on a network. The daemon tries the credential on each host it scans there — the **Target All Hosts** option in the wizard. |
| **Daemon host** | The daemon's own machine (`127.0.0.1`). Used for local Docker and Podman sockets. |
| **Remote hosts** | Specific hosts you name by IP address. |
Not every type supports every target — the **Can be targeted at** column above shows which. A protocol that any device can speak reaches a whole **Network**; a local socket reaches only its **daemon host**; a proxy or controller names the one host its endpoint lives on. An unsupported assignment is rejected on save, and changing a credential's type drops assignments the new type doesn't allow.
## Attaching a credential
Where you attach a credential depends only on whether the host exists yet:
| Attach it | When |
| ----------------------------- | -------------------------------------------------------------------------------------- |
| **While setting up a daemon** | Before anything has been scanned |
| **From a discovery** | An existing daemon starts covering a new segment |
| **On a host** | The host has been discovered, and needs something different from its network's default |
The first two name a target by IP, since the host isn't in Scanopy yet — see [auto-assignment](#auto-assignment). An assignment made on a host can be narrowed to specific interfaces, which matters when a credential is reachable only one way in.
## Credential Resolution
When the daemon scans a host, it resolves credentials in this priority order:
1. **Host-level assignment** — credentials assigned directly to the host (or its specific interface)
2. **Network default** — credentials targeted at the host's whole network
3. **Fallback** — for SNMP, the community string `public` is tried as a last resort
All available credentials at each level are tried — not just the first match. If every credential fails (timeout or authentication error), the host is scanned without that credential type's data.
## Auto-Assignment
Naming a target by IP covers the gap between knowing a device's credentials and Scanopy having discovered it. The next scan tries the credential at that address; if it works, the credential is assigned to whatever host was found there. Until then it shows as unassigned — expected, not a failure.
The target is used once, and cleared when the scan ends whether or not it matched, so nothing is re-probed forever. A target covering a whole **Network** moves into that network's assignment rather than being dropped.
If auto-assignment isn't working, see [Troubleshooting credentials](https://scanopy.net/docs/setting-up-daemons/troubleshooting-scans/credentials/).
# Dependencies
Source: https://scanopy.net/docs/using-scanopy/dependencies/
Dependencies are connections you create between services to capture relationships that network discovery can't infer on its own — like the chain a request travels through your stack, or the set of services that all rely on a shared database.
## Dependency types
**Request Path** — An ordered chain showing how requests flow through services:
```
Internet → Reverse Proxy → App Server → Database
```
Use this for request routing, data pipelines, and backup chains.
**Hub and Spoke** — A central service with multiple dependents:
```
Web App ─┐
Workers ──→ Database
Admin ───┘
```
Use this when several services connect to a shared resource like a database, DNS server, or message queue.
## Where dependencies appear
Dependencies are the primary connections in the **Application** perspective. They can also appear as overlays in the **L3 Logical** perspective when configured with port-level detail. They aren't shown in the **L2 Physical** or **Workloads** perspectives.
## Creating dependencies
Select multiple elements in the topology and create a dependency between them from the selection. You can also manage existing dependencies under **Assets > Dependencies**.
# Using Scanopy
Source: https://scanopy.net/docs/using-scanopy/
# Hosts & Subnets
Source: https://scanopy.net/docs/using-scanopy/network-data/
## Hosts
Hosts represent devices on your network. They are automatically discovered during network scans.
### Consolidating Duplicate Hosts
When a device appears on multiple VLANs or through different discovery methods, it may be discovered as separate hosts. Use consolidation to merge them:
1. Open the host you want to keep (primary)
2. Click **Consolidate**
3. Select the duplicate host(s) to merge
4. Confirm
The primary host gains all interfaces, services, and properties from the merged hosts. Use this to unify a server that has interfaces on multiple VLANs into a single logical host.
#### Restrictions
* Cannot consolidate a host with itself.
* Cannot consolidate a host that has a daemon - consolidate into it instead.
### Unclaimed Open Ports
Ports discovered without a matching service definition are assigned to an "Unclaimed Open Ports" placeholder. To reassign them:
1. Open the host and go to the **Services** tab
2. Select ports from "Unclaimed Open Ports"
3. Click **Transfer Ports** on the service you want to assign them to
Useful when Scanopy detects an open port but can't identify the service running on it.
### Virtualization Relationships
Scanopy tracks which VMs and containers run on which hosts:
* **Proxmox**: Links VMs and LXC containers to their Proxmox host
* **vCenter / ESXi**: Links virtual machines to their VMware vCenter or ESXi hypervisor
* **Docker**: Links containers to their Docker host
* **Podman**: Links containers to their Podman host
These relationships appear in the topology and host details, showing your virtualization hierarchy. The [Workloads perspective](https://scanopy.net/docs/using-scanopy/topology/workloads) provides a dedicated view of this hierarchy.
### Service Detection
Scanopy automatically detects 200+ services. See [Service Detection](https://scanopy.net/docs/reference/service-detection/) for how detection works, confidence levels, and what to do when a service isn't found.
### Stale Hosts
Hosts discovery hasn't seen within their network's window carry a **Stale** badge, and the host list has a **Stale only** filter to isolate them. This flags devices that may have gone offline without deleting them. See [Staleness](https://scanopy.net/docs/using-scanopy/staleness/) for how the window works and why an entity goes stale.
### Hiding Hosts
Mark hosts as hidden to flag them for exclusion from the topology. Hidden hosts remain in the database with all their services, interfaces, and discovery history intact.
To toggle visibility:
1. Go to **Assets > Hosts**
2. Click the **Hide** button (eye icon) on the host card
The button turns blue when a host is hidden. Use the **Hidden** column filter to find hidden hosts.
**When to use:**
* Flag decommissioned devices that shouldn't appear in diagrams
* Mark infrastructure hosts that clutter the visualization (PDUs, UPS units, management interfaces)
* Identify test or development hosts to exclude from production views
## Subnets
Subnets represent network segments. Scanopy automatically detects subnets during discovery, but you can also create them manually. Assign a type (LAN, WiFi, DMZ, etc.) to label your subnets for filtering and organization.
Like hosts and services, subnets can go [stale](https://scanopy.net/docs/using-scanopy/staleness/) — carrying a **Stale** badge and a **Stale only** filter when discovery hasn't seen them within their network's window.
### Organizational Subnets
Two special subnet types use CIDR `0.0.0.0/0` and serve as organizational containers rather than real network segments:
**Internet** — For public/external services:
* Public DNS servers (1.1.1.1, 8.8.8.8)
* Cloud services your network connects to
* External APIs
**Remote** — For hosts not on your local network:
* Mobile devices connecting via VPN
* Remote office machines
* Friend's servers you want to track
# Organization & Access
Source: https://scanopy.net/docs/using-scanopy/organization/
## Organizations
Organizations are the top-level container in Scanopy. Every user belongs to exactly one organization, and all networks, hosts, and services exist within that organization.
Organizations are automatically created during registration and cannot be manually created or transferred.
### Roles
Account creation is invite-only — there is no self-registration. Invite users via **Platform > Users** — generate an invite link, select the invitee's role and which networks they may access, and share the link or send it by email.
Removing a user revokes all of their API keys, pending invitations, and network access grants.
## Networks
Networks are the primary organizational unit. Each network represents a distinct environment with its own hosts, services, and topology.
**Common patterns:**
* Separate networks for production vs. development
* One network per physical location
* Distinct networks for different security zones
### Network Settings
Open a network from **Assets > Networks** to edit its name, type, and its **Consider entities stale after** window — see [Staleness](https://scanopy.net/docs/using-scanopy/staleness/#the-staleness-window).
### Network Access
Users can be restricted to specific networks within an organization. When inviting a user, select which networks they may access — all data queries are filtered to those networks. This allows granting a contractor or external partner visibility into only the networks relevant to them while keeping other environments hidden.
## Credentials
Credentials are organization-scoped: managed under **Assets > Credentials**, and assignable to any network or host in the organization. See [Credentials](https://scanopy.net/docs/using-scanopy/credentials/) for types, targets, and assignment.
## API Keys
Each daemon gets its own API key, minted and bound to it automatically when you create the daemon. You don't create daemon keys by hand.
**Platform > API Keys** lists them. Any key marked legacy predates 1:1 binding and is shared across a network's daemons; see [Daemon Identity](https://scanopy.net/docs/reference/daemon-identity/) for how those behave and how to give a daemon a key of its own.
User API keys, for programmatic access to the API, are created here too — see the [API reference](https://scanopy.net/docs/api/).
# Staleness
Source: https://scanopy.net/docs/using-scanopy/staleness/
Scanopy marks an entity **Stale** when discovery hasn't observed it within its network's staleness window — a device powered off, a container removed, a host that moved. Nothing is deleted.
Stale does not mean gone. A stale host might be switched off, on the road, or briefly unreachable during a scan. Scanopy reports only how long it's been since discovery last saw it.
## The staleness window
Each network has its own window, because staleness is only meaningful relative to how often that network is scanned — a segment swept every 15 minutes and one swept monthly need very different thresholds.
The default is **28 days**. Set a network's own in the network edit modal (**Assets > Networks** → select the network → **Consider entities stale after**), as days and hours; leave it blank for the default. Set it comfortably above the network's scan interval so a normal gap between scans never trips it.
See [Networks](https://scanopy.net/docs/using-scanopy/organization/#networks) for where this fits among a network's settings.
## What can go stale
Only what discovery manages: hosts, services, and subnets it found and keeps re-observing. Anything you created by hand never goes stale, since there's no scan timestamp to judge it against.
Every entity is judged on **its own** last-seen time, independently of its parent. A service on a host that's gone quiet is stale only if that service hasn't been seen within the window — a recently observed service on an otherwise-dark host reads as current, and vice versa.
## Where it shows up
* **Lists** — hosts, services, and subnets carry a **Stale** badge past their window, with a **Stale only** filter and sorting by last-seen.
* **Topology** — stale nodes carry a **Stale** tag and a "last seen" detail. A **By staleness** filter (Current / Stale) fades everything else. See [Customizing Views](https://scanopy.net/docs/using-scanopy/topology/customizing-views/).
The tag is amber rather than red, matching the daemon status convention where red means broken and amber means behind.
# Tags
Source: https://scanopy.net/docs/using-scanopy/tags/
Tags are organization-wide labels you apply to hosts, services, subnets, networks, groups, and daemons. They're defined once for the organization, shared by every user, and usable across all networks — deleting one removes it from every entity that carried it.
There are two kinds, and they behave differently.
## Standard tags
Free-form labels. An entity can carry as many as you like, and they're used for filtering — in entity lists, and in the topology, where filtered elements fade rather than disappear so the layout stays stable.
Common schemes:
* **Environment** — production, staging, development
* **Criticality** — critical, high, medium, low
* **Ownership** — which team runs it
* **Lifecycle** — deprecated, migrating, new
* **Compliance** — pci-scope, hipaa, gdpr, internal-only
Combining two axes is usually more useful than one long list. `production` + `critical` identifies core infrastructure in a way that neither tag does alone.
## Application tags
An application tag answers "what is this part of?" rather than "what is this like", so the rules are stricter:
* A host or service carries **at most one**.
* A service **inherits its host's** application tag. A host tagged `billing` lends that to every service on it, without tagging each one.
* Tagging a service directly **overrides** what it inherited. Use this for the service on a shared host that belongs to something else.
This inheritance is what the [Application perspective](https://scanopy.net/docs/using-scanopy/topology/application/) groups on — each application tag becomes a container, and services fall into it by their own tag or their host's. A host running one thing needs a single tag; only the exceptions need tagging individually.
## Managing tags
Tags live under **Platform > Tags**, and can also be created inline while editing any entity. Renaming one updates it everywhere it's applied.
# Bulk delete Bindings
Source: https://scanopy.net/docs/api/bindings/bulk_delete_bindings/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new Binding
Source: https://scanopy.net/docs/api/bindings/create_binding/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete Binding
Source: https://scanopy.net/docs/api/bindings/delete_binding/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Export Bindings to CSV
Source: https://scanopy.net/docs/api/bindings/export_bindings_csv/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get Binding by ID
Source: https://scanopy.net/docs/api/bindings/get_binding_by_id/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Overview
Source: https://scanopy.net/docs/api/bindings/
Service bindings linking services to IP addresses and/or ports. Defines where a service is accessible.
# List all Bindings
Source: https://scanopy.net/docs/api/bindings/list_bindings/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a Binding
Source: https://scanopy.net/docs/api/bindings/update_binding/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk create Credentials
Source: https://scanopy.net/docs/api/credentials/bulk_create_credentials/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk delete Credentials
Source: https://scanopy.net/docs/api/credentials/bulk_delete_credentials/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new Credential
Source: https://scanopy.net/docs/api/credentials/create_credential/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete Credential
Source: https://scanopy.net/docs/api/credentials/delete_credential/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Export Credentials to CSV
Source: https://scanopy.net/docs/api/credentials/export_credentials_csv/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List all Credentials
Source: https://scanopy.net/docs/api/credentials/get_all_credentials/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a Credential by ID
Source: https://scanopy.net/docs/api/credentials/get_by_id_credential/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Overview
Source: https://scanopy.net/docs/api/credentials/
Credentials for network device discovery and management. Supports SNMP, Docker proxy, and other credential types.
# Update Credential
Source: https://scanopy.net/docs/api/credentials/update_credential/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk delete daemon_api_keys
Source: https://scanopy.net/docs/api/daemon-api-keys/bulk_delete_daemon_api_keys/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create Daemon API Key
Source: https://scanopy.net/docs/api/daemon-api-keys/create_daemon_api_key/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete daemon_api_key
Source: https://scanopy.net/docs/api/daemon-api-keys/delete_daemon_api_key/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Export Daemon API Keys to CSV
Source: https://scanopy.net/docs/api/daemon-api-keys/export_daemon_api_keys_csv/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get Daemon API Key by ID
Source: https://scanopy.net/docs/api/daemon-api-keys/get_daemon_api_key_by_id/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Overview
Source: https://scanopy.net/docs/api/daemon-api-keys/
API keys for daemon authentication. Create and manage keys that allow daemons to communicate with the server.
# List all Daemon API Keys
Source: https://scanopy.net/docs/api/daemon-api-keys/list_daemon_api_keys/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Rotate a Daemon API Key
Source: https://scanopy.net/docs/api/daemon-api-keys/rotate_key_handler/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a Daemon API Key
Source: https://scanopy.net/docs/api/daemon-api-keys/update_daemon_api_key/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk delete Dependencies
Source: https://scanopy.net/docs/api/dependencies/bulk_delete_dependencies/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new Dependency
Source: https://scanopy.net/docs/api/dependencies/create_dependency/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete Dependency
Source: https://scanopy.net/docs/api/dependencies/delete_dependency/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Export Dependencies to CSV
Source: https://scanopy.net/docs/api/dependencies/export_dependencies_csv/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List all Dependencies
Source: https://scanopy.net/docs/api/dependencies/get_all_dependencies/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get Dependency by ID
Source: https://scanopy.net/docs/api/dependencies/get_dependency_by_id/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Overview
Source: https://scanopy.net/docs/api/dependencies/
Service dependency relationships. Define how services depend on each other.
# Update a Dependency
Source: https://scanopy.net/docs/api/dependencies/update_dependency/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk delete daemons
Source: https://scanopy.net/docs/api/daemons/bulk_delete_daemons/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete daemon
Source: https://scanopy.net/docs/api/daemons/delete_daemon/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Email install command to current user
Source: https://scanopy.net/docs/api/daemons/email_install_command/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Export Daemons to CSV
Source: https://scanopy.net/docs/api/daemons/export_daemons_csv/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get daemon by ID
Source: https://scanopy.net/docs/api/daemons/get_daemon_by_id/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Generate daemon install command
Source: https://scanopy.net/docs/api/daemons/get_daemon_install_command/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get all daemons
Source: https://scanopy.net/docs/api/daemons/get_daemons/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Overview
Source: https://scanopy.net/docs/api/daemons/
Daemons are scanning agents that connect to the server to perform network discovery.
# Provision a daemon, or re-provision an existing one
Source: https://scanopy.net/docs/api/daemons/provision_daemon/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Retry connection to unreachable daemon
Source: https://scanopy.net/docs/api/daemons/retry_daemon_connection/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Test reachability of a daemon URL
Source: https://scanopy.net/docs/api/daemons/test_daemon_reachability/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update daemon
Source: https://scanopy.net/docs/api/daemons/update_daemon/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk delete hosts
Source: https://scanopy.net/docs/api/hosts/bulk_delete_hosts/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Consolidate hosts
Source: https://scanopy.net/docs/api/hosts/consolidate_hosts/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new host
Source: https://scanopy.net/docs/api/hosts/create_host/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete a host
Source: https://scanopy.net/docs/api/hosts/delete_host/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Export Hosts to CSV
Source: https://scanopy.net/docs/api/hosts/export_hosts_csv/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Export hosts with children to ZIP
Source: https://scanopy.net/docs/api/hosts/export_hosts_zip/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List all hosts
Source: https://scanopy.net/docs/api/hosts/get_all_hosts/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a host by ID
Source: https://scanopy.net/docs/api/hosts/get_host_by_id/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Overview
Source: https://scanopy.net/docs/api/hosts/
Network hosts (devices). Manage discovered or manually created hosts on your network.
# Rescan a host
Source: https://scanopy.net/docs/api/hosts/rescan_host/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a host
Source: https://scanopy.net/docs/api/hosts/update_host/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk delete Interfaces
Source: https://scanopy.net/docs/api/interfaces/bulk_delete_interfaces/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new Interface
Source: https://scanopy.net/docs/api/interfaces/create_if_entry/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete Interface
Source: https://scanopy.net/docs/api/interfaces/delete_interface/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Export Interfaces to CSV
Source: https://scanopy.net/docs/api/interfaces/export_interfaces_csv/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get Interface by ID
Source: https://scanopy.net/docs/api/interfaces/get_interface_by_id/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Overview
Source: https://scanopy.net/docs/api/interfaces/
SNMP ifTable entries. Physical and logical interfaces discovered via SNMP on hosts.
# List all Interfaces
Source: https://scanopy.net/docs/api/interfaces/list_interfaces/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update an Interface
Source: https://scanopy.net/docs/api/interfaces/update_if_entry/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create invite
Source: https://scanopy.net/docs/api/invites/create_invite/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get an invite by ID
Source: https://scanopy.net/docs/api/invites/get_invite/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List all invites
Source: https://scanopy.net/docs/api/invites/get_invites/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Overview
Source: https://scanopy.net/docs/api/invites/
Organization invitations. Invite users to join your organization.
# Revoke an invite
Source: https://scanopy.net/docs/api/invites/revoke_invite/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk delete IP addresses
Source: https://scanopy.net/docs/api/ip-addresses/bulk_delete_ip_addresses/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new IP address
Source: https://scanopy.net/docs/api/ip-addresses/create_ip_address/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete an IP address
Source: https://scanopy.net/docs/api/ip-addresses/delete_ip_address/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Export IP Addresses to CSV
Source: https://scanopy.net/docs/api/ip-addresses/export_ip_addresses_csv/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get IP Address by ID
Source: https://scanopy.net/docs/api/ip-addresses/get_ip_address_by_id/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Overview
Source: https://scanopy.net/docs/api/ip-addresses/
IP addresses assigned to hosts. Each address belongs to a host and a subnet, optionally has a MAC address, and represents an observed or configured address on the network.
# List all IP Addresses
Source: https://scanopy.net/docs/api/ip-addresses/list_ip_addresses/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update an IP address
Source: https://scanopy.net/docs/api/ip-addresses/update_ip_address/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk delete networks
Source: https://scanopy.net/docs/api/networks/bulk_delete_networks/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new network
Source: https://scanopy.net/docs/api/networks/create_network/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete a network
Source: https://scanopy.net/docs/api/networks/delete_network/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Export Networks to CSV
Source: https://scanopy.net/docs/api/networks/export_networks_csv/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List all networks
Source: https://scanopy.net/docs/api/networks/get_all_networks/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a network by ID
Source: https://scanopy.net/docs/api/networks/get_by_id_network/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Overview
Source: https://scanopy.net/docs/api/networks/
Network containers. Top-level organizational unit that contains subnets, hosts, and other entities.
# Update a network
Source: https://scanopy.net/docs/api/networks/update_network/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
#
Source: https://scanopy.net/docs/api/organizations/daemon_prompt_response/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete the organization entirely, including all data and users
Source: https://scanopy.net/docs/api/organizations/delete_organization/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get the current user's organization
Source: https://scanopy.net/docs/api/organizations/get_organization/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Overview
Source: https://scanopy.net/docs/api/organizations/
Manage organization settings.
# Submit referral source (how did you hear about us)
Source: https://scanopy.net/docs/api/organizations/submit_referral_source/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update organization name
Source: https://scanopy.net/docs/api/organizations/update_org_name/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update user profile with deferred marketing fields
Source: https://scanopy.net/docs/api/organizations/update_profile/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk delete Ports
Source: https://scanopy.net/docs/api/ports/bulk_delete_ports/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new port
Source: https://scanopy.net/docs/api/ports/create_port/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete Port
Source: https://scanopy.net/docs/api/ports/delete_port/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Export Ports to CSV
Source: https://scanopy.net/docs/api/ports/export_ports_csv/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get Port by ID
Source: https://scanopy.net/docs/api/ports/get_port_by_id/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Overview
Source: https://scanopy.net/docs/api/ports/
Ports that have been scanned and found open on a host.
# List all Ports
Source: https://scanopy.net/docs/api/ports/list_ports/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a port
Source: https://scanopy.net/docs/api/ports/update_port/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk delete Services
Source: https://scanopy.net/docs/api/services/bulk_delete_services/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new service
Source: https://scanopy.net/docs/api/services/create_service/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete Service
Source: https://scanopy.net/docs/api/services/delete_service/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Export Services to CSV
Source: https://scanopy.net/docs/api/services/export_services_csv/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List all services
Source: https://scanopy.net/docs/api/services/get_all_services/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get Service by ID
Source: https://scanopy.net/docs/api/services/get_service_by_id/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Overview
Source: https://scanopy.net/docs/api/services/
Services running on hosts. Detected or manually added services like databases, web servers, etc.
# Update a service
Source: https://scanopy.net/docs/api/services/update_service/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk delete Shares
Source: https://scanopy.net/docs/api/shares/bulk_delete_shares/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new share
Source: https://scanopy.net/docs/api/shares/create_share/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete Share
Source: https://scanopy.net/docs/api/shares/delete_share/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Export Shares to CSV
Source: https://scanopy.net/docs/api/shares/export_shares_csv/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get share metadata
Source: https://scanopy.net/docs/api/shares/get_public_share_metadata/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get Share by ID
Source: https://scanopy.net/docs/api/shares/get_share_by_id/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Overview
Source: https://scanopy.net/docs/api/shares/
Shared network views. Create read-only shareable links to your network topology.
# List all Shares
Source: https://scanopy.net/docs/api/shares/list_shares/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a share
Source: https://scanopy.net/docs/api/shares/update_share/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Take a snapshot of the current live topology + entity state for a network.
Source: https://scanopy.net/docs/api/snapshots/create_snapshot/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete Snapshot
Source: https://scanopy.net/docs/api/snapshots/delete_snapshot/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get Snapshot by ID
Source: https://scanopy.net/docs/api/snapshots/get_snapshot_by_id/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Overview
Source: https://scanopy.net/docs/api/snapshots/
Point-in-time capture of a network's topology and entities. Created manually via the topology tab; loadable from the snapshots dropdown.
# List all Snapshots
Source: https://scanopy.net/docs/api/snapshots/list_snapshots/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk delete Subnets
Source: https://scanopy.net/docs/api/subnets/bulk_delete_subnets/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new subnet
Source: https://scanopy.net/docs/api/subnets/create_subnet/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete Subnet
Source: https://scanopy.net/docs/api/subnets/delete_subnet/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Export Subnets to CSV
Source: https://scanopy.net/docs/api/subnets/export_subnets_csv/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get Subnet by ID
Source: https://scanopy.net/docs/api/subnets/get_subnet_by_id/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Overview
Source: https://scanopy.net/docs/api/subnets/
IP subnets within networks. Define address ranges and organize hosts by subnet.
# List all subnets
Source: https://scanopy.net/docs/api/subnets/list_subnets/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a subnet
Source: https://scanopy.net/docs/api/subnets/update_subnet/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk add a tag to multiple entities
Source: https://scanopy.net/docs/api/tags/bulk_add_tag/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk delete Tags
Source: https://scanopy.net/docs/api/tags/bulk_delete_tags/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk remove a tag from multiple entities
Source: https://scanopy.net/docs/api/tags/bulk_remove_tag/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new tag
Source: https://scanopy.net/docs/api/tags/create_tag/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete Tag
Source: https://scanopy.net/docs/api/tags/delete_tag/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Export Tags to CSV
Source: https://scanopy.net/docs/api/tags/export_tags_csv/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List all tags
Source: https://scanopy.net/docs/api/tags/get_all_tags/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get Tag by ID
Source: https://scanopy.net/docs/api/tags/get_tag_by_id/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Overview
Source: https://scanopy.net/docs/api/tags/
Custom tags for categorization. Apply labels to entities for filtering and organization.
# Set all tags for an entity
Source: https://scanopy.net/docs/api/tags/set_entity_tags/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update Tag
Source: https://scanopy.net/docs/api/tags/update_tag/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Export topology as Confluence wiki markup
Source: https://scanopy.net/docs/api/topologies/export_confluence/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Export topology as Mermaid flowchart
Source: https://scanopy.net/docs/api/topologies/export_mermaid/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Export Topologies to CSV
Source: https://scanopy.net/docs/api/topologies/export_topologies_csv/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get all topologies for the authenticated user's networks.
Source: https://scanopy.net/docs/api/topologies/get_all_topologies/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get Topology by ID
Source: https://scanopy.net/docs/api/topologies/get_topology_by_id/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Overview
Source: https://scanopy.net/docs/api/topologies/
Network topology maps showing host relationships and connections.
# Update_topology
Source: https://scanopy.net/docs/api/topologies/update_topology/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk delete user API keys
Source: https://scanopy.net/docs/api/user-api-keys/bulk_delete_user_api_keys/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new user API key
Source: https://scanopy.net/docs/api/user-api-keys/create_user_api_key/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete a user API key
Source: https://scanopy.net/docs/api/user-api-keys/delete_user_api_key/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Export User API Keys to CSV
Source: https://scanopy.net/docs/api/user-api-keys/export_user_api_keys_csv/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get all user API keys for the current user
Source: https://scanopy.net/docs/api/user-api-keys/get_all_user_api_keys/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a user API key by ID
Source: https://scanopy.net/docs/api/user-api-keys/get_user_api_key_by_id/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Overview
Source: https://scanopy.net/docs/api/user-api-keys/
User API keys for programmatic access. Create and manage personal API keys with scoped permissions.
# Rotate a user API key
Source: https://scanopy.net/docs/api/user-api-keys/rotate_user_api_key/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update a user API key
Source: https://scanopy.net/docs/api/user-api-keys/update_user_api_key/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk delete users
Source: https://scanopy.net/docs/api/users/bulk_delete_users/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete a user
Source: https://scanopy.net/docs/api/users/delete_user/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Export Users to CSV
Source: https://scanopy.net/docs/api/users/export_users_csv/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List all users
Source: https://scanopy.net/docs/api/users/get_all_users/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get user by ID
Source: https://scanopy.net/docs/api/users/get_user_by_id/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Overview
Source: https://scanopy.net/docs/api/users/
User account management. Manage user profiles and permissions within organizations.
# Update your own user record
Source: https://scanopy.net/docs/api/users/update_user/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk delete Vlans
Source: https://scanopy.net/docs/api/vlans/bulk_delete_vlans/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Create a new VLAN
Source: https://scanopy.net/docs/api/vlans/create_vlan/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete Vlan
Source: https://scanopy.net/docs/api/vlans/delete_vlan/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Bulk upsert VLANs from discovery
Source: https://scanopy.net/docs/api/vlans/discovery_upsert_vlans/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Export Vlans to CSV
Source: https://scanopy.net/docs/api/vlans/export_vlans_csv/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List all VLANs
Source: https://scanopy.net/docs/api/vlans/get_all_vlans/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get Vlan by ID
Source: https://scanopy.net/docs/api/vlans/get_vlan_by_id/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Overview
Source: https://scanopy.net/docs/api/vlans/
VLANs (802.1Q virtual LANs) defined or discovered on the network. Each VLAN has a number (1-4094), a name, and an optional description, and is referenced by interfaces that participate in it.
# Update Vlan
Source: https://scanopy.net/docs/api/vlans/update_vlan/
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Set up Docker discovery
Source: https://scanopy.net/docs/guides/integrations/docker/
Scanopy daemons discover Docker containers by connecting to the Docker API. On the daemon's own host that happens automatically; reaching a restricted API, or one on another machine, needs a proxy.
## What gets discovered
* Running containers, with their names and metadata
* Container networks and published port bindings, including automatically created bridge subnets
* Containerized services, identified from the API rather than from a port scan — so containers that publish nothing to the host are still detected
Containers appear as **Docker Container** services on their host, and the container-to-host relationship shows up in the [Workloads perspective](https://scanopy.net/docs/using-scanopy/topology/workloads).
Discovery is local to one host: the daemon sees the containers on its own machine, or on the single host a proxy credential points at. See [Docker host matching](https://scanopy.net/docs/using-scanopy/discovery/naming-and-deduplication/#container-host-matching) for what that means for deduplication.
## Prerequisites
* Docker running on the host whose containers you want to discover
* For direct socket access, the daemon can read `/var/run/docker.sock` on its own host
* For proxied access, a socket proxy deployed and reachable from the daemon — see [Docker Proxy](#docker-proxy) below for supported proxies and the operations they must allow
## Choosing a credential type
| Situation | Use |
| ----------------------------------------------------------------------- | ------------------------------------------------- |
| Daemon runs on the Docker host and may hold full API access | Nothing — the local socket is used automatically |
| Daemon runs on the Docker host, but the socket is at a non-default path | **Docker Socket** |
| You want to restrict which API operations the daemon can call | **Docker Proxy**, with the proxy on the same host |
| Docker runs on a machine with no daemon | **Docker Proxy**, with the proxy on that machine |
## Docker Proxy
A socket proxy sits in front of the Docker socket and allows only the API operations you permit. One credential type covers both reasons to use one — restricting local access, and reaching Docker on another machine — since only the address changes.
To discover containers on a machine with no daemon: deploy a proxy there, create a Docker Proxy credential with its port, and assign it to that host. Containers are then reported as services on the remote host.
The TLS fields apply to HTTPS proxies only. Each can be entered inline or as a file path readable by the daemon.
### Supported proxies
Configuration for two common socket proxies.
**Tecnativa docker-socket-proxy**
```yaml
services:
docker-proxy:
image: tecnativa/docker-socket-proxy
environment:
- CONTAINERS=1 # Required: list and inspect containers
- NETWORKS=1 # Required: list networks for subnet discovery
- EXEC=1 # Required: exec into containers for endpoint probing
- POST=1 # Required: create exec instances
- INFO=1 # Optional: system info
- BUILD=0
- COMMIT=0
- CONFIGS=0
- DISTRIBUTION=0
- GRPC=0
- IMAGES=0
- NODES=0
- PLUGINS=0
- SECRETS=0
- SERVICES=0
- SESSION=0
- SWARM=0
- SYSTEM=0
- TASKS=0
- VOLUMES=0
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
ports:
- '2375:2375'
```
**wollomatic socket-proxy**
```yaml
services:
docker-proxy:
image: wollomatic/socket-proxy
environment:
- ALLOW_RESTARTS=0
- LOG_LEVEL=info
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
ports:
- '2375:2375'
```
wollomatic's proxy uses allowlists rather than per-capability flags. Refer to their documentation for configuring the endpoints below.
### Required API operations
Whatever proxy you use, allow these endpoints:
| Endpoint | Method | Purpose |
| ----------------------- | ------ | ---------------------------------------------------- |
| `/containers/json` | GET | List running containers |
| `/containers/{id}/json` | GET | Get container details (ports, networks, config) |
| `/networks` | GET | List networks for subnet discovery |
| `/exec/{id}/json` | GET | Check exec instance status |
| `/containers/{id}/exec` | POST | Create exec instance for endpoint probing |
| `/exec/{id}/start` | POST | Start exec to probe HTTP endpoints inside containers |
**Why exec?** Scanopy uses `exec` to probe HTTP endpoints from inside containers that don't publish ports to the host. Without exec access, containers are still discovered but service detection is less accurate.
## Docker Socket
The daemon connects directly to `/var/run/docker.sock` on its own host. This needs no credential and no toggle — the daemon uses the socket whenever it's accessible.
Create a **Docker Socket** credential only to point the daemon at a socket somewhere other than the default path.
Because the socket is reachable only over the daemon's own loopback, this credential type can be assigned to a daemon host and nowhere else.
## Verifying it works
1. Go to **Discover > Scan > Scheduled** and run a discovery, or wait for the next scheduled run
2. Open the host running Docker and go to its **Services** tab
3. Look for **Docker Container** services, each with its published ports
4. Open the [Workloads perspective](https://scanopy.net/docs/using-scanopy/topology/workloads) and confirm the containers appear nested under their host
## Troubleshooting
### "Connection refused" to proxy
1. Verify the proxy is running: `docker ps | grep proxy`
2. Check the proxy is listening: `curl http://docker-proxy:2375/version`
3. Ensure the daemon can reach it — same Docker network, or an exposed port
### "Permission denied" from proxy
The proxy is blocking a required endpoint. For Tecnativa, confirm `CONTAINERS=1`, `NETWORKS=1`, `EXEC=1`, and `POST=1` (the last is required for exec).
### SSL certificate errors
1. Verify certificate paths are correct and mounted
2. Check the certificate is valid: `openssl x509 -in /certs/client-cert.pem -text -noout`
3. Include the CA chain if using self-signed certs
4. Check that the cert and key aren't swapped
For diagnosing credential loading and file read or PEM errors from daemon logs, see [Credential troubleshooting](https://scanopy.net/docs/setting-up-daemons/troubleshooting-scans/credentials/).
# Set up Instant On discovery
Source: https://scanopy.net/docs/guides/integrations/instant-on/
An Instant On site's portal holds an inventory of the devices it manages, including their ports and the uplinks between them. Pointing Scanopy at the site is how that inventory becomes hosts, interfaces, and physical links.
One credential covers a whole site, so assign it to **one** of the Instant On devices on that site. Scanopy reads the site through that device and creates the rest from what the site reports.
## What gets discovered
On each scan, the daemon reads the site's inventory and client list:
| From the portal | Becomes |
| -------------------------------------------------- | --------------------------------------------------------------------- |
| Site inventory (switches, access points, gateways) | Hosts, matched against existing hosts by IP |
| Each device's ports | Interfaces, with names, speeds and link state |
| Uplinks between devices | Physical links in [L2 Physical](https://scanopy.net/docs/using-scanopy/topology/l2-physical/) |
| Device class (`SWITCH`, `ACCESS_POINT`, …) | An **Instant On Switch** or **Instant On Access Point** service |
| Wired clients | The MAC on the port it is connected to |
A link the portal reports and the same link reported by a switch at its other end describe one cable rather than two.
## Prerequisites
* The daemon host can reach `portal.instant-on.hpe.com` and `sso.arubainstanton.com` over HTTPS. The devices themselves are not contacted for this data
* A portal account with **multi-factor authentication disabled** — see below
## Choosing a credential type
## Instant On Portal Account
The daemon signs in to the cloud portal as a site account and reads the site's inventory through it.
### Creating an account for Scanopy
Add a dedicated account for Scanopy with the read-only **Viewer** role rather than using your own administrator login:
1. In the Instant On app or portal, open the site and go to **Site management → Accounts managing this site**
2. Choose **Add account**, enter an email address for the Scanopy account, and assign the **Viewer** role
3. Complete the invitation from that mailbox to activate the account
4. Make sure multi-factor authentication is **not** enabled on it
Scanopy submits the account credentials directly and has nowhere to answer a second-factor challenge, so an account with MFA enabled cannot sign in.
Leave **Site** blank to read every site the account can see. Setting it restricts the fetch to one site.
## Verifying it works
1. Run a discovery from **Discover > Scan > Scheduled**
2. Open the scan session — the credential appears in the session's credential summary
3. Check **Assets > Hosts** for the site's other devices, each carrying its model, serial and firmware
4. Open [L2 Physical](https://scanopy.net/docs/using-scanopy/topology/l2-physical/) and confirm the switches now show what they connect to
## Troubleshooting
### Sign-in fails with the right password
Multi-factor authentication on the account blocks it. Scanopy submits the credentials directly and cannot answer a challenge. Create a dedicated Viewer account with MFA disabled and use that.
### The scan reports that the host is not in any site this account manages
The credential is assigned to a host that is not one of the site's Instant On devices. Reassign it to a switch or access point the portal lists.
### Devices appear but have no ports
Access points report no port table — only switches do. If a *switch* shows no ports, the portal may have changed the shape of its response; please [report it](https://github.com/scanopy/scanopy/issues/new).
### Duplicate work on a large site
Each host the credential is assigned to fetches the whole site independently. Assign it to one device per site.
For diagnosing credential loading and assignment from daemon logs, see [Credential troubleshooting](https://scanopy.net/docs/setting-up-daemons/troubleshooting-scans/credentials/).
# Set up Podman discovery
Source: https://scanopy.net/docs/guides/integrations/podman/
Scanopy daemons discover Podman containers by connecting to the Podman API. On the daemon's own host that happens automatically; reaching a restricted API, or one on another machine, needs a proxy.
## What gets discovered
* Running containers, with their names and metadata
* Container networks and published port bindings, including automatically created bridge subnets
* Containerized services, identified from the API rather than from a port scan — so containers that publish nothing to the host are still detected
Containers appear as **Podman Container** services on their host. Pods are modeled as generic containers with each container in the pod attributed individually. The container-to-host relationship shows up in the [Workloads perspective](https://scanopy.net/docs/using-scanopy/topology/workloads).
Discovery is local to one host: the daemon sees the containers on its own machine, or on the single host a proxy credential points at.
## Prerequisites
* Podman running on the host whose containers you want to discover
* The API socket enabled — it isn't always running by default:
```bash
# Rootless (per-user socket at $XDG_RUNTIME_DIR/podman/podman.sock)
systemctl --user start podman.socket
# Rootful (system socket at /run/podman/podman.sock)
sudo systemctl start podman.socket
```
* For proxied access, a TCP endpoint in front of that socket, reachable from the daemon
**Rootless vs rootful.** A rootless socket only sees that user's containers, and rootless networking may not expose a bridge subnet. Run the daemon (or the proxy) as the same user that owns the containers you want to discover, or use the rootful socket for host-wide visibility.
## Choosing a credential type
| Situation | Use |
| ----------------------------------------------------------------------- | ------------------------------------------------- |
| Daemon runs on the Podman host and the socket is in a standard location | Nothing — the socket is auto-detected |
| Daemon runs on the Podman host, but the socket is elsewhere | **Podman Socket** |
| You want to restrict which API operations the daemon can call | **Podman Proxy**, with the proxy on the same host |
| Podman runs on a machine with no daemon | **Podman Proxy**, with the proxy on that machine |
## Podman Proxy
A proxy in front of the socket lets you restrict which API operations are allowed, and lets a daemon reach Podman on a machine it isn't running on.
Any reverse proxy that forwards to the socket works — for example an nginx proxy mapping a TCP port onto `/run/podman/podman.sock`, optionally terminating TLS. To discover containers on a machine that runs no daemon: expose the endpoint there, create a Podman Proxy credential with its port, and assign it to the remote host. Containers are then reported as services on the remote host rather than on the daemon's host.
The TLS fields apply to HTTPS endpoints only. Each can be entered inline or as a file path readable by the daemon.
### Required API operations
Podman exposes a Docker-compatible API, and Scanopy uses that compatibility layer. Restrict the proxy to these endpoints:
| Endpoint | Method | Purpose |
| ----------------------- | ------ | ---------------------------------------------------- |
| `/containers/json` | GET | List running containers |
| `/containers/{id}/json` | GET | Get container details (ports, networks, config) |
| `/networks` | GET | List networks for subnet discovery |
| `/exec/{id}/json` | GET | Check exec instance status |
| `/containers/{id}/exec` | POST | Create exec instance for endpoint probing |
| `/exec/{id}/start` | POST | Start exec to probe HTTP endpoints inside containers |
**Why exec?** Scanopy uses `exec` to probe HTTP endpoints from inside containers that don't publish ports to the host. Without exec access, containers are still discovered but service detection is less accurate.
## Podman Socket
The daemon connects directly to the Podman socket on its own host, automatically, whenever it's accessible. It looks in this order:
1. The `CONTAINER_HOST` environment variable, when it points at a `unix://` socket path
2. The rootful socket: `/run/podman/podman.sock`
3. The rootless socket: `$XDG_RUNTIME_DIR/podman/podman.sock`
Create a **Podman Socket** credential only when your socket is somewhere else — its path overrides the auto-detection above.
Because the socket is reachable only over the daemon's own loopback, this credential type can be assigned to a daemon host and nowhere else.
## Verifying it works
1. Go to **Discover > Scan > Scheduled** and run a discovery, or wait for the next scheduled run
2. Open the host running Podman and go to its **Services** tab
3. Look for **Podman Container** services, each with its published ports
4. Open the [Workloads perspective](https://scanopy.net/docs/using-scanopy/topology/workloads) and confirm the containers appear nested under their host
## Troubleshooting
### No containers discovered
1. Confirm the API socket is running: `systemctl --user status podman.socket` (rootless) or `sudo systemctl status podman.socket` (rootful)
2. Confirm the daemon can see the socket path — check `CONTAINER_HOST`, `/run/podman/podman.sock`, and `$XDG_RUNTIME_DIR/podman/podman.sock`
3. If the daemon and the containers run under different users, the rootless socket won't expose them — run both as the same user, or use the rootful socket
### "Connection refused" to a proxy
1. Verify the proxy is running and listening on the expected port
2. Ensure the daemon can reach it — same network, or an exposed port
3. Check the proxy forwards to a valid Podman socket path
### SSL certificate errors
1. Verify certificate paths are correct and mounted
2. Check the certificate is valid: `openssl x509 -in /certs/client-cert.pem -text -noout`
3. Include the CA chain if using self-signed certs
4. Check that the cert and key aren't swapped
For diagnosing credential loading and file read or PEM errors from daemon logs, see [Credential troubleshooting](https://scanopy.net/docs/setting-up-daemons/troubleshooting-scans/credentials/).
# Set up SNMP discovery
Source: https://scanopy.net/docs/guides/integrations/snmp/
Without SNMP, a discovered host has only what port scanning and DNS provide — addresses, open ports, detected services, and a reverse DNS name. SNMP adds what the device says about itself.
## What gets discovered
* **System identity** — sysName, sysDescr, sysLocation, sysContact
* **Interface table** — every interface with its name, speed, MAC address, IP assignments, and operational status
* **LLDP/CDP neighbors** — the directly connected devices the host itself reports
Neighbor data is what makes accurate physical topology possible: it's the source for the links drawn in [L2 Physical](https://scanopy.net/docs/using-scanopy/topology/l2-physical/). Interface and IP data also improve host deduplication, since a MAC address gives Scanopy a second way to recognize a device whose address has changed.
For the full list of queried MIBs and OIDs, see the [SNMP reference](https://scanopy.net/docs/reference/snmp/).
## Prerequisites
* SNMP enabled on the target devices
* UDP port 161 reachable from the daemon host to those devices
* A community string for v1/v2c, or USM credentials for v3
SNMPv1 and SNMPv2c send community strings in cleartext on the wire. Use dedicated read-only community strings for Scanopy, and consider network segmentation to limit exposure. For devices that support it, prefer SNMPv3, which authenticates and encrypts on the wire.
## Choosing a credential type
Each SNMP version is its own credential type. Pick the highest one your devices support.
| Situation | Use |
| -------------------------------------------------------------------- | --------------------------------------------------- |
| The device supports v3 | **SNMP v3** |
| The device is v2c-era gear, or you need a shared read-only community | **SNMP v2c** |
| The device supports nothing newer than v1 | **SNMP v1** |
| The device is still on the default `public` community | Nothing — `public` is always tried as a last resort |
Versions can be mixed freely across a network. Each host resolves its own credential, so v3 on the core switches alongside v2c on older access gear works fine.
## SNMP v1
A single shared community string, sent in cleartext. Use this only for devices that support nothing newer — v2c takes the same field and is otherwise identical from Scanopy's side.
## SNMP v2c
A single shared community string, sent in cleartext. The common choice for gear that predates v3.
### Reading a per-VLAN MAC-address table
Cisco switches keep a separate forwarding database for each VLAN and expose it by **indexing the community string** — you append `@` and a VLAN id, and the switch answers from that VLAN's table. Scanopy sends the community exactly as you type it, so this works without any extra setting:
| Community you enter | What the switch returns |
| ------------------- | ------------------------------------------------- |
| `readonly` | The default context — often almost no MAC entries |
| `readonly@20` | VLAN 20's MAC-address table |
If a switch reports a full set of interfaces but only one or two learned MACs, this is usually why. Add a second credential with the indexed form and assign it to that switch. Other vendors expose the same data through an SNMPv3 context name instead — see below.
## SNMP v3
Authenticated and encrypted on the wire, and the version to prefer wherever a device supports it. Scanopy always uses AuthPriv — authentication *and* privacy together — which is why both protocol pairs are required rather than optional. AuthNoPriv and NoAuthNoPriv are not supported, and MD5 authentication is not available.
Leave **Context Name** blank unless the device keeps its **MAC-address table in a named context**, which some switches do to separate it per VLAN. Naming the context here reads that table instead of the default one, which on such devices holds almost nothing.
The context applies to the bridge and VLAN queries only. Interfaces, LLDP neighbours and ARP entries are always read from the default context, because that is where devices serve them — so setting this field cannot cost you the rest of a switch's data.
Sending the context name requires a daemon on **0.17.12 or newer**. Earlier daemons accept the field and store it, but always query the default context.
## Verifying it works
1. Go to **Discover > Scan > Scheduled** and run a discovery, or wait for the next scheduled run
2. Open a host that should have SNMP data
3. Check for:
* **sysName** and **sysDescr** in the host's SNMP tab
* **Interfaces** in the ifEntry tab — names, speeds, and IP assignments
* **Neighbors** in the [L2 Physical](https://scanopy.net/docs/using-scanopy/topology/l2-physical/) topology
You can confirm a network-wide assignment from **Discover > Daemons** — hover the Network chip to see whether an SNMP credential is configured.
## Troubleshooting
The usual symptom is a scan that completes and detects port 161, but leaves the host's **SNMP** and **ifEntry** tabs empty.
### No SNMP data on any host
The credential exists but isn't assigned. Creating a credential doesn't activate SNMP — see [Where a credential applies](https://scanopy.net/docs/using-scanopy/credentials/#where-a-credential-applies).
### No SNMP data on one host
Test the device directly from the daemon's host to separate a credential problem from a device problem:
```bash
# v1 / v2c — try the default community, then yours
snmpwalk -v2c -c public DEVICE_IP 1.3.6.1.2.1.2.2.1.2
snmpwalk -v2c -c YOUR_COMMUNITY_STRING DEVICE_IP 1.3.6.1.2.1.2.2.1.2
# v3
snmpwalk -v3 -l authPriv -u SECURITY_NAME \
-a SHA -A AUTH_PASSWORD -x AES -X PRIV_PASSWORD \
DEVICE_IP 1.3.6.1.2.1.2.2.1.2
```
If `public` works but your own string doesn't, the credential is wrong or isn't reaching this host — a per-host credential overrides the network default. If neither works, SNMP isn't enabled on the device or it refuses that version.
A device that enforces v3 will not answer a community string at all. Note that Scanopy's v3 is AuthPriv only, so a device configured for AuthNoPriv won't connect either.
### System data appears but no neighbors
The device isn't reporting an LLDP or CDP neighbor table. Some managed switches collect neighbor data internally without exposing the LLDP MIB over SNMP, in which case a walk returns nothing and the neighbors have to come from that vendor's own management API instead.
### Interfaces missing or incomplete
Fewer interfaces than the device has — or interfaces with no neighbor, VLAN or learned-MAC data — means the table walk was cut short. A walk is capped at 10,000 rows and each query at 30 seconds, so a very large table or a device that stalls partway through returns a partial result. Truncation is reported as a warning on the discovery session, not just in the daemon logs.
Interfaces are saved before the neighbor and VLAN queries run, so bare interfaces with no neighbor data mean the device hung later in collection.
Check how long a full `snmpwalk` of the interface table takes from the daemon host, whether the device is rate-limiting SNMP, and whether its interface count is genuinely near the cap — stacked chassis and large modular switches can be.
### Reading the daemon logs
With debug logging enabled, these messages appear during a scan of the target host:
| Log message | Meaning |
| ------------------------------------------------------ | ------------------------------------------ |
| `Creating SNMP community session` | Attempting v1/v2c |
| `Creating SNMPv3 session` | Attempting v3 |
| `SNMP session created successfully` | UDP socket opened |
| `SNMP GET {name} failed from {ip}` | OID query failed — auth or protocol error |
| `SNMP GET {name} timeout from {ip}` | Device didn't respond in time |
| `SNMP system info retrieved` with sys\_descr/sys\_name | Success |
| `No working SNMP credential found for {ip}` | Every credential was tried and none worked |
For credential loading problems — unreadable files, PEM errors, and the per-session assignment summary — see [Credential troubleshooting](https://scanopy.net/docs/setting-up-daemons/troubleshooting-scans/credentials/).
# Set up UniFi discovery
Source: https://scanopy.net/docs/guides/integrations/unifi/
A UniFi controller holds an inventory of every device it has adopted, including neighbor data its switches collect internally but do not publish over SNMP. Pointing Scanopy at the controller is how that inventory becomes hosts, interfaces, and physical links.
One credential describes one controller, which then reports on many devices, so it is pointed at the single host running that controller — or at the daemon host, when the daemon runs on the controller itself.
## What gets discovered
On each scan, the daemon reads the controller's device inventory for the configured site:
| From the controller | Becomes |
| ----------------------------------------- | --------------------------------------------------------------------- |
| Adopted devices (switches, APs, gateways) | Hosts, matched against existing hosts by IP |
| Each device's port table | Interfaces, with names, speeds and MAC addresses |
| LLDP neighbor and uplink tables | Physical links in [L2 Physical](https://scanopy.net/docs/using-scanopy/topology/l2-physical/) |
| Device class (`usw`, `uap`, …) | A **UniFi Switch** or **UniFi Gateway** service |
A link the controller reports and the same link reported by the device at its other end describe one cable rather than two.
## Prerequisites
* The controller is reachable from the daemon host on its HTTPS port
* You know the controller's **internal** site name — the `` in `/manage/site/` from the controller URL, not the display name shown in the site switcher. The two are identical on a default install and diverge when a site is renamed
* The daemon has **Probe with invalid certificates** enabled in its scan settings. UniFi controllers ship self-signed certificates, and without this the connection fails before it can authenticate
Every credential type needs the right **Controller Port**:
| Controller | Port |
| ---------------------------------------------------------- | ----- |
| UniFi OS console — Dream Machine, Cloud Key, Cloud Gateway | 443 |
| Self-hosted UniFi OS Server | 11443 |
| Legacy self-hosted Network Application | 8443 |
Read it off the URL you use to reach the controller. The API layout differs between these products, so a wrong port fails to connect rather than failing to authenticate.
## Choosing a credential type
Both types reach the same API and discover the same things. The deciding factor is which controller you run.
| Situation | Use |
| -------------------------------------------------------------- | ---------------------------------------------------------------- |
| UniFi OS console or self-hosted UniFi OS Server | Either — **UniFi API Key** is simpler |
| Legacy self-hosted Network Application on 8443 | **UniFi Local Admin** — Ubiquiti does not support API keys there |
| The controller account has multi-factor authentication enabled | **UniFi API Key**, or create a local-only admin account |
## UniFi API Key
The daemon authenticates with a controller API key sent as a request header. Stateless, and nothing expires on a session timeout — but UniFi OS only, since the legacy Network Application has no API keys at all.
Create the key first: in the controller, go to **Settings → Control Plane → Integrations → Create API Key**.
## UniFi Local Admin
The daemon logs in with a controller admin account and holds the resulting session. Works on every controller type, including the legacy self-hosted Network Application.
Use a **local-only** admin account. A cloud-linked Ubiquiti SSO account with multi-factor authentication enabled cannot complete the login, and the daemon has no way to answer the challenge.
## Verifying it works
1. Run a discovery from **Discover > Scan > Scheduled**
2. Open the scan session — the credential appears in the session's credential summary
3. Check **Assets > Hosts** for the controller's adopted devices
4. Open [L2 Physical](https://scanopy.net/docs/using-scanopy/topology/l2-physical/) and confirm the switches now show their neighbors
## Troubleshooting
### The credential connects but no devices appear
The site name is the usual cause: an unknown site returns an empty device list rather than an error. Confirm the value against `/manage/site/` in your controller URL.
### Connection fails on a UniFi OS console
Check the port against the table above. A UniFi OS console serves the Network Application behind a proxy path on 443, while the legacy self-hosted application serves it at the root on 8443.
### Authentication fails with a correct password
Multi-factor authentication on the account blocks the login. Create a local-only admin account on the controller and use that instead.
### Certificate errors
Enable **Probe with invalid certificates** in the daemon's scan settings. Controllers ship self-signed certificates by default.
For diagnosing credential loading and assignment from daemon logs, see [Credential troubleshooting](https://scanopy.net/docs/setting-up-daemons/troubleshooting-scans/credentials/).
# Isolating Daemon Networking with MACVLAN
Source: https://scanopy.net/docs/guides/networking/macvlan-setup/
MACVLAN lets you deploy the daemon as a separate device on your LAN with its own MAC and IP address, without using `--network=host`. This provides network isolation and avoids port conflicts.
## When to Use MACVLAN
| Use MACVLAN when... | Use host networking when... |
| -------------------------------------------- | --------------------------- |
| You need network isolation | Simpler setup is preferred |
| You want the daemon as a separate LAN device | |
**Platform support:** Linux only, same as Docker deployment for Daemon in general.
## Prerequisites
1. Linux host with Docker
2. Physical network interface (e.g., `eth0`, `enp3s0`)
3. Static IP range reserved outside your DHCP pool
## Setup
### 1. Find Your Network Interface
```bash
ip link show
# Look for: eth0, enp3s0, eno1, etc.
```
### 2. Plan Your IP Range
Reserve IPs that DHCP won't assign:
| Setting | Example |
| ------------- | -------------------------- |
| Subnet | 192.168.1.0/24 |
| Gateway | 192.168.1.1 |
| DHCP range | 192.168.1.100–200 |
| MACVLAN range | 192.168.1.224/27 (224–255) |
### 3. Edit generated docker-compose.yml
In the UI, create a new daemon; then, edit the docker compose as follows:
**Add network service**
```yaml
networks:
scanopy_net:
driver: macvlan
driver_opts:
parent: eth0 # Your interface from step 1
ipam:
config:
- subnet: 192.168.1.0/24
gateway: 192.168.1.1
```
**Add to daemon service**
```yaml
networks:
scanopy_net:
ipv4_address: 192.168.1.225
```
*and*
```yaml
cap_add:
- NET_RAW
```
**Remove from daemon service**
```yaml
network_mode: host
privileged: true
```
**Example**
```yaml
services:
daemon:
image: ghcr.io/scanopy/scanopy/daemon:latest
container_name: scanopy-daemon
restart: unless-stopped
cap_add:
- NET_RAW
networks:
scanopy_net:
ipv4_address: 192.168.1.225
environment:
- SCANOPY_SERVER_URL=https://your-server.example.com
- SCANOPY_NETWORK_ID=your-network-id
- SCANOPY_DAEMON_API_KEY=your-api-key
volumes:
- daemon-config:/root/.config/daemon
networks:
scanopy_net:
driver: macvlan
driver_opts:
parent: eth0 # Your interface from step 1
ipam:
config:
- subnet: 192.168.1.0/24
gateway: 192.168.1.1
volumes:
daemon-config:
```
### 4. Deploy
```bash
docker compose up -d
```
## Troubleshooting
Enable debug logging to see interface detection details:
```yaml
environment:
- SCANOPY_LOG_LEVEL=debug
```
For other issues, see [Daemon Troubleshooting](https://scanopy.net/docs/setting-up-daemons/troubleshooting-setup/).
## Combining with Multi-VLAN
MACVLAN works with the strategies in [Planning Daemon Deployment](https://scanopy.net/docs/setting-up-daemons/planning-daemon-deployment/). Deploy one MACVLAN daemon per VLAN for full Layer 2 access on each segment.
# Scanning Isolated Networks from One Host
Source: https://scanopy.net/docs/guides/networking/multiple-daemons/
This guide walks through running multiple daemon instances on a single host, each scanning different interfaces or network segments. For help deciding whether this is the right approach for your network, see [Planning Daemon Deployment](https://scanopy.net/docs/setting-up-daemons/planning-daemon-deployment/).
## How multiple daemons coexist
Each `scanopy-daemon install` on a host takes its own **slot** — an independent install with its own config file, service, and log file. The first install takes the default slot (`scanopy-daemon`); each additional install takes the next number (`scanopy-daemon-2`, `scanopy-daemon-3`, …). The installer allocates the slot for you — you don't name it.
| Slot | Service | Config (Linux) |
| ------------------ | ------------------ | -------------------------------------------------- |
| `scanopy-daemon` | `scanopy-daemon` | `/etc/scanopy/daemon/scanopy-daemon/config.json` |
| `scanopy-daemon-2` | `scanopy-daemon-2` | `/etc/scanopy/daemon/scanopy-daemon-2/config.json` |
| `scanopy-daemon-3` | `scanopy-daemon-3` | `/etc/scanopy/daemon/scanopy-daemon-3/config.json` |
Each slot's config holds its own daemon ID, API key, server URL, and runtime state. Slots are fully independent — starting, stopping, or reconfiguring one does not affect the others. A slot is not the daemon's *name*: the name is assigned by the server and shown in the app, and can be changed there without moving the slot.
A daemon's display name comes from the server, so you never pass it at install time. Run `scanopy-daemon list` on the host to see each installed slot, its service, and its config path.
**System config locations by platform** (service installs):
| Platform | Base directory |
| -------- | ---------------------------------------------- |
| Linux | `/etc/scanopy/daemon/` |
| macOS | `/Library/Application Support/Scanopy/daemon/` |
| Windows | `%ProgramData%\Scanopy\daemon\` |
| FreeBSD | `/usr/local/etc/scanopy/daemon/` |
## Restricting daemons to specific interfaces
Use `--interfaces` to limit which network interfaces a daemon scans. Without it, the daemon scans all interfaces.
```bash
# Scan only eth0
sudo scanopy-daemon install --interfaces eth0
# Scan two interfaces
sudo scanopy-daemon install --interfaces eth0,eth1
```
The same setting works as an environment variable or in the config file:
```bash
# Environment variable
SCANOPY_INTERFACES=eth0,eth1
# config.json
{ "interfaces": ["eth0", "eth1"] }
```
When `--interfaces` is set, the daemon only reports and scans subnets attached to those interfaces. This prevents overlap between daemon instances sharing the same host.
## Ports and bind addresses
Every daemon instance runs an HTTP server, bound to `0.0.0.0:60073` by default. Because `0.0.0.0` binds all interfaces, the first daemon claims port 60073 on every address — a second daemon can't reuse it.
The installer handles this. Install another daemon without asking for a specific port and it takes the next free one — 60074, 60075, and so on.
Set `--daemon-port` only when you need a *specific* port. A **ServerPoll** daemon is the case that matters: its port is the one the server dials, so the install command already carries it and the installer leaves it alone.
To pin a daemon's listener to one interface, give it that interface's IP with `--bind-address`:
```bash
sudo scanopy-daemon install --interfaces eth0 --bind-address 192.168.1.10
```
The same settings work as environment variables or in the config file:
```bash
# Environment variables
SCANOPY_BIND_ADDRESS=192.168.1.10
SCANOPY_DAEMON_PORT=60073
# config.json
{ "bind_address": "192.168.1.10", "daemon_port": 60073 }
```
## Setting up multiple daemons
Create a separate daemon entry in **Discover > Scan > Daemons** for each instance. Each is provisioned with its own API key and can target the same or different networks. During setup, use the **Advanced** tab to set that instance's interface restrictions, bind address, and port under Network Discovery — they're baked into the install artifact the wizard gives you, so you don't have to add them by hand.
Run each daemon's own install artifact on the host. Each run takes a fresh slot, so you can install several without them overwriting each other — no per-instance flags to remember:
```bash
# First daemon — takes the scanopy-daemon slot
sudo scanopy-daemon install --interfaces eth0
# Second daemon — takes the scanopy-daemon-2 slot automatically
sudo scanopy-daemon install --interfaces eth1
```
Each `install` registers a per-slot background service that starts immediately and on every reboot, and writes that slot's config. Manage each by its service — for example `sudo systemctl restart scanopy-daemon-2` (Linux) or `sc.exe stop scanopy-daemon-2` (Windows), where the service id matches the slot. On macOS the launchd label is `com.scanopy.daemon.` (for example `sudo launchctl kickstart -k system/com.scanopy.daemon.scanopy-daemon-2`). Run `scanopy-daemon list` to see each slot's exact service id. Interface, bind address, and port are persisted in each slot's config, so restarting or re-running `install` doesn't need them again.
To re-run against an *existing* slot — reconfigure it, or re-key it after rebuilding the host — pass `--instance `, where the selector is the daemon's name, its slot, its service id, or its daemon id (all shown by `scanopy-daemon list`). Without a selector on a multi-daemon host, a non-interactive install allocates a new slot, and an interactive one asks which to use. The reconfigure command the app generates already includes the right `--instance`.
### systemd template (Linux, manual)
Prefer `scanopy-daemon install` above — it registers a per-slot service for you. The systemd template below is the manual alternative, kept for older daemons or hand-managed setups.
Scanopy provides a [systemd template unit](https://github.com/scanopy/scanopy/blob/main/scanopy-daemon%40.service) for running multiple instances. Download and install it:
```bash
sudo curl -o /etc/systemd/system/scanopy-daemon@.service \
https://raw.githubusercontent.com/scanopy/scanopy/main/scanopy-daemon%40.service
sudo systemctl daemon-reload
```
The template uses the instance identifier as `--interfaces`. Each instance needs its own bind address and port — set these via environment overrides:
```ini
ExecStart=/usr/local/bin/scanopy-daemon --interfaces=%i
SyslogIdentifier=scanopy-daemon-%i
```
To configure the bind address and port per instance, create a systemd override:
```bash
sudo systemctl edit scanopy-daemon@eth0
```
```ini
[Service]
Environment="SCANOPY_BIND_ADDRESS=192.168.1.10"
Environment="SCANOPY_DAEMON_PORT=60073"
```
```bash
sudo systemctl edit scanopy-daemon@eth1
```
```ini
[Service]
Environment="SCANOPY_BIND_ADDRESS=10.0.0.10"
Environment="SCANOPY_DAEMON_PORT=60074"
```
Enable and start instances by appending the interface name after `@`:
```bash
sudo systemctl enable --now scanopy-daemon@eth0
sudo systemctl enable --now scanopy-daemon@eth1
```
Each instance gets separate journal logs:
```bash
journalctl -u scanopy-daemon@eth0
journalctl -u scanopy-daemon@eth1
```
The template ties the instance name to a single interface. If a daemon needs multiple interfaces, create a custom service file that passes `--interfaces eth0,eth1`.
## Listing installed daemons
To see which daemons are installed on a host — their slots, services, config paths, and IDs — run:
```bash
scanopy-daemon list
```
Each line is one installed slot. Use any of the values it prints as the `--instance` selector for `install` or the `--name` selector for `uninstall`.
## Upgrading and restarting
All daemon slots share the same binary. Download the latest binary once (the app's **Update** button provides the command), then restart each slot's service:
```bash
# Slots installed with `scanopy-daemon install`
sudo systemctl restart scanopy-daemon
sudo systemctl restart scanopy-daemon-2
# Legacy systemd-template instances (scanopy-daemon@): stop, replace the binary, start
sudo systemctl stop scanopy-daemon@eth0 scanopy-daemon@eth1
sudo systemctl start scanopy-daemon@eth0 scanopy-daemon@eth1
```
Configuration is preserved across upgrades — each slot reads its settings from its own config file on startup.
## Docker
For Docker deployments, run a separate container per daemon instance. Use the install commands from the app for each daemon entry and add the `SCANOPY_INTERFACES` environment variable to restrict each container to its assigned interface. Both containers need `network_mode: host` to access host interfaces.
# Managing Scan Targets
Source: https://scanopy.net/docs/guides/networking/scanning-remote-subnets/
## What a daemon scans by default
A daemon automatically discovers every subnet its host has a network interface on and adds them to its discovery. These **interfaced subnets** get fast Layer 2 (ARP) scanning. You can see a daemon's interfaced subnets on its card in **Discover > Scan > Daemons**.
## Viewing and editing scan targets
To see exactly which subnets a daemon will scan:
1. Go to **Discover > Scan > Scheduled**
2. Find the discovery for the daemon
3. The subnet list shows all targets for that discovery
You can add or remove subnets from this list to control what the daemon scans. Changes take effect on the next scan run.
## Adding a remote subnet
A daemon can scan subnets it can route to but doesn't have an interface on. Add them to the discovery's subnet list.
If the subnet doesn't exist in Scanopy yet (because no daemon has reported an interface on it), create it first:
1. Go to **Assets > Subnets**
2. Click **Create Subnet**
3. Enter the CIDR (e.g. `10.0.50.0/24`) and assign it to the appropriate network
4. Return to **Discover > Scan > Scheduled** and add it to the discovery
Remote subnets are scanned via Layer 3 (TCP probing) since the daemon has no local interface for ARP. This means hosts without open ports won't be found, and MAC addresses won't be collected. For full Layer 2 coverage, deploy a daemon on the target segment — see [Planning Daemon Deployment](https://scanopy.net/docs/setting-up-daemons/planning-daemon-deployment/).
**When to use remote scanning**:
* **Quick visibility into a remote segment** before deploying a dedicated daemon there
* **Small subnets with known services** where Layer 3 discovery is sufficient
* **Temporary scanning** of a network you're evaluating
For permanent monitoring of a subnet, deploy a daemon on the segment for full Layer 2 discovery. See [Planning Daemon Deployment](https://scanopy.net/docs/setting-up-daemons/planning-daemon-deployment/) to decide on your strategy.
## Removing subnets
To stop scanning a subnet, remove it from the discovery's subnet list in **Discover > Scan > Scheduled**. The daemon will skip it on the next run.
This is useful for:
* **Oversized subnets** (e.g. a /16 when only a /24 has hosts) — remove the large one and add the smaller target
* **Docker bridge networks** (e.g. 172.17.0.0/16) — these are handled by Docker container discovery, not network scanning
* **Decommissioned segments** — remove subnets that no longer have relevant hosts
# Scanning VPN Networks
Source: https://scanopy.net/docs/guides/networking/scanning-vpn-networks/
If the daemon's host is connected to a VPN, the daemon can scan that network. Scanopy works with **WireGuard**, **Tailscale**, **Headscale**, **OpenVPN**, and any other VPN that creates a network interface on the host.
## Setup
### 1. Connect the daemon's host to your VPN
Install and configure your VPN client on the machine running the daemon. Verify the VPN interface is up:
```bash
# You should see a VPN interface (e.g. wg0, tailscale0, tun0)
ip addr
```
### 2. Report the new interface to Scanopy
The daemon needs to detect the new VPN interface. Either:
* **Restart the daemon** — it reports interfaces on startup
* **Run the discovery** from **Discover > Scan > Scheduled** — the daemon re-scans its own interfaces and reports them to the server as part of each discovery run
### 3. Verify the VPN subnet appears
After the discovery completes, check **Assets > Subnets**. The VPN subnet should appear as an interfaced subnet for the daemon.
### 4. Configure scanning
* **If your discovery has no specific subnets configured** (the default), the VPN subnet is picked up and scanned automatically on the next run.
* **If you've configured specific subnets**, add the VPN subnet manually via **Discover > Scan > Scheduled**.
## What to expect
Since the daemon has a network interface on the VPN, it gets **Layer 2 access** — full ARP discovery, MAC addresses, and the ability to find hosts even without open ports. This is the same quality of discovery as a local network.
## Multiple VPN networks
If the daemon's host is connected to multiple VPNs, each VPN interface is detected and its subnet is scanned. No additional configuration is needed beyond connecting the VPN client.
# Configuring Single Sign-On (OIDC)
Source: https://scanopy.net/docs/guides/operations/oidc/
Scanopy supports OpenID Connect (OIDC) for enterprise authentication with providers like Authentik, Keycloak, Auth0, Okta, PocketID, and others.
## Quick Start
**Option A: TOML File (recommended for complex setups)**
1. Copy `oidc.toml.example` to `oidc.toml`
2. Configure your provider settings (see examples below)
3. Mount the file in docker-compose
4. Restart the server
**Option B: Environment Variable**
1. Set `SCANOPY_OIDC_PROVIDERS` with your provider config (see format below)
2. Restart the server
See [Environment Variable Configuration](#environment-variable-configuration) for details.
## Configuration File Format
```toml
[[oidc_providers]]
name = "Provider Name" # Display name in UI
slug = "provider-slug" # Used in callback URL (lowercase, no spaces)
logo = "https://..." # Optional: logo URL for UI
issuer_url = "https://..." # Provider's OIDC issuer URL
client_id = "your-client-id"
client_secret = "your-client-secret"
```
You can configure multiple providers by adding multiple \[\[oidc\_providers]] sections.
## Environment Variable Configuration
Instead of using a TOML file, you can configure OIDC providers using the `SCANOPY_OIDC_PROVIDERS` environment variable.
**Important**: This uses TOML-like syntax, NOT JSON. Use `key="value"` with equals signs, not `"key":"value"` with colons.
### Shell / .env Files
For shell scripts, `.env` files, or tools like Kamal that use shell-style secrets:
```bash
SCANOPY_OIDC_PROVIDERS='[{name="Authentik",slug="authentik",issuer_url="https://auth.example.com/application/o/scanopy",client_id="your-client-id",client_secret="your-client-secret"}]'
```
**Requirements:**
* Single quotes around the entire value
* No spaces after commas
* No newlines (must be a single line)
For multiple providers:
```bash
SCANOPY_OIDC_PROVIDERS='[{name="Authentik",slug="authentik",issuer_url="https://auth.example.com/application/o/scanopy",client_id="id1",client_secret="secret1"},{name="Keycloak",slug="keycloak",issuer_url="https://keycloak.example.com/realms/main",client_id="id2",client_secret="secret2"}]'
```
### YAML Files (Docker Compose, Kubernetes)
For YAML-based configuration, use the `>-` block scalar to handle the format cleanly:
```yaml
environment:
SCANOPY_OIDC_PROVIDERS: >-
[{name="Authentik",slug="authentik",issuer_url="https://auth.example.com/application/o/scanopy",client_id="your-client-id",client_secret="your-client-secret"}]
```
The `>-` folds the value into a single line and removes trailing newlines. You can also format it for readability:
```yaml
environment:
SCANOPY_OIDC_PROVIDERS: >-
[{
name="Authentik",
slug="authentik",
issuer_url="https://auth.example.com/application/o/scanopy",
client_id="your-client-id",
client_secret="your-client-secret"
}]
```
Use `>-` (with the hyphen), not just `>`. The hyphen strips trailing newlines which prevents parsing errors.
Each provider object supports these fields:
| Field | Required | Description |
| --------------- | -------- | ------------------------------------------ |
| `name` | Yes | Display name shown in UI |
| `slug` | Yes | URL-safe identifier (lowercase, no spaces) |
| `issuer_url` | Yes | OIDC provider's issuer URL |
| `client_id` | Yes | OAuth2 client ID |
| `client_secret` | Yes | OAuth2 client secret |
| `logo` | No | Logo URL for UI display |
### Docker Compose Example
```yaml
services:
scanopy-server:
image: ghcr.io/scanopy/scanopy/server:latest
environment:
SCANOPY_PUBLIC_URL: https://scanopy.example.com
SCANOPY_OIDC_PROVIDERS: >-
[{name="Authentik",slug="authentik",issuer_url="https://auth.example.com/application/o/scanopy",client_id="your-client-id",client_secret="your-client-secret"}]
```
This method is useful when:
* You want to keep all configuration in environment variables
* You're using container orchestration that injects secrets via env vars
* You prefer not to mount additional config files
**Note**: Environment variables take precedence over the TOML file if both are configured.
## Callback URL Format
Configure this URL in your OIDC provider's redirect/callback settings:
```
http://your-scanopy-domain:60072/api/auth/oidc/{slug}/callback
```
Replace `{slug}` with the slug value from your oidc.toml. For example, if `slug = "authentik"`:
```
http://scanopy.local:60072/api/auth/oidc/authentik/callback
```
**Required scopes**: `openid`, `email`, `profile` (profile is optional but recommended)
## Docker Compose Setup
Add the following volume mount to your `scanopy-server` service:
```yaml
services:
scanopy-server:
image: ghcr.io/scanopy/scanopy/server:latest
volumes:
- ./oidc.toml:/oidc.toml:ro
# ... rest of config
```
## Provider Examples
### Authentik
1. **Create Application** in Authentik Admin → Applications → Create:
* Name: `Scanopy`
* Slug: `scanopy`
* Provider: Create a new OAuth2/OpenID Provider
2. **Configure Provider**:
* Name: `Scanopy OIDC`
* Authorization flow: `default-provider-authorization-implicit-consent`
* Client type: `Confidential`
* Redirect URIs: `http://your-scanopy:60072/api/auth/oidc/authentik/callback`
* Copy the Client ID and Client Secret
3. **Find your Issuer URL**:
* Go to Providers → your provider → OpenID Configuration Issuer
* Usually: `https://auth.yourdomain.com/application/o/scanopy/`
* **Important**: Remove trailing slash if present (see Common Issues)
4. **Configure oidc.toml**:
```toml
[[oidc_providers]]
name = "Authentik"
slug = "authentik"
logo = "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/authentik.svg"
issuer_url = "https://auth.yourdomain.com/application/o/scanopy"
client_id = "your-client-id"
client_secret = "your-client-secret"
```
### Keycloak
1. **Create Client** in Keycloak Admin → Clients → Create:
* Client ID: `scanopy`
* Client type: `OpenID Connect`
* Client authentication: `On`
2. **Configure Client Settings**:
* Valid redirect URIs: `http://your-scanopy:60072/api/auth/oidc/keycloak/callback`
* Web origins: `http://your-scanopy:60072`
3. **Get Credentials**:
* Go to Credentials tab
* Copy Client Secret
4. **Configure oidc.toml**:
```toml
[[oidc_providers]]
name = "Keycloak"
slug = "keycloak"
logo = "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/keycloak.svg"
issuer_url = "https://keycloak.yourdomain.com/realms/your-realm"
client_id = "scanopy"
client_secret = "your-client-secret"
```
### PocketID
1. **Create OIDC Client** in PocketID:
* Go to OIDC Clients → Add Client
* Name: `Scanopy`
* Callback URLs: `http://your-scanopy:60072/api/auth/oidc/pocketid/callback`
2. **Copy Credentials**:
* Client ID
* Client Secret
3. **Configure oidc.toml**:
```toml
[[oidc_providers]]
name = "PocketID"
slug = "pocketid"
logo = "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/pocketid.svg"
issuer_url = "https://pocketid.yourdomain.com"
client_id = "your-client-id"
client_secret = "your-client-secret"
```
### Auth0
1. **Create Application** in Auth0 Dashboard → Applications → Create:
* Type: `Regular Web Application`
* Name: `Scanopy`
2. **Configure Application Settings**:
* Allowed Callback URLs: `http://your-scanopy:60072/api/auth/oidc/auth0/callback`
* Allowed Web Origins: `http://your-scanopy:60072`
3. **Configure oidc.toml**:
```toml
[[oidc_providers]]
name = "Auth0"
slug = "auth0"
logo = "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/auth0.svg"
issuer_url = "https://your-tenant.auth0.com"
client_id = "your-client-id"
client_secret = "your-client-secret"
```
### Okta
1. **Create App Integration** in Okta Admin → Applications → Create:
* Sign-in method: `OIDC - OpenID Connect`
* Application type: `Web Application`
2. **Configure Settings**:
* Sign-in redirect URIs: `http://your-scanopy:60072/api/auth/oidc/okta/callback`
* Sign-out redirect URIs: `http://your-scanopy:60072`
3. **Configure oidc.toml**:
```toml
[[oidc_providers]]
name = "Okta"
slug = "okta"
logo = "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/okta.svg"
issuer_url = "https://your-org.okta.com"
client_id = "your-client-id"
client_secret = "your-client-secret"
```
### Authelia
```yaml
identity_providers:
oidc:
clients:
- client_id: 'scanopy'
client_name: 'Scanopy'
client_secret: { { secret "/run/secrets/authelia_scanopy_oidc" | msquote } }
public: false
authorization_policy: 'two_factor'
require_pkce: false
pkce_challenge_method: ''
redirect_uris:
- 'https://scanopy.YOURDOMAIN/api/auth/oidc/authelia/callback'
scopes:
- 'openid'
- 'email'
- 'profile'
response_types:
- 'code'
grant_types:
- 'authorization_code'
access_token_signed_response_alg: 'none'
userinfo_signed_response_alg: 'none'
token_endpoint_auth_method: 'client_secret_basic'
consent_mode: 'auto'
pre_configured_consent_duration: '1M'
```
```toml
[[oidc_providers]]
name = "Authelia"
slug = "authelia"
logo = "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/authelia.svg"
issuer_url = "https://auth.YOURDOMAIN"
client_id = "scanopy"
client_secret = "YOURSUPERSECRET"
```
## Linking OIDC to Existing Accounts
If OIDC is enabled, users can link it to their existing email/password account:
1. Log in with your existing email and password
2. Go to **Account Settings** (click the user icon in top right)
3. Click **Link** under your OIDC provider
4. Complete the authentication flow with your provider
5. Your account is now linked — you can log in with either method
**Unlinking**: You can unlink OIDC at any time from Account Settings, but you'll need to have a password set first.
## Common Issues
### "Unexpected issuer URI" error
```
Failed to generate auth URL: Validation error: unexpected issuer URI
`https://auth.example.com/app/` (expected `https://auth.example.com/app`)
```
**Cause**: Trailing slash mismatch between your config and what the provider returns.
**Solution**: Try both with and without trailing slash in `issuer_url`. The value must exactly match what your provider returns in its `.well-known/openid-configuration`.
To check what your provider expects:
```bash
curl https://your-provider/.well-known/openid-configuration | jq .issuer
```
### "Invalid redirect URI" error
**Cause**: The callback URL in your provider doesn't match what Scanopy sends.
**Solution**: Ensure the redirect URI in your provider exactly matches:
```
http://your-scanopy:60072/api/auth/oidc/{slug}/callback
```
Common mistakes:
* Wrong protocol (http vs https)
* Wrong port
* Wrong slug (must match oidc.toml)
* Missing `/callback` at the end
### OIDC button not appearing in UI
**Causes**:
1. oidc.toml file not mounted in Docker (if using file-based config)
2. oidc.toml has syntax errors
3. `SCANOPY_OIDC_PROVIDERS` has invalid JSON (if using env var config)
4. Server not restarted after adding config
**Solution**:
1. If using TOML file: Verify the volume mount exists in docker-compose.yml and validate TOML syntax
2. If using env var: Validate JSON syntax — use `echo $SCANOPY_OIDC_PROVIDERS | jq .` to check
3. Restart with `docker compose restart scanopy-server`
4. Check server logs: `docker logs scanopy-server`
### "Connection refused" when authenticating
**Cause**: Scanopy server can't reach your OIDC provider.
**Solutions**:
1. Ensure the provider URL is reachable from the server container
2. If provider is internal, ensure Docker can resolve the hostname
3. Add provider to Docker's extra\_hosts if needed:
```yaml
extra_hosts:
- 'auth.internal:192.168.1.100'
```
# Export metrics to Prometheus
Source: https://scanopy.net/docs/guides/operations/prometheus-setup/
## Prerequisites
* Scanopy server running
* Prometheus 2.47+ or Grafana Alloy installed
## Configure Scanopy
First, generate a metrics token using `openssl rand -base64` or similar.
Set the metrics token in your Scanopy environment:
```yaml
SCANOPY_METRICS_TOKEN=your-secure-token-here
```
To restrict access to specific IPs or networks:
```yaml
SCANOPY_EXTERNAL_SERVICE_PROMETHEUS_ALLOWED_IPS=192.168.1.0/24,10.0.0.1
```
Restart Scanopy after changing environment variables.
## Configure Prometheus
Add a scrape job to your prometheus.yml:
```yaml
scrape_configs:
- job_name: 'scanopy'
metrics_path: /api/metrics
static_configs:
- targets: ['scanopy.example.com:60072']
authorization:
type: Bearer
credentials: your-secure-token-here
http_headers:
X-Service-Name:
values: [prometheus]
```
## Configure Grafana Alloy
Add a prometheus.scrape component to your Alloy configuration:
```
prometheus.scrape "scanopy" {
targets = [{
__address__ = "scanopy.example.com:60072",
}]
forward_to = [prometheus.remote_write.default.receiver]
metrics_path = "/api/metrics"
authorization {
type = "Bearer"
credentials = "your-secure-token-here"
}
http_headers = {
"X-Service-Name" = "prometheus",
}
}
```
# Share a topology link
Source: https://scanopy.net/docs/guides/operations/sharing-topology/
Share links let you give people outside your organization a read-only view of a network's topology. The recipient opens a link in their browser and sees the live topology — they can't sign in, edit anything, or change your data. Use this for handing a network map to a client, embedding a diagram in an internal wiki, or sharing read-only access with a colleague who doesn't have a Scanopy account.
For the API equivalent, see the [Shares API](https://scanopy.net/docs/api/shares/).
## Prerequisites
* A network with a topology to share
* A plan that includes share links — see [pricing](https://scanopy.net/pricing) for which plans include sharing, embedding, and export
## Creating a share
1. Open the network's topology and click the **Share** button in the toolbar
2. Click **Create Share**
3. Configure the share:
* **Name** — a label to identify this share
* **Views** — which perspectives the recipient can see (L2 Physical, L3 Logical, Workloads, Application). The first enabled view is what they land on
* **Password** *(optional)* — require a password before the topology loads
* **Expiration** *(optional)* — a date after which the link stops working
* **Allowed domains** *(optional)* — restrict where the topology can be embedded as an iframe
* **Display options** — toggle the inspect panel, zoom controls, export button, and minimap for the recipient
4. **Save** — the link is live immediately
## What the recipient sees
* A **read-only, live** view of the network limited to the views you enabled — it reflects current discovery data, not a [snapshot](https://scanopy.net/docs/using-scanopy/topology/snapshots/)
* No editing, tagging, or account access
* Only the controls you enabled (inspect panel, zoom, export, minimap)
* A password prompt first, if you set one
## Managing shares
From the same **Share** dialog you can edit a share's settings, disable it temporarily, or delete it. Disabling or deleting a share takes effect immediately — anyone with the link loses access.
Share links always show the **live** topology. To preserve a fixed point-in-time view, use a [snapshot](https://scanopy.net/docs/using-scanopy/topology/snapshots/) instead.
# Credentials
Source: https://scanopy.net/docs/setting-up-daemons/troubleshooting-scans/credentials/
**Symptoms**: You've configured a credential (SNMP or Docker Proxy) but it isn't being applied to hosts during discovery. The "Credential Mappings" summary at the end of a scan session shows 0 hosts for the credential type.
The daemon logs credential diagnostics at three points during a scan session. To access logs, see [Checking Daemon Logs](https://scanopy.net/docs/setting-up-daemons/troubleshooting-setup/#checking-daemon-logs).
## Credential summary at session start
At the beginning of each discovery session, the daemon logs a summary of all configured credentials:
```log
INFO Credentials:
INFO For Docker proxy connection on 192.168.4.126
INFO Port 2376
INFO SSL cert successfully read from /path/to/client-cert.pem
INFO SSL key successfully read from /path/to/client-key.pem
INFO SSL chain successfully read from /path/to/ca.pem
WARN For Docker proxy connection on 192.168.4.127
INFO Port 2376
ERROR SSL cert failed to read from /non-existent.pem
ERROR SSL key failed to read from /non-existent.pem
ERROR SSL chain failed to read from /non-existent.pem
INFO For SNMP queries on all scanned hosts
INFO Community ******** (10 chars)
INFO Version V2c
```
**What to look for**:
* **INFO** on all fields — credential loaded successfully and will be tried during the scan
* **ERROR on file fields** — the daemon couldn't read a certificate or key file. The credential will not be used. Fix the file path or permissions and restart the daemon.
* **WARN on the credential header** — at least one field has an error. Check the indented lines below for details.
For SNMP credentials, the community string is redacted but the character count is shown — useful for confirming the right credential was picked up.
## File read errors during the scan
When the daemon resolves credential file paths, it logs each read attempt. Errors appear as `ERROR` lines:
```log
ERROR Failed to resolve Docker credential file paths
error=Failed to read ssl_cert from /non-existent.pem: No such file or directory (os error 2)
ip=192.168.4.127
```
```log
ERROR Failed to resolve Docker credential file paths
error=SSL Certificate must contain a CERTIFICATE PEM block, found: PRIVATE KEY
ip=192.168.4.126
```
**Common file errors**:
| Error | Cause | Fix |
| ---------------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------- |
| `No such file or directory` | File path doesn't exist on the daemon host | Correct the path in the credential, or ensure the file is mounted/copied to the daemon host |
| `Permission denied` | Daemon process can't read the file | Fix file permissions (`chmod 644`) or run the daemon as a user with access |
| `must contain a CERTIFICATE PEM block, found: PRIVATE KEY` | Certificate and key files are swapped | Check which file contains `BEGIN CERTIFICATE` vs `BEGIN PRIVATE KEY` and swap the paths |
| `must contain a PRIVATE KEY PEM block, found: CERTIFICATE` | Same as above, opposite direction | Swap the cert and key file paths |
## Credential mappings at session end
At the end of each discovery session, the daemon logs how many hosts each credential type was successfully applied to:
```log
INFO Credential Mappings:
INFO SNMP: 12 hosts
INFO Docker: 2 hosts
```
If a credential type shows **0 hosts**, the credential was loaded but never successfully used during the scan. This means either:
1. **File/path errors** prevented the credential from loading — check the session start summary and file read logs above
2. **The credential was loaded but authentication failed** on every target host — see below
3. **The credential isn't assigned** to any network or host — see [Credential assignment](https://scanopy.net/docs/using-scanopy/credentials/#attaching-a-credential)
## Credential loaded but not assigned to hosts
A credential can load from disk without errors but still show 0 hosts in the mapping summary. Right after a daemon is installed this is normal: a host-targeted credential is assigned to a host only once a scan probes it there successfully, so it stays unassigned until the first scan runs. If it's still unassigned after a scan, the authentication or connection attempt is failing on the target device:
* **SNMP**: The credential was rejected by the device (wrong community string or SNMPv3 USM parameters, ACL blocking the daemon's IP, or that SNMP version disabled)
* **Docker Proxy**: The TLS handshake failed (expired cert, CA mismatch), the proxy refused the connection, or the proxy is blocking required API endpoints
**Diagnosis**: Look for per-host errors in the scan logs between the session start and session end summaries. SNMP failures appear as timeout or authentication errors; Docker failures appear as connection or TLS errors.
**Resolution**:
* For SNMP, verify the credential works from the daemon host using `snmpwalk` — see [SNMP troubleshooting](https://scanopy.net/docs/guides/integrations/snmp/#troubleshooting)
* For Docker Proxy, verify the proxy is reachable and the certificate is accepted — see [Docker Proxy troubleshooting](https://scanopy.net/docs/guides/integrations/docker/#troubleshooting)
## Quick reference
| Symptom | Where to look | Likely cause |
| -------------------------------------------------- | --------------------- | ---------------------------------------------- |
| ERROR in session start summary | Credential file paths | File missing or unreadable |
| "must contain a CERTIFICATE/PRIVATE KEY PEM block" | File read logs | Cert and key files swapped |
| Credential Mappings shows 0 hosts | Session end summary | Auth failed, or credential not assigned |
| Credential not listed at all | Session start summary | Credential not assigned to any network or host |
# Host Discovery
Source: https://scanopy.net/docs/setting-up-daemons/troubleshooting-scans/host-discovery/
**Symptoms**: Discovery completes but some known hosts are missing from results.
**Most likely causes**:
* **The host is not responding to ARP** — it's on a different subnet, asleep, filtering ARP, or on an overlay network.
* **Switch rate limiting (DAI)** — the switch is dropping ARP packets before they reach hosts or before responses return.
To tell them apart, run `arping -c 3 ` from the scanner host. If arping also fails, the host is unreachable at Layer 2 — see [Hosts Not Responding to ARP](#hosts-not-responding-to-arp). If arping succeeds but Scanopy misses the host, the issue is likely rate limiting — see [Switch Rate Limiting (DAI)](#switch-rate-limiting-dai).
## Hosts Not Responding to ARP
These issues are independent of Scanopy — the host is genuinely unreachable via Layer 2 ARP from the scanner's network segment. Manual `arping` will also fail.
### Host is on a different subnet or behind a router
ARP is Layer 2 only. Hosts on other subnets won't see broadcast ARP requests from the scanner's VLAN — routers don't forward ARP broadcasts.
**Solution**: Run a scanner daemon on each subnet, or add the remote subnet as a target so the daemon's routed-ping fallback can reach it.
### Host is asleep or in low-power mode
Laptops, IoT devices, and phones in sleep or Wi-Fi power-save mode may not respond to ARP promptly or at all.
**Solution**: Increase `arp_retries` to give sleeping hosts more chances to wake and respond. Schedule scans during active hours. Accept that sleeping devices will appear intermittently.
### Host has static ARP or ARP filtering enabled
Some hardened hosts or embedded devices ignore broadcast ARP or only reply to unicast ARP for known peers.
**Solution**: Increase `arp_retries` (unicast retries may succeed where broadcast failed). Verify with `arping -c 3 ` from the scanner host — if that also fails, the host is filtering ARP.
### Virtual machines or containers with restricted networking
VMs in NAT mode or containers on an overlay network are not reachable via Layer 2 ARP on the physical segment.
**Solution**: Scan from within the virtual network, or switch the VM/container to bridged networking so it appears on the physical segment.
## Switch Rate Limiting (DAI)
### Understanding ARP Scanning
Scanopy uses broadcast ARP for fast host discovery. For each target IP, we send an ARP "who-has" request and wait for a reply. This is faster and more reliable than TCP/UDP probing because hosts cannot firewall ARP and still communicate on the network.
However, many managed switches implement **Dynamic ARP Inspection (DAI)** to prevent ARP spoofing attacks. DAI rate-limits ARP packets on untrusted ports to a maximum number of packets per second (pps). When the limit is exceeded, the switch either drops excess packets silently or disables the port entirely.
**Known default rate limits:**
| Vendor | Default Limit | Exceeded Behavior | Documentation |
| ----------------------------------------------------- | ----------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| Cisco Catalyst/Nexus | 15 pps | Port enters errdisable | [Cisco DAI Guide](https://www.cisco.com/c/en/us/td/docs/switches/lan/catalyst4500/12-2/31sg/configuration/guide/conf/dynarp.html) |
| Dell OS9/OS10 | 15 pps | Interface error-disabled | [Dell DAI Config](https://www.dell.com/support/manuals/en-us/dell-emc-os-9/s4048-on-9.14.2.4-config/configuring-dynamic-arp-inspection-limit) |
| Juniper MX/SRX | \~150 kbps (\~2500 pps) | Packets dropped | [Juniper ARP Policer](https://www.juniper.net/documentation/us/en/software/junos/routing-policy/topics/concept/security-arp-policer-overview.html) |
| Juniper EX | No default limit | N/A (unless configured) | [Lindsay Hill](https://lkhill.com/juniper-arp-policer/) |
| Aruba, Ruckus, Extreme, Consumer Networking Equipment | Varies by model | Check switch docs | — |
If your switch has DAI enabled and Scanopy sends ARP faster than the limit allows, excess packets are dropped and those hosts appear offline.
### How Retries Work
Retries are targeted — each round narrows to the addresses that haven't answered:
1. **Round 1**: Send ARP request to all target IPs at the configured rate
2. **Wait**: Collect responses for 3 seconds
3. **Round 2**: Send ARP requests **only to IPs that didn't respond**
4. **Repeat**: Continue for the configured number of retry rounds
5. **Final wait**: Extra collection period for late-arriving responses
This approach minimizes total packets sent (only non-responders are retried) while giving slow or rate-limited hosts multiple chances to be discovered.
**Example with `arp_retries=2` (default) scanning a /24:**
| Round | Target IPs | Packets Sent | Cumulative |
| ----- | ------------------------ | ------------ | ---------- |
| 1 | 256 (all) | 256 | 256 |
| 2 | \~50 (no response) | \~50 | \~306 |
| 3 | \~10 (still no response) | \~10 | \~316 |
If rate limiting caused packet loss in round 1, rounds 2 and 3 retry only those hosts—often successfully since the burst is smaller.
### Diagnosis
1. **Check if your switch has DAI enabled.** Consult your switch documentation for how to view DAI configuration and statistics. Look for rate limit violations or dropped ARP packets.
2. **Check for port shutdown.** Some switches (notably Cisco) disable the port entirely when the rate limit is exceeded. If your scanner lost network connectivity during a scan, this is likely the cause.
3. **Test with manual arping:**
```bash
arping -c 3
```
If arping finds the host but Scanopy doesn't, rate limiting is likely the cause. If arping also fails, the host is unreachable at Layer 2 — see [Hosts Not Responding to ARP](#hosts-not-responding-to-arp) above.
### Solutions
**Option 1: Reduce ARP scan rate**
Lower `arp_rate_pps` to stay within your switch's DAI limit:
```bash
# CLI
scanopy-daemon --arp-rate-pps 15
# Environment variable
export SCANOPY_ARP_RATE_PPS=15
# Docker
environment:
- SCANOPY_ARP_RATE_PPS=15
```
**Recommended values:**
| Scenario | `arp_rate_pps` |
| ---------------------------- | -------------- |
| Cisco/Dell with DAI enabled | 10-15 |
| Unknown managed switch | 30-50 |
| DAI disabled or trusted port | 100-500 |
**Option 2: Increase retries**
If rate limiting causes occasional packet loss, more retries give hosts additional chances to respond:
```bash
# CLI
scanopy-daemon --arp-retries 4 # 5 total rounds (default is 2 = 3 rounds)
# Environment variable
export SCANOPY_ARP_RETRIES=4
```
Higher retries help when:
* Rate limiting drops some packets but not all
* Hosts are slow to respond (busy or sleeping devices)
* Network has high background traffic
The tradeoff is longer scan time, but since retries only target non-responders, the overhead is usually small.
**Option 3: Adjust your switch DAI configuration**
If you control the switch, you can increase the rate limit on the scanner's port or mark it as trusted (no rate limit). Consult your switch documentation for the specific commands.
**Option 4: Use Npcap for broadcast ARP (Windows only)**
On Windows, Scanopy defaults to the SendARP API which scans hosts sequentially. For faster scanning, you can enable Npcap broadcast ARP:
```bash
# CLI
scanopy-daemon --use-npcap-arp
# Environment variable
export SCANOPY_USE_NPCAP_ARP=true
# Docker
environment:
- SCANOPY_USE_NPCAP_ARP=true
```
**Requirements:**
* [Npcap](https://npcap.com/) must be installed (free for personal use)
* During Npcap installation, enable "WinPcap API-compatible Mode"
**When to use Npcap:**
* Scanning large subnets where sequential SendARP is too slow
* You need the same broadcast ARP behavior as Linux/macOS
**When to stick with SendARP (default):**
* Npcap is not installed or cannot be installed
* Scanning small networks where speed doesn't matter
* You want zero additional dependencies
### Tuning for Your Network
| Situation | Recommended Settings |
| -------------------------------------------- | -------------------------------------------------------- |
| Port disabled during scan (Cisco errdisable) | `arp_rate_pps=10`, check DAI config |
| Missing hosts | `arp_retries=3` or `arp_retries=4` |
| Scans are too slow | Increase `arp_rate_pps` if DAI allows, or trust the port |
See [Daemon Configuration](https://scanopy.net/docs/reference/daemon-configuration/) for all ARP settings.
# Overview
Source: https://scanopy.net/docs/setting-up-daemons/troubleshooting-scans/
Issues triggered by running scans, including network load, missing hosts, and scan performance.
### [Host Discovery](https://scanopy.net/docs/setting-up-daemons/troubleshooting-scans/host-discovery/)
Hosts missing from discovery results — ARP issues, switch rate limiting (DAI), Npcap, and tuning.
### [Scan Performance](https://scanopy.net/docs/setting-up-daemons/troubleshooting-scans/scan-performance/)
Scans crashing targets, taking hours, failing from high concurrency, or hitting file descriptor limits.
### [Credentials](https://scanopy.net/docs/setting-up-daemons/troubleshooting-scans/credentials/)
Credential file read failures, PEM errors, and credentials loading successfully but failing authentication on target devices.
## Topology Empty After Discovery
**Symptoms**: Discovery completes but topology shows nothing
**Check these**:
1. **Discovery errors**: Check **Discover > Scan > Sessions** for failures
2. **Network filter**: Check topology options panel — wrong network may be selected
3. **Snapshot vs live view**: Make sure the view selector is on **Live** — a selected snapshot shows a past capture, not current discovery data
4. **Service filters**: Category filters may be hiding everything
5. **Reachability**: Verify daemon can actually reach the target network
## Getting Help
If your issue isn't covered here:
* **Discord**: Join our [Discord community](https://discord.gg/b7ffQr8AcZ)
* **GitHub Issues**: [Open an issue](https://github.com/scanopy/scanopy/issues/new)
# Scan Performance
Source: https://scanopy.net/docs/setting-up-daemons/troubleshooting-scans/scan-performance/
## Port Scanning Slows Down or Crashes Target Hosts
**Symptoms**: Web interfaces on target hosts become unresponsive during scans; network feels slow; IoT devices or services with small listen backlogs (e.g. Pi-hole) stop responding; services require restart after scan.
**Cause**: The default port scan rate (500 pps) and batch size (200 ports) can overwhelm hosts with small TCP listen queues, especially on low-latency networks (e.g. LXC containers on the same Proxmox host).
**Solutions**:
Reduce `scan_rate_pps` and/or `port_scan_batch_size` in the discovery's **Speed** tab (**Discover > Scan > Scheduled** → edit discovery → Speed).
**Recommended values:**
| Scenario | `scan_rate_pps` | `port_scan_batch_size` |
| ------------------------------------------ | --------------- | ---------------------- |
| Sensitive network (IoT, Pi-hole, embedded) | 50–100 | 50–100 |
| Mixed IoT and servers | 100–200 | 100–150 |
| Robust servers only | 500 (default) | 200 (default) |
See [Daemon Configuration](https://scanopy.net/docs/reference/daemon-configuration/) for the full parameter reference.
## Discovery Takes Hours
**Symptoms**: Discovery takes hours to complete.
**Cause**: Two common reasons:
1. **Deep scanning**: Deep scans perform a full 65,535-port TCP scan on every host, which takes significantly longer than light scans. By default, every third scan is deep. If your network has many active hosts, deep scans can take hours. See [Light vs Deep Scanning](https://scanopy.net/docs/using-scanopy/discovery/scans/#deep-scans) for how scan intervals work and how to adjust or disable deep scans.
2. **Non-interfaced subnets**: Scanning subnets the daemon doesn't have a network interface on is orders of magnitude slower. On interfaced subnets, the daemon uses fast ARP scanning (seconds for a /24). On non-interfaced subnets, it falls back to TCP probing — scanning each IP individually. See [Layer 2 vs Layer 3](https://scanopy.net/docs/reference/limitations/#layer-2-vs-layer-3).
**Diagnosis**: Check the daemon's interfaced subnets on its card in **Discover > Daemons**, then compare to the subnets in the discovery under **Discover > Scan > Scheduled**. Any subnet not in the interfaced list is being scanned via slow TCP probing. See [Managing Scan Targets](https://scanopy.net/docs/guides/networking/scanning-remote-subnets/) for how to view and edit what a daemon scans.
**If the slow subnet is not interfaced**:
* **Deploy a daemon on that segment** for fast L2 scanning. See [Planning Daemon Deployment](https://scanopy.net/docs/setting-up-daemons/planning-daemon-deployment/).
* **Shrink the subnet** if it's larger than needed. A /16 is 65,536 IPs — if only a /24 within it has hosts, target that /24 instead.
* **Remove it** if you don't need visibility on that segment.
* **Docker networks** (e.g. 172.17.0.0/16) should not be scanned over the network. Docker container discovery queries the Docker API directly and takes seconds.
**If the slow subnet is interfaced**: The daemon should already be using ARP. If the subnet is reasonably sized, check whether the daemon's scan concurrency has been clamped by a low file descriptor limit — see ["Too Many Open Files"](#too-many-open-files-error). If the subnet is large (/16 or bigger), see [Large Interfaced Subnets](#large-interfaced-subnets) below.
## Large Interfaced Subnets
**Symptoms**: An interfaced subnet isn't fully scanned, or the daemon logs "ARP target list truncated".
**Cause**: To prevent excessive memory use and scan times, the daemon caps ARP targets per subnet at `arp_scan_cutoff` (default: /15, \~131,000 IPs). Subnets larger than this prefix are truncated — only the first \~131K IPs are ARP scanned.
**Solution**: Lower `arp_scan_cutoff` in the discovery's **Speed** tab to match your subnet size. **Important**: also increase `arp_rate_pps` to keep scan times reasonable.
**Scan time estimates at different rates:**
| Cutoff | IPs | At 50 pps (default) | At 500 pps | At 1000 pps |
| ------------- | ------ | ------------------- | --------------- | -------------- |
| /15 (default) | \~131K | \~44 min/round | \~4 min/round | \~2 min/round |
| /14 | \~262K | \~87 min/round | \~9 min/round | \~4 min/round |
| /12 | \~1M | \~5.8 hrs/round | \~35 min/round | \~17 min/round |
| /10 | \~4M | \~23 hrs/round | \~2.3 hrs/round | \~70 min/round |
Each scan performs up to 3 ARP rounds (1 initial + 2 retries by default). Subsequent rounds only target non-responsive hosts, so they're typically much faster than the first.
Before increasing `arp_rate_pps`, check whether your switch has Dynamic ARP Inspection (DAI) enabled. High ARP rates can trigger rate limiting or port shutdown on managed switches. See [Switch Rate Limiting (DAI)](https://scanopy.net/docs/setting-up-daemons/troubleshooting-scans/host-discovery/#switch-rate-limiting-dai).
## Discovery Finished with Hosts Unscanned
**Symptoms**: A run completes with a warning like `120 hosts not scanned (~35m remaining)`, and those hosts show stale data.
**Cause**: The run hit **Max Discovery Duration** — the hard ceiling on a single discovery run, 6 hours by default. Scanopy completes the run rather than failing it, keeps what it collected, and leaves the rest for the next run.
**Solutions**:
* **Fix the underlying slowness first**. Hitting a 6-hour ceiling usually means something above is wrong — non-interfaced subnets being TCP-probed, oversized targets, or deep scans on a large host count. Work through [Discovery Takes Hours](#discovery-takes-hours) before raising the limit.
* **Narrow the targets** so a run covers less ground, or **add a daemon** on the slow segment.
* **Raise the limit** in the discovery's **Detection** tab if your network genuinely needs longer than the default window.
If the warning appears on every run, the scan is not keeping up with its schedule and each run is starting from a further-behind position.
## "Too Many Open Files" Error
**Symptoms**: `Critical error scanning: Too many open files (os error 24)` or `CONCURRENT_SCANS is likely too high for this system` in daemon logs, or scans that are far slower than expected because only one host is scanned at a time.
**Cause**: The daemon's file descriptor limit is too low. Deep scanning a single host needs a few hundred descriptors, so a low limit forces concurrency down — sometimes to one host at a time.
On Unix, the daemon raises its own soft limit at startup (up to the hard limit, capped at 10,240), which handles the common case of macOS defaulting to 256. If you still see this, the **hard** limit is what needs raising.
**Solutions**:
1. **Check what the daemon actually has**:
```bash
ulimit -Hn # hard limit — the ceiling the daemon can raise itself to
ulimit -n # current soft limit
```
2. **Raise the hard limit** (add to `/etc/security/limits.conf`, then log out and back in):
```
* soft nofile 65535
* hard nofile 65535
```
On macOS, raise it with `launchctl limit maxfiles`.
3. **For Docker**, set it on the daemon container:
```yaml
ulimits:
nofile:
soft: 65535
hard: 65535
```
# Overview
Source: https://scanopy.net/docs/using-scanopy/discovery/
Daemons scan your network to discover hosts and services, then report their findings to the server.
## How Discovery Works
Each daemon runs a single discovery. Every run does two things unconditionally: it scans the subnets it has been given, and it reports back which subnets it has interfaces on.
### Network scanning
Scanning IP addresses on the configured subnets is what finds hosts in the first place. It:
* Detects open TCP and UDP ports
* Identifies services via [pattern matching](https://scanopy.net/docs/reference/service-detection/)
* Performs reverse DNS lookups
* Collects MAC addresses, on directly connected subnets only
The daemon can scan any subnet it can route to — if its host can reach an address, it can scan it. What differs is depth. On a subnet the daemon has an interface on, it gets Layer 2: MAC addresses via ARP, and every responsive host. On a remote subnet it is limited to Layer 3, finding only hosts with an open port. See [Layer 2 vs Layer 3](https://scanopy.net/docs/reference/limitations/#layer-2-vs-layer-3).
On very large interfaced subnets the daemon caps how many addresses it ARP scans, governed by the **ARP Scan Cutoff** [scan setting](https://scanopy.net/docs/using-scanopy/discovery/scans/#scan-settings).
### What credentials add
Everything beyond ports, names and addresses comes from a credential. Each one unlocks a different source:
A host reached through more than one source is still one host — everything flows into the same ingestion and matching path, so a device a controller reports and the same device found by a subnet scan are recognized as one.
## In this section
* **[Scans and Settings](https://scanopy.net/docs/using-scanopy/discovery/scans/)** — how often a scan runs, how deep it goes, and every setting that governs it
* **[Rescanning a Host](https://scanopy.net/docs/using-scanopy/discovery/rescanning/)** — re-checking one host without sweeping its subnet
* **[Naming and Deduplication](https://scanopy.net/docs/using-scanopy/discovery/naming-and-deduplication/)** — how a discovered host gets its name, and how Scanopy avoids creating it twice
# Naming and Deduplication
Source: https://scanopy.net/docs/using-scanopy/discovery/naming-and-deduplication/
## Host Naming
When a host is discovered, Scanopy determines its name using this priority:
1. **Reverse DNS** — hostname from PTR record, if available
2. **Best Service** or **IP** — configurable fallback per discovery:
* *Best Service*: Uses the first named service found on the host
* *IP*: Uses the host's IP address
3. **Remaining fallback** — whichever option wasn't selected in step 2
A name you set yourself is never overwritten by a later scan.
## Host Deduplication
When a daemon discovers a host, Scanopy checks if it already exists before creating a new entry. This prevents duplicate hosts when:
* The same host is discovered by multiple daemons
* A host is rediscovered across multiple scan sessions
* Docker and network scanning both find the same machine
Host deduplication is only applied to hosts on the same [Network](https://scanopy.net/docs/using-scanopy/organization/#networks). If two hosts on different Networks meet the criteria below, they will not be deduplicated.
### Interface Matching
Scanopy primarily identifies duplicate hosts by comparing network interfaces. Two interfaces are considered the same if **any** of these conditions are true:
| Condition | When it applies |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| **Same IP + Same Subnet** | Most common. If a host has `192.168.1.50` on subnet `192.168.1.0/24`, any discovery finding that same IP on that subnet matches. |
| **Same MAC Address** | Useful when IPs change (DHCP) but MAC stays constant. Both interfaces must have a MAC address set. |
MAC addresses come from ARP on directly connected subnets, or from container network configuration — see [Layer 2 vs Layer 3](https://scanopy.net/docs/reference/limitations/#layer-2-vs-layer-3).
**Floating virtual IPs (CARP/VRRP).** A high-availability VIP has no hardware of its own — its only identity is the address it floats on, carried by a shared virtual-router MAC. Scanopy matches it on IP + subnet, so the VIP is recognized across scans rather than recreated each time. The shared virtual MAC is never matched on, which keeps the HA peers hosting it distinct instead of merging them into one host.
### Container Host Matching
Container discovery reports on exactly one host — the daemon's own, or the one a proxy credential points at — so there is nothing to deduplicate across hosts. What it does change is *which* host the containers land on: a proxy pointing at a remote machine attributes them there, not to the daemon's host.
Container names appear in a host's virtualization metadata, not as its hostname.
### Upsert Behavior
When a duplicate is found, Scanopy merges the new discovery data into the existing host rather than creating a duplicate:
**Entities merged**
Ports, interfaces, and services discovered on the duplicate will be reconciled against the existing host and deduplicated.
* **Ports**: Must have same number and protocol to be considered a duplicate
* **Interfaces**: As described above, must have same IP + subnet OR same MAC address to be considered a duplicate
* **Services**: Follow complex deduplication logic that depends on the type of service detected. Refer to `backend/src/server/services/impl/base.rs -> impl PartialEq` if you are curious.
**Fields merged (only if not already set):**
* **Hostname** — DNS-discovered hostname fills in if the existing host has none
**Fields preserved (never overwritten):**
* **Name** — User-assigned name stays intact
* **Hidden status** — Visibility preference preserved
* **Tags** — User tags unchanged
* **Description** — User-provided description unchanged
**Always merged:**
* **Discovery metadata** — Each discovery source is recorded, so you can see which daemons and discovery methods found the host
* **Credential assignments** — When hosts are consolidated, credential assignments are preserved using broadest-scope-wins merging
This means discovery enriches existing hosts without overwriting user customizations.
### Manual Consolidation
Automatic deduplication handles most cases, but sometimes hosts slip through as duplicates — for example, when a server has interfaces on multiple VLANs that were scanned by different daemons before the interfaces could be matched.
Use manual consolidation to merge these. See [Consolidating Duplicate Hosts](https://scanopy.net/docs/using-scanopy/network-data/#consolidating-duplicate-hosts).
# Rescanning a Host
Source: https://scanopy.net/docs/using-scanopy/discovery/rescanning/
A full discovery sweeps every configured subnet. When the question is narrower — is this one host still there, and is what we know about it current? — use **Rescan** from a host's menu in **Assets > Hosts**.
A rescan checks only that host's known addresses. Alongside the standard discovery port set it re-checks the ports already recorded on the host, so it both confirms what was there and surfaces services that have opened since.
## Which daemon runs it
The daemon that last discovered the host, since that daemon has demonstrably reached the address before. If it can no longer reach the host, the rescan is refused — see [When it is refused](#when-it-is-refused).
## Where it shows up
In the Scans tab as an AdHoc run, with the same progress and cancel controls as any other scan. A `Queued` phase means the daemon is busy and will start when its current scan finishes.
## How to read the result
Where the daemon's interface has a MAC address, it ARPs the target, which sees a live host even when every port is firewalled. On an interface without one — a point-to-point VPN tunnel, say — it falls back to a TCP probe, which cannot tell a firewalled host from an absent one.
## When it is refused
Rescan is refused with the specific reason, when:
| Reason | What to do |
| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| The host has never been scanned, or the scan that found it is no longer on record | Run a discovery first |
| The daemon that last scanned it no longer exists | Run a discovery from another daemon to refresh the host |
| That daemon has moved to a different network | Scan the host from a daemon on its network |
| That daemon is older than 0.17.7 | [Upgrade the daemon](https://scanopy.net/docs/setting-up-daemons/managing-daemons/) |
| That daemon has no interface on a subnet holding any of the host's scannable addresses | Deploy a daemon that does — see [Planning Daemon Deployment](https://scanopy.net/docs/setting-up-daemons/planning-daemon-deployment/) |
# Scans and Settings
Source: https://scanopy.net/docs/using-scanopy/discovery/scans/
## Scanning Lifecycle
Discovery alternates between two scan depths on a configurable interval.
### Light scans
Light scans focus on host and service discovery using a targeted port set — the ports defined in the discovery's port configuration plus any credential-specific ports (e.g., SNMP port 161). This is faster and sufficient for detecting new hosts and tracking port changes.
### Deep scans
Deep scans perform a full 65,535-port TCP scan on each host. This catches open ports that aren't covered by the light scan's targeted port set.
### Scan interval
The **Full Scan Interval** setting controls how often deep scans run — a value of 3 means every third scan is deep, with light scans in between:
| Setting | Behavior |
| ------- | ------------------------------------------- |
| `1` | Every scan is deep |
| `3` | Light, light, deep, light, light, deep, ... |
| `0` | Never deep scan (light scans only) |
You can also force a deep scan at any time from the discovery session controls.
## Scan Settings
Everything that governs how a discovery scans, configured per-discovery rather than per-daemon, so one daemon can scan different networks at different rates:
## Run Types
| Type | Behavior |
| ------------- | ------------------------------------------------------ |
| **Scheduled** | Runs automatically on a cron schedule (default: daily) |
| **AdHoc** | Manual execution only, for testing or one-time scans |
## Discovery Duration
Duration depends primarily on the number of active hosts on the network, and will be estimated by the daemon during initial scan setup. You can view the estimated duration in **Scans > Sessions**
Factors affecting speed:
* Number of active hosts — more hosts means more deep scanning work
* Scan depth — deep scans take longer than light scans
* Network latency and host responsiveness
* Network prefix size — only a limiting factor without a Layer 2 interface, since the daemon must probe every IP individually via TCP
If scan time is longer than expected, see [discovery takes hours](https://scanopy.net/docs/setting-up-daemons/troubleshooting-scans/scan-performance/#discovery-takes-hours).
### Time limit
A single discovery run has a hard ceiling, set by **Max Discovery Duration** in [Scan Settings](#scan-settings).
A run that hits the ceiling completes rather than failing, keeping everything collected so far. Queued hosts are left for the next run, and the finished run warns how many were skipped and roughly how much time they needed.
Raise the limit for large networks that need a longer window. A run hitting it unexpectedly means the scan is slower than it should be — see [discovery finished with hosts unscanned](https://scanopy.net/docs/setting-up-daemons/troubleshooting-scans/scan-performance/#discovery-finished-with-hosts-unscanned).
# Application
Source: https://scanopy.net/docs/using-scanopy/topology/application/
The Application perspective shows your services grouped by application, with explicit dependencies as the connections between them. Generic infrastructure services like SSH, NTP, and DHCP are hidden by default so the view stays focused on the applications you actually care about.
## What you see
* **Containers**: Application groups — one per application tag you've defined
* **Nodes**: Services that belong to those applications
* **Connections**: [Dependencies](https://scanopy.net/docs/using-scanopy/dependencies) — request paths and hub-and-spoke relationships
## What drives it
Application groups are tags with the application flag set. A service joins a group either through its own tag or by inheriting from its host's tag. Connections come from dependencies you've created between services. The first time you open this perspective, the UI walks you through defining groups and assigning services.
Services that don't belong to any application group are collected in an **Ungrouped** container, which is collapsed by default so untagged services stay out of the way until you're ready to organize them.
## When to use it
* Understanding the shape of an application or stack
* Mapping request flow across services
* Communicating service relationships to people who don't think in subnets
See also: [Dependencies](https://scanopy.net/docs/using-scanopy/dependencies), [Customizing Views](https://scanopy.net/docs/using-scanopy/topology/customizing-views).
# Customizing Views
Source: https://scanopy.net/docs/using-scanopy/topology/customizing-views/
Each topology perspective can be customized with grouping rules, edge filters, and manual positioning to show exactly what you need.
## Grouping Rules
Grouping rules change how things are organized in your topology. Use the **Group by** controls in the toolbar to apply them. Each perspective has a fixed top-level container rule and a customizable set of element rules that organize nodes within those containers.
**Container rules** define what the main containers are:
* **Subnet** — L3 Logical's container rule
* **Application** — Application's container rule
* **Host** — L2 Physical and Workloads' container rule
* **Docker Bridges** — On L3 Logical, collapses Docker bridge subnets into their host
**Element rules** organize nodes within each container into sub-groups. Each rule applies to specific perspectives:
## Edge Filtering
Each perspective shows its own connection types, some by default and some as overlays you toggle on when you need the detail. On L3 Logical, for example, **Same Container** links the addresses of a container attached to several bridge subnets, and **Container Runtime** connects each bridge subnet to its host.
Clicking a connection highlights what it relates. A relationship edge such as Same Host lights up every node it ties together; a point-to-point edge highlights only its own segment.
## Metadata Filtering
Beyond tags, some perspectives offer metadata filter groups that fade everything except the elements you're interested in. The **By staleness** group (Current / Stale) narrows the view to entities discovery hasn't seen within their network's window — see [Staleness](https://scanopy.net/docs/using-scanopy/staleness/). Other groups include service category and, on Workloads, virtualization type. A filter group only appears when the entities in view actually differ on that dimension.
## Tag-Based Filtering
Filter your topology by tags to focus on what matters. Filtered elements fade to reduced opacity instead of disappearing, so the overall layout stays stable while your focus area stands out.
## Large topologies
Above a few hundred elements, a perspective changes how it opens so it stays usable:
* **Containers start collapsed**, and you expand the ones you're interested in. A container you expand stays expanded — the view never re-collapses it behind you.
* **Off-screen nodes aren't drawn while you pan.** Only what's in the viewport is rendered, which is what keeps panning smooth at that size.
Neither affects the diagram itself. Layout and export both work from the whole graph, so an exported image is never cropped to what happened to be on screen. Below those sizes nothing changes — small topologies open exactly as they always have.
## Positioning and Layout
* **Drag nodes** to reposition hosts and services (requires Edit Mode)
* **Drag container edges** to resize containers
* **Click connections** to show handles, then drag to reroute them
Your manual layout is preserved automatically as discovery continues.
# Overview
Source: https://scanopy.net/docs/using-scanopy/topology/
The topology generates an interactive visualization of your infrastructure, showing hosts, services, subnets, and their relationships. You can view your infrastructure from four different perspectives, each answering a different question.
## Perspectives
Each perspective shows a different angle of your infrastructure:
* **[L2 Physical](https://scanopy.net/docs/using-scanopy/topology/l2-physical)** — How is my network physically wired?
* **[L3 Logical](https://scanopy.net/docs/using-scanopy/topology/l3-logical)** — How is my network logically segmented?
* **[Workloads](https://scanopy.net/docs/using-scanopy/topology/workloads)** — Where does everything run, and what's inside what?
* **[Application](https://scanopy.net/docs/using-scanopy/topology/application)** — How do my services depend on each other?
Switch between perspectives using the perspective selector in the toolbar. Each perspective has its own grouping options and edge types.
## Live View and Snapshots
The **Live** view always reflects your current discovery data — as scans add, change, or remove entities, the topology updates to match. There's no manual rebuild step.
To preserve a point-in-time view of your infrastructure, capture a **snapshot**. Snapshots are read-only and don't change as discovery continues, so you can compare your network against a past state. See [Snapshots](https://scanopy.net/docs/using-scanopy/topology/snapshots/).
## Refining Your Topology
After initial discovery, these refinements make your topology more accurate across all perspectives.
### Assign Unclaimed Ports
Open hosts and go to **Services > Unclaimed Open Ports** to assign detected ports to the services that are actually listening on them.
See [Unclaimed Open Ports](https://scanopy.net/docs/using-scanopy/network-data/#unclaimed-open-ports).
### Consolidate Multi-NIC Hosts
Hosts detected across multiple VLANs can be merged into a single logical host.
See [Consolidating Duplicate Hosts](https://scanopy.net/docs/using-scanopy/network-data/#consolidating-duplicate-hosts).
## Edit Mode
Toggle **Edit Mode** in the toolbar to enable drag-to-reposition and resize. When edit mode is off, you can pan and zoom without accidentally moving things.
## Exporting
Click **Export** to download a PNG of your topology with all customizations and positioning intact. The file is named for the current perspective, network, and date — for example, `scanopy-app-home-network-2026-07-02.png`.
# L2 Physical
Source: https://scanopy.net/docs/using-scanopy/topology/l2-physical/
The L2 Physical perspective shows the cable-level shape of your network — which device is plugged into which port on which switch. It's built from LLDP and CDP neighbor data, so any device that reports neighbor information appears as a container in this view, with its interfaces and ports as nodes inside.
## What you see
* **Containers**: Devices that report LLDP/CDP neighbors — typically managed switches and routers
* **Nodes**: Their interfaces and ports
* **Connections**: Physical links, derived from each side's neighbor table
## What drives it
LLDP and CDP run on managed network gear. Scanopy reads those neighbor tables from two places:
* **The device itself**, over SNMP. When a host is queried with valid SNMP credentials, its neighbor table is pulled and used to draw links between interfaces.
* **A network controller**, over its API. Some managed switches collect neighbor data internally but don't publish it over SNMP — a walk of the LLDP MIB returns nothing no matter what credentials you use. Where the vendor's controller holds that data, reading the controller recovers it. See [Set up UniFi discovery](https://scanopy.net/docs/guides/integrations/unifi/).
Both sources feed the same resolution, so a link reported by a controller and the same link reported by the device at its other end are recognized as one cable rather than two.
Devices that don't report neighbor data from either source simply don't appear here — that's expected for unmanaged switches and most endpoints.
## When to use it
* Troubleshooting connectivity at the cable level
* Planning physical changes (port moves, recabling)
* Auditing port utilization and trunk configuration
See also: [Credentials](https://scanopy.net/docs/using-scanopy/credentials/), [Customizing Views](https://scanopy.net/docs/using-scanopy/topology/customizing-views).
# L3 Logical
Source: https://scanopy.net/docs/using-scanopy/topology/l3-logical/
The L3 Logical perspective shows where every host appears in your network's IP address space. Subnets are containers, and each host is shown inside every subnet it has an address on. When a host has addresses on multiple subnets, it bridges them — making it visible at a glance which devices route, span, or otherwise touch multiple network segments.
This is the default perspective and the most general-purpose view of your network.
## What you see
* **Containers**: Subnets — one per discovered network segment
* **Nodes**: Each host's presence in each subnet it has an address on
* **Connections**: Links between subnets where a single host has addresses on both, plus dependency overlays when configured with port-level detail
## What drives it
The L3 view is built from your discovered subnets and the IP addresses found on each host's interfaces. No manual setup is required — it appears as soon as discovery runs. [Dependencies](https://scanopy.net/docs/using-scanopy/dependencies) configured with port-level detail surface as overlay connections on top of the structural layout.
## Containers
Container hosts (Docker, Podman) attach their containers to one or more bridge networks, and each bridge network is a subnet on this view. A container that joins several bridges appears in **each** of those subnets, so you can see everywhere it's attached rather than in just one place.
Two overlays make container structure legible:
* **Container Runtime** connects each bridge subnet back to its host, showing which host runs the containers on that bridge.
* **Same Container** links the addresses of a single container that spans multiple bridge subnets, so a multi-attached container reads as one thing rather than as unrelated cards in separate subnet boxes. It's hidden by default — toggle it on when you need it.
The **Container Bridges** grouping rule collapses a host's bridge subnets into a single group when they'd otherwise clutter the view. See [Customizing Views](https://scanopy.net/docs/using-scanopy/topology/customizing-views/) for grouping rules and edge overlays.
## When to use it
* General-purpose situational awareness of the network
* Spotting which devices bridge multiple segments (routers, firewalls, multi-homed servers)
* Auditing which hosts live on which subnet
See also: [Hosts & Subnets](https://scanopy.net/docs/using-scanopy/network-data/), [Customizing Views](https://scanopy.net/docs/using-scanopy/topology/customizing-views).
# Snapshots
Source: https://scanopy.net/docs/using-scanopy/topology/snapshots/
A snapshot is a read-only, point-in-time copy of a network's topology — its hosts, services, ports, interfaces, subnets, VLANs, and tags, exactly as they were when you captured it. While the [Live view](https://scanopy.net/docs/using-scanopy/topology/#live-view-and-snapshots) keeps changing as discovery runs, a snapshot stays frozen, so you can see what your network looked like before a change, a migration, or an incident.
## Taking a Snapshot
Click **Take Snapshot** in the topology toolbar. The snapshot captures the network's current state immediately and is added to the snapshot selector.
Snapshots are captured manually — Scanopy does not take them on a schedule.
## Viewing a Snapshot
Use the snapshot selector in the toolbar to switch between **Live** and any saved snapshot. Selecting a snapshot shows the topology as it was at that moment; switching is instant and keeps your current layout and perspective.
While viewing a snapshot, the topology is read-only. You can:
* Inspect hosts, services, and their details
* Filter by tags and switch perspectives (L2 Physical, L3 Logical, Workloads, Application)
* Pan, zoom, and export
You can't edit while viewing a snapshot — tagging, dependency editing, grouping changes, edit mode, and taking new snapshots are only available on the Live view. Switch back to **Live** to make changes.
## Deleting a Snapshot
Select the snapshot and choose **Delete Snapshot**, then confirm. Deleting a snapshot doesn't affect your live topology or any other snapshot.
## Availability and Retention
Snapshots are available on paid plans, and each plan keeps snapshots for a set retention window — older snapshots are pruned automatically once they fall outside it. See [pricing](https://scanopy.net/pricing) for which plans include snapshots and how long each retains them.
Self-hosted deployments can override the retention window with the `SCANOPY_SNAPSHOT_RETENTION_DAYS_OVERRIDE` environment variable. See [Server Configuration](https://scanopy.net/docs/reference/server-configuration/).
# Workloads
Source: https://scanopy.net/docs/using-scanopy/topology/workloads/
The Workloads perspective shows your compute hierarchy — what runs where. Hosts contain hypervisors, hypervisors contain VMs, VMs can contain container runtimes, and container runtimes contain containers. The nesting itself shows the relationships, so this view has no connection lines.
A typical view might show:
```
bare-metal-host
└── Proxmox
├── vm-web-server
│ └── Docker
│ ├── nginx
│ ├── app
│ └── postgres
└── vm-monitoring
└── Docker
├── grafana
└── prometheus
```
## What you see
* **Containers**: Hosts, with hypervisor and container-runtime sub-containers nested inside
* **Nodes**: Managed workloads — VMs, containers, and other virtualized resources
* **Connections**: None. The nesting is the relationship.
## What drives it
The hierarchy is built from virtualization relationships on each entity: a VM points at the hypervisor service that runs it, and a container points at the container runtime service that runs it. Docker and Podman containers populate automatically when a Docker or Podman credential is configured. VMs and containers managed by Proxmox, vCenter, or ESXi are linked through the virtualization assignment UI, since network discovery alone doesn't tie them to their manager.
## When to use it
* Capacity and consolidation planning
* Tracing what's running on a given physical host
* Auditing virtualization sprawl
See also: [Virtualization Relationships](https://scanopy.net/docs/using-scanopy/network-data/#virtualization-relationships).