Nuclei is an open source vulnerability scanner from ProjectDiscovery. Instead of hardcoded checks, it runs YAML templates, and the community template repository contains thousands of them for known CVEs, misconfigurations, exposed admin panels, default credentials and leaked files. In this tutorial you will install Nuclei on Ubuntu 24.04, run safe, rate-limited scans against your own site, read the results, write a custom template and schedule a scan in GitHub Actions.

Prerequisites

To follow this guide you need:

  • A machine running Ubuntu 24.04 LTS (x86_64) to scan from, for example a CubePath VPS, with a non-root user that has sudo privileges.
  • At least 1 GB of free RAM; large scans across many hosts use more.
  • A website you own or are authorized to test, referred to as your_domain. Ideally scan a staging copy first.

Step 1 - Installing Nuclei

Nuclei is a single Go binary. The simplest reliable installation is the prebuilt release from GitHub, verified against its published checksums. Install the tools you need:

sudo apt update
sudo apt install curl unzip jq

Look up the latest release version and store it in a variable:

NUCLEI_VERSION=$(curl -fsSL https://api.github.com/repos/projectdiscovery/nuclei/releases/latest | jq -r .tag_name | sed 's/^v//')
echo "$NUCLEI_VERSION"
3.4.10

Download the Linux archive and the checksum file for that version into a temporary directory:

cd "$(mktemp -d)"
curl -fsSLO "https://github.com/projectdiscovery/nuclei/releases/download/v${NUCLEI_VERSION}/nuclei_${NUCLEI_VERSION}_linux_amd64.zip"
curl -fsSLO "https://github.com/projectdiscovery/nuclei/releases/download/v${NUCLEI_VERSION}/nuclei_${NUCLEI_VERSION}_checksums.txt"

Verify the archive before using it:

sha256sum --check --ignore-missing "nuclei_${NUCLEI_VERSION}_checksums.txt"
nuclei_3.4.10_linux_amd64.zip: OK

Extract it and install the binary system-wide:

unzip nuclei_*_linux_amd64.zip nuclei
sudo install -m 0755 nuclei /usr/local/bin/nuclei

On an ARM server, use the linux_arm64 archive instead.

Confirm the installation:

nuclei -version
[INF] Nuclei Engine Version: v3.4.10

Step 2 - Downloading the templates

Nuclei's checks live in the separate nuclei-templates repository. Download them into your home directory (Nuclei also does this automatically the first time it runs):

nuclei -update-templates
[INF] Successfully installed nuclei-templates at /home/your_user/nuclei-templates

The repository is organised by protocol and then by category. The directories you will use most are:

DirectoryContents
http/cves/Checks for specific CVEs, grouped by year
http/exposed-panels/Admin and login panels reachable from the internet
http/misconfiguration/Directory listings, debug pages, permissive CORS and similar
http/exposures/Leaked configuration files, backups, logs and keys
http/technologies/Fingerprinting of servers, frameworks and CMSs
ssl/Expired, self-signed or weak TLS configurations
network/Non-HTTP services such as Redis, FTP or SSH

You can list templates by tag without running them. For example, all templates tagged wordpress:

nuclei -tl -tags wordpress | head

Run nuclei -update-templates regularly (the scheduled job in Step 6 does it on every run) because new CVE templates are added every week.

Step 3 - Running a first scan

Start with a scan limited to high and critical findings and a modest request rate, so you do not overload the target or trip its firewall:

nuclei -u https://your_domain -severity high,critical -rate-limit 50 -stats

The -stats flag prints progress every few seconds. When a template matches, Nuclei prints one line per finding with the template ID, protocol, severity and matched URL:

[CVE-2023-XXXXX] [http] [critical] https://your_domain/vulnerable/path
[git-config] [http] [medium] https://your_domain/.git/config

If the scan ends with [INF] No results found. Better luck next time!, none of the selected templates matched.

Useful variations:

  • Technology fingerprinting only, fast and non-intrusive: nuclei -u https://your_domain -tags tech
  • Everything except informational templates: nuclei -u https://your_domain -exclude-severity info
  • Several targets from a file with one URL per line: nuclei -l targets.txt -severity medium,high,critical

By default Nuclei sends up to 150 requests per second (-rate-limit), runs 25 templates in parallel (-concurrency) against 25 hosts at a time (-bulk-size). For production sites, keep -rate-limit low and scan outside peak hours.

Step 4 - Saving and filtering results

For anything beyond a quick look, save the results in a machine-readable format. -jsonl writes one JSON object per finding:

nuclei -u https://your_domain -exclude-severity info -rate-limit 50 -jsonl -o results.jsonl

Summarise the findings by severity with jq:

jq -r '.info.severity' results.jsonl | sort | uniq -c
      2 low
      3 medium
      1 high

List only the high and critical findings with the matched URL:

jq -r 'select(.info.severity == "high" or .info.severity == "critical") | "\(.info.severity)\t\(.["template-id"])\t\(.["matched-at"])"' results.jsonl

To produce a report you can hand to a developer, export one Markdown file per finding:

nuclei -u https://your_domain -exclude-severity info -rate-limit 50 -markdown-export report/

Treat every finding as a lead, not a verdict. Open the matched URL, check the version or response yourself, and only then open a ticket.

Step 5 - Writing a custom template

Custom templates let you check for problems specific to your own applications, for example a debug endpoint that must never be public. Create a directory for them:

mkdir -p ~/custom-templates
nano ~/custom-templates/myapp-debug-endpoint.yaml

This template requests two paths and reports a finding only when the response has status 200 and contains a string that only your debug page produces:

id: myapp-debug-endpoint

info:
  name: MyApp debug endpoint exposed
  author: your_name
  severity: high
  description: The MyApp debug endpoint is reachable without authentication and leaks configuration values.
  tags: myapp,exposure,debug

http:
  - method: GET
    path:
      - "{{BaseURL}}/debug"
      - "{{BaseURL}}/_debug/info"

    stop-at-first-match: true
    matchers-condition: and
    matchers:
      - type: status
        status:
          - 200

      - type: word
        part: body
        words:
          - "MyApp debug console"

    extractors:
      - type: regex
        part: body
        name: app_version
        group: 1
        regex:
          - 'app_version=([0-9.]+)'

matchers-condition: and requires both matchers to succeed, which avoids false positives from sites that return 200 for every path. The extractor pulls the version string out of the page and adds it to the finding.

Check the syntax:

nuclei -validate -t ~/custom-templates/
[INF] All templates validated successfully

Then run only your templates against the target:

nuclei -u https://your_domain -t ~/custom-templates/

Keep custom templates in a Git repository next to your application so they are reviewed and versioned like code.

Step 6 - Scheduling scans in GitHub Actions

Running Nuclei on a schedule catches regressions such as a new deployment that exposes a file or a dependency that becomes vulnerable. The following workflow scans a staging URL every Monday and on manual request, uploads the results and fails when there is any critical finding. Store the target URL as a repository secret named SCAN_URL.

Create .github/workflows/nuclei.yml in your repository:

name: Nuclei scan

on:
  schedule:
    - cron: "0 3 * * 1"
  workflow_dispatch:

jobs:
  nuclei:
    runs-on: ubuntu-24.04
    env:
      NUCLEI_VERSION: "3.4.10"
    steps:
      - uses: actions/checkout@v4

      - name: Install Nuclei
        run: |
          curl -fsSLO "https://github.com/projectdiscovery/nuclei/releases/download/v${NUCLEI_VERSION}/nuclei_${NUCLEI_VERSION}_linux_amd64.zip"
          curl -fsSLO "https://github.com/projectdiscovery/nuclei/releases/download/v${NUCLEI_VERSION}/nuclei_${NUCLEI_VERSION}_checksums.txt"
          sha256sum --check --ignore-missing "nuclei_${NUCLEI_VERSION}_checksums.txt"
          unzip -q "nuclei_${NUCLEI_VERSION}_linux_amd64.zip" nuclei
          sudo install -m 0755 nuclei /usr/local/bin/nuclei

      - name: Update templates
        run: nuclei -update-templates

      - name: Scan
        run: |
          nuclei -u "${{ secrets.SCAN_URL }}" \
            -exclude-severity info \
            -rate-limit 20 \
            -jsonl -o nuclei-results.jsonl \
            -silent
          touch nuclei-results.jsonl

      - name: Upload results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: nuclei-results
          path: nuclei-results.jsonl

      - name: Fail on critical findings
        run: |
          critical=$(jq -s '[.[] | select(.info.severity == "critical")] | length' nuclei-results.jsonl)
          echo "Critical findings: ${critical}"
          test "${critical}" -eq 0

Set NUCLEI_VERSION to the version you installed in Step 1. Pinning it keeps the job reproducible; update it deliberately when a new release comes out. If your repository contains custom templates, add -t custom-templates/ to the scan step, or run them in a second step so the community templates keep running too.

Trigger the workflow once from the Actions tab with Run workflow and check that the nuclei-results artifact appears.

Troubleshooting

nuclei -update-templates fails or hangs. The scanner downloads templates from GitHub. Check outbound HTTPS with curl -I https://github.com. Behind a proxy, set HTTPS_PROXY or pass -proxy http://proxy_host:port.

The scan is very slow. Large template sets against slow sites take time. Narrow the scope with -tags or -severity, and raise -concurrency only if the target can take it. Headless templates are skipped unless you add -headless, which is much slower.

Many requests are blocked or return 403. A WAF or rate limiter on the target is doing its job. Lower -rate-limit, scan from an IP the WAF allows, or scan the origin server directly on a staging environment.

A finding looks wrong. Reproduce it with curl -i against the matched URL. If the template is at fault, exclude it with -exclude-id template-id and report it in the nuclei-templates issue tracker.

Conclusion

You installed a verified Nuclei binary, downloaded the community templates, ran rate-limited scans, filtered the JSONL results with jq, wrote a template for your own application and scheduled weekly scans in GitHub Actions. Automated scanning finds known issues quickly but does not replace a code review or a manual penetration test. As next steps, keep a targets.txt inventory of all your public hostnames, add custom templates for every incident you fix so it cannot silently return, and review the Markdown reports with the teams that own each service.