Self-hosted code analysis
Run the Sonda server on your own machine with Docker Compose — architecture, installation, configuration, exposing it safely, security, upgrades and backups.
The Sonda CLI gates code on its own. The server adds memory: it keeps every analysis, tracks issues across them, and serves the web app — trends, triage, component owners, security review and quality gates — and can post results on GitHub pull requests. This guide runs it on your own machine.
Why self-host, and what it costs
What you get: source code and findings stay on infrastructure you control. The server makes no outbound connections by default. You choose when to upgrade, and you can read every line of what you're running.
What it costs: you run it. That means a machine, backups, upgrades, TLS if people reach it over a network, and being the one who notices when it's down.
Architecture
developer / CI ── sonda scan --upload ──▶ sonda (server + web app) ──▶ db (PostgreSQL)
HTTPS, or HTTP to 127.0.0.1:9000
localhost only- Two containers:
sonda, which is the server and serves the web app, anddb, PostgreSQL 15 or newer. - Uploads:
sonda scan --uploadorsonda uploadsends the JSON report and the analyzed sources. The server checks everything it receives: each source must match the report's SHA-256, paths and rule ids are validated, and each upload is one transaction. - Data: everything durable is in PostgreSQL, in the
pgdatavolume. - Outbound connections: none by default. Advisory sync (
SONDA_ADVISORY_SYNC=true) fetches OSV data daily, and a GitHub App lets it post on pull requests. Both are opt-in.
Requirements
- A machine with Docker, Docker Compose and git.
- Port 9000 free on
127.0.0.1, or another port with--port.
Install
From a checkout of a release:
git clone --branch v0.1.0 https://github.com/lvaoliveira/sonda.git && cd sonda
scripts/setup-server.shThe script creates the secrets once (database password, API token, first admin password) in secrets/, a git-ignored
directory only you can read, builds the checkout, starts both containers and waits until the server is healthy. It's
safe to rerun.
Open http://127.0.0.1:9000 and sign in as admin, with the password in secrets/admin_password. Change it under
Account, and add other users under Administration.
Check health from the command line:
curl http://127.0.0.1:9000/api/healthUpload the first analysis
From any repository on the same machine:
export SONDA_TOKEN_FILE=/path/to/sonda/secrets/sonda_token
sonda scan . --upload --server http://127.0.0.1:9000 \
--project my-project --branch main --commit "$(git rev-parse HEAD)"The first upload creates the project. For CI, either connect the repository from the web app or follow the GitHub Actions guide.
Configuration
scripts/setup-server.sh keeps its options in .env, so later runs need none:
| Option | What it does |
|---|---|
--port PORT |
Publish on 127.0.0.1:PORT instead of 9000 |
--github-app ID KEY.pem |
Post results on GitHub pull requests with this GitHub App |
--lan [IP] |
Also serve plain HTTP on this machine's LAN address, for a trusted network |
--https [IP][:PORT] |
Also serve HTTPS on the LAN address (port 9443 by default), with Caddy |
--public-url URL |
The server's address in pull request links |
The server itself reads environment variables, and secrets from files (*_FILE), never from command-line flags. A
few worth knowing:
| Variable | Meaning |
|---|---|
SONDA_ADVISORY_SYNC |
true syncs vulnerability advisories from OSV daily. Off by default |
SONDA_METRICS_LISTEN |
Serve Prometheus metrics on this address. Unauthenticated: keep it private |
SONDA_LOG_FORMAT |
json for one JSON object per log line |
SONDA_PUBLIC_URL |
The address people use, for links in pull request results |
The full list is in docs/server.md.
Reaching it from other machines
The server publishes only on 127.0.0.1. To reach it from elsewhere:
- A reverse proxy with TLS (Caddy, nginx, a cloud load balancer) in front of
127.0.0.1:9000, or an SSH tunnel:ssh -L 9000:127.0.0.1:9000 host. - HTTPS on your LAN with
--https: Caddy serves the LAN address with a certificate from its own local authority, which you install once on each device. Treat thecaddy_datavolume like a password: a device that trusts that root trusts anything it signs. - Plain HTTP on a trusted LAN with
--lan. Passwords and session cookies then cross the network unencrypted, so use it only where you trust everyone on the network.
Security considerations
- Not exposed by default. Off loopback, every read needs a token or a browser session.
- Passwords are stored as Argon2id hashes; tokens are stored as SHA-256 hashes.
- The CLI protects its token: it reads it from
SONDA_TOKENor a file, never a flag; sends it only over HTTPS, except tolocalhost; and doesn't follow redirects. - Private certificate authorities: the CLI trusts Mozilla's roots only, so uploads to a server behind a private CA
(Caddy's local one included) fail from other machines. Upload on the server's own machine to
127.0.0.1. - Uploads are untrusted: they carry other people's source code. Bodies are size-capped, decompression is capped, and a rejected upload leaves nothing behind.
SECURITY.md has the full threat model.
Maintenance
Upgrade to a new release. Back up first: there's no downgrade.
git fetch --tags && git checkout v0.2.0 # the release you're upgrading to
scripts/setup-server.shMigrations run at startup, each in a transaction. If one fails, the server exits instead of serving.
Back up the database:
docker compose exec -T db pg_dump -U sonda -Fc sonda > sonda.dumpStop with docker compose down. Data survives in the pgdata volume; docker compose down -v deletes it.
Troubleshooting
- Port 9000 is taken. Rerun
scripts/setup-server.sh --port 9100. - The page loads but sign-in fails on an address other than
localhost. The session cookie needs HTTPS there: use a reverse proxy or--https, or--lanon a trusted network. sonda uploadrefuses the server URL. Plainhttp://is allowed only tolocalhost; use HTTPS.- Uploads from another machine fail with a certificate error. The server uses a private CA; see the security section above.
- Something failed and a user saw an error. Every response carries an
x-request-idheader; search the logs for it withdocker compose logs sonda.