One Go binary and a SQLite file: vuln management without the Docker tax

Part of my series on RiskRancher, the open-source vulnerability manager I build. Source: github.com/Kuebiko-LLC/risk-rancher-core.

Intro

Most vulnerability-management platforms want a lot from you before they do anything useful. Stand up Postgres, add Redis, wire up a Docker Compose file, maybe a Kubernetes cluster if you’re feeling ambitious, and then you can start storing scanner output. I got tired of that, so I built RiskRancher to be the opposite: one Go binary and a SQLite file. Download it, run it, open your browser. In this post I want to explain that choice, because “boring and self-contained” turned out to be the whole product.

I. The pitch, in one command

Here’s the entire install:

chmod +x rr-linux-amd64
./rr-linux-amd64
# open http://localhost:8080

That’s it. There’s no database to provision, no services to orchestrate, nothing to docker compose up. The binary is the app, and it keeps its data in a SQLite file right next to it. I describe it as “DefectDojo without the Docker tax,” and that tax is real: the time and ops burden of running the stack is often more than the time spent actually triaging findings.

II. What the binary actually does

The whole server starts from a main that you can read in one sitting:

func main() {
    db := datastore.InitDB("./data/RiskRancher.db")
    defer db.Close()

    store := datastore.NewSQLiteStore(db)
    app := server.NewApp(store)
    server.RegisterRoutes(app)

    log.Println("RiskRancher Core running on http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", app.Router))
}

It opens a SQLite file, wraps it in a store, builds the app, registers the routes, and listens. The UI is embedded in the binary, so there are no static files to deploy alongside it. One file on disk is the application, the web server, and the admin console all at once.

III. Why SQLite is the right call here

People hear “SQLite” and think “toy.” For this workload it’s the opposite of a compromise. A vulnerability manager is mostly one team, writing and reading findings on one server. That is exactly the shape SQLite is best at: a single writer, lots of reads, all local. There’s no network hop to a database, no connection pool to tune, and no second thing that can be down while your app is up.

And it makes the operational story trivial. Backups are copying a file. Moving to a new server is copying a file. Giving someone a demo is handing them a file. The database isn’t infrastructure you manage, it’s just part of the app.

IV. The constraint that paid off

Choosing one binary and SQLite wasn’t only about convenience. It forced a discipline I’m glad I kept: everything the product needs has to fit inside that binary, and everything it stores has to fit in that file. No reaching for a queue, a cache, or a second datastore to paper over a design problem. When something felt like it needed Redis, that was usually a sign the design was wrong, not that I needed Redis.

That constraint is also what makes the air-gapped story possible, which I’ll get into in another post. You can’t accidentally depend on a cloud service when the whole app is one file you carry onto the server yourself.

Conclusion

The best thing about RiskRancher is also the least flashy: you run it by downloading a file and double-clicking it. One Go binary, one SQLite file, no stack to stand up first. For a tool whose entire job is to reduce busywork, making the tool itself zero-busywork to run felt like the point. If you’re building something internal and you’re reaching for Docker Compose out of habit, ask whether a single binary and SQLite would do. For a surprising amount of software, it would. Thanks for reading, and may your installs stay boring.

cd ../blog