Guide

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.

Updated 4 min read

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

output
 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, and db, PostgreSQL 15 or newer.
  • Uploads: sonda scan --upload or sonda upload sends 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 pgdata volume.
  • 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:

shell
git clone --branch v0.1.0 https://github.com/lvaoliveira/sonda.git && cd sonda
scripts/setup-server.sh

The 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:

shell
curl http://127.0.0.1:9000/api/health

Upload the first analysis

From any repository on the same machine:

shell
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 the caddy_data volume 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_TOKEN or a file, never a flag; sends it only over HTTPS, except to localhost; 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.

shell
git fetch --tags && git checkout v0.2.0   # the release you're upgrading to
scripts/setup-server.sh

Migrations run at startup, each in a transaction. If one fails, the server exits instead of serving.

Back up the database:

shell
docker compose exec -T db pg_dump -U sonda -Fc sonda > sonda.dump

Stop 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 --lan on a trusted network.
  • sonda upload refuses the server URL. Plain http:// is allowed only to localhost; 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-id header; search the logs for it with docker compose logs sonda.