Running Greenbone/OpenVAS in Docker: What Actually Happens on First Run
In another post I got an intentionally vulnerable box running on Proxmox. The obvious next step is to point a scanner at it.
OpenVAS now ships as the Greenbone Community Edition, and Greenbone publishes an official Docker Compose stack for it. The install is a handful of commands. The confusing part is the first hour after docker compose up: dozens of containers, walls of logs, a crash or two, and a web UI that loads before it's really ready.
This post is the guide I wanted while staring at those logs. It covers the setup, but mostly it covers how to tell what's normal.
Environment I used:
- Ubuntu-based homelab box running Docker Engine with the
docker composeplugin - The same box was also running Ollama with a local LLM loaded (this matters, see below)
- Greenbone Community Edition, current compose file from Greenbone's docs
Greenbone's stated requirements are 2 CPU cores, 4 GB RAM, and 20 GB of disk at minimum. They recommend 4 cores, 8 GB, and 60 GB. Take the recommended numbers seriously, and treat them as what Greenbone needs for itself.
1. Get the right compose file
A lot of older blog posts, and some of my own notes, point at a docker-compose-22.4.yml URL. That file is gone and returns a 404. The current file is just compose.yaml:
export DOWNLOAD_DIR=$HOME/greenbone-community-container
mkdir -p $DOWNLOAD_DIR
curl -f -O -L https://greenbone.github.io/docs/latest/_static/compose.yaml \
--output-dir "$DOWNLOAD_DIR"
Use -f. Without it, curl happily saves a 404 error page as compose.yaml and exits successfully. You only find out when Docker fails to parse an HTML page. With -f, curl fails loudly on any HTTP error.
Sanity-check what you downloaded before going further:
head -5 $DOWNLOAD_DIR/compose.yaml # should be YAML, not <html>
Also watch out for running the setup commands twice from inside the directory you just created. That's an easy way to end up with greenbone-community-container/greenbone-community-container/compose.yaml and two half-configured stacks. Pick one directory and stay in it.
2. Start the stack
docker compose -f $DOWNLOAD_DIR/compose.yaml pull
docker compose -f $DOWNLOAD_DIR/compose.yaml up -d
The project name (greenbone-community-edition) is set inside the compose file, so you don't need -p.
Now follow the logs. This is where the next hour happens:
docker compose -f $DOWNLOAD_DIR/compose.yaml logs -f
To cut down on noise, watch only the management daemon:
docker compose -f $DOWNLOAD_DIR/compose.yaml logs -f gvmd
3. Understand the two feeds
This is the single most useful thing to know about first run. Greenbone loads two separate kinds of data, and they finish at very different times.
| Feed | What it is | Used by | Done when you see |
|---|---|---|---|
| VTs (NVTs) | The actual vulnerability tests | openvas-scanner / ospd-openvas |
Finished loading VTs |
| SCAP / CERT | CVE, CPE, and CVSS reference data | gvmd |
SCAP sync reports success, Feed Status shows current |
The VT feed loads relatively quickly. The SCAP sync is the long one. gvmd works through CPE match data, then every yearly NVD CVE file from 2002 to the present, then CVSS scoring. Expect 30 minutes or more, depending on disk and RAM.
What that means in practice:
- The web UI is usable before SCAP finishes. You can log in, create targets and credentials, and even start scans.
- Scan results aren't fully trustworthy until SCAP finishes. A scan can detect "Apache 2.4.49" but can't map it to CVEs if that year's CVE data hasn't loaded yet. Dashboard widgets based on CVE counts will look empty or wrong.
My rule: poke around freely, but don't trust any severity numbers until Administration → Feed Status shows every feed as current.
4. "Is it stuck?" How to read gvmd's logs
The SCAP sync is quiet. It can go minutes without a log line while it chews through a big file, which looks exactly like a hang.
Check what it's doing over a recent window:
docker compose -f $DOWNLOAD_DIR/compose.yaml logs gvmd --since 3m
You're looking for forward progress in the timestamps and filenames. A healthy sync moves through stages like this:
Updating CPE match strings ...
Updating CVEs ... nvdcve-2.0-2021.json.gz
Updating CVEs ... nvdcve-2.0-2022.json.gz
Updating CVEs ... nvdcve-2.0-2023.json.gz
...
Updating CVSS scores ...
If the yearly CVE filename keeps advancing every few minutes, it's working. Leave it alone.
Don't try to force it. I tried --rebuild-scap while I thought it was stuck and got this:
SCAP sync is currently running
That error is actually good news. It means a sync is in progress, not hung. If you truly see no new log lines and no CPU activity from the gvmd container for a long stretch (docker stats), then restart just that container:
docker compose -f $DOWNLOAD_DIR/compose.yaml restart gvmd
Feed data lives in volumes, so a restart loses nothing.
5. The gvmd crash during CPE matching (RAM contention)
Partway through my first sync, gvmd hit this, with a full backtrace:
md manage:MESSAGE:... Received Aborted signal
It happened while parsing nvd-cpe-matches.json.gz, which is one of the largest files in the SCAP feed. The cause was memory pressure. The same box was running Ollama with a local model loaded, and between the model and the Greenbone stack (Postgres, Redis, the scanner, and gvmd parsing a huge JSON file) there wasn't enough RAM to go around.
gvmd restarted itself and resumed the sync a few seconds later. But a crash mid-parse is exactly when I'd stop trusting the data, so I killed Ollama and let the sync run without competition.
If you see a SIGABRT, check:
docker compose -f $DOWNLOAD_DIR/compose.yaml ps # restart counts on gvmd?
free -h
docker stats --no-stream
sudo dmesg | grep -i "out of memory"
The fix: Give Greenbone the box to itself during first sync. That means no LLM inference, no big builds, nothing memory-hungry. After the initial sync, the daily incremental updates are much lighter. Even then, I'd avoid scheduling feed syncs or large scans at the same time as heavy workloads. If you can't free up RAM, add swap.
6. Change the admin password (and use -u gvmd)
The default login is admin / admin. Change it right away:
docker compose -f $DOWNLOAD_DIR/compose.yaml \
exec -u gvmd gvmd gvmd --user=admin --new-password='your-new-password'
Two details matter here:
-u gvmdis required. Without it, the command runs as root inside the container and fails with semaphore permission errors. That error has nothing to do with your password, which makes it confusing.- Use single quotes around the password if it contains
$or other characters your shell would interpret.
You'll also probably see this line in the startup logs:
Failed to create user: Invalid characters in user name
It's from the container's bootstrap logic, not something you did. Ignore it.
7. Make the web UI reachable from your network
The web UI (GSA, served through nginx) is at https://127.0.0.1 on port 443, not port 9392 as many older guides say.
Notice 127.0.0.1. The compose file binds nginx to localhost only, which is a sensible default. If Greenbone runs on a homelab server and you browse from your laptop, you won't reach it until you change that binding.
Find the nginx service in compose.yaml. Its port mapping looks like this:
nginx:
ports:
- 127.0.0.1:443:443
Before changing it, check that nothing else on the host already owns 443, like a reverse proxy:
sudo ss -tlnp | grep ':443'
Then pick one of these:
- 443:443 # all interfaces
- 192.168.1.50:443:443 # only the host's LAN IP (better)
- 8443:443 # if 443 is already taken on the host
Apply the change:
docker compose -f $DOWNLOAD_DIR/compose.yaml up -d nginx
Then browse to https://<your-host>/ and accept the self-signed certificate.
A word of caution: once it's on the network, the only thing between your LAN and a vulnerability scanner is the admin password you just set. On a homelab network behind your own firewall that's usually fine. Just make it a decision rather than an accident. Binding to a specific LAN IP, or leaving it on localhost and using an SSH tunnel (ssh -L 8443:127.0.0.1:443 you@host), are both reasonable choices.
Bonus: talking to gvmd from scripts (GMP over TCP)
If you want to drive scans from code instead of the web UI, the interface is GMP (Greenbone Management Protocol). In the container stack, gvmd only listens on a Unix socket. The simplest way to expose it over TCP is a small socat sidecar added to compose.yaml:
gmp-proxy:
image: alpine/socat
restart: on-failure
ports:
- 127.0.0.1:9390:9390
volumes:
- gvmd_socket_vol:/run/gvmd
command: TCP-LISTEN:9390,fork,reuseaddr UNIX-CONNECT:/run/gvmd/gvmd.sock
depends_on:
- gvmd
Quick test:
printf '<get_version/>' | nc localhost 9390
Two gotchas once you start scripting against it:
- GMP returns 10 rows by default. Add
filter="rows=-1"to list queries, or your results get silently truncated. - A timeout isn't necessarily a failure. On long operations, reconnect and check whether the target or task was actually created before you retry, or you'll end up with duplicates.
Raw GMP has no TLS and authenticates per-session over plain TCP, so keep this port bound to localhost unless you know why you're exposing it.
First-run checklist
- Download
compose.yamlwithcurl -fand confirm it's YAML. pull, thenup -d, from one directory.- Free up RAM. Nothing heavy runs alongside the first sync.
- Wait for
Finished loading VTs(the scanner is ready). - Watch
gvmdadvance through the yearly CVE files. Slow is normal; don't run--rebuild-scap. - Change the admin password with
exec -u gvmd. - Rebind nginx if you need network access, deliberately.
- Trust severity results only after Feed Status shows everything current.
Then point it at your Metasploitable3 VM and enjoy the wall of red.
References
- Greenbone Community Edition docs: Greenbone Community Containers
- Current compose file: compose.yaml