Stackray's local development setup is split by lifecycle:
- run the Next.js app on the host with
pnpm devfor fast refresh - run Postgres in Docker for local app data and Graphile Worker jobs
- run MinIO in Docker for local screenshot storage
- run the worker in Docker so
httpx,nuclei, templates, and Linux screenshot dependencies match production
This avoids pushing worker code to Railway for testing and avoids mutating the Railway database during schema work.
Install Docker Desktop with the WSL 2 backend, then run:
pnpm dev:initdev:init does four things:
- creates
.env.localfrom.env.local.exampleif.env.localdoes not already exist - starts local Postgres, MinIO, and the bucket init container
- applies Drizzle and Graphile Worker migrations to local Postgres
- creates a local admin user
Default local login:
- email:
admin@stackray.local - password:
StackrayDev123!
The setup script will not overwrite an existing .env.local. If .env.local already exists and points at Railway, edit it manually or move it aside before running local-dev commands.
Run the full local stack from one terminal:
pnpm dev:localdev:local starts local infrastructure, runs migrations, starts the Docker worker, starts the host Next.js dev server, and prefixes logs from the app and worker:
[next] ...
[worker] ...
The app URL is printed at startup. dev:local prefers the default ports, but if another worktree already has a local stack running it chooses the next open host ports for Next.js, Postgres, and MinIO. Each worktree also gets a distinct Docker Compose project and separate data volumes.
Press Ctrl+C to stop the Next.js dev server and the worker container. Postgres and MinIO stay running so local data persists and the next startup is faster.
To stop the backing services too:
pnpm dev:local:downRun this from the same worktree you used for pnpm dev:local; it targets that worktree's Docker Compose project.
To stop everything and wipe local Postgres/MinIO data:
pnpm dev:local:reset| Script | What it does | When to use it |
|---|---|---|
pnpm dev |
Starts the Next.js dev server on the host. | Normal UI/API development. |
pnpm dev:init |
Creates .env.local if missing, starts local infra, runs startup migrations, and seeds the default admin. |
First local setup, or after dev:infra:reset. |
pnpm dev:local |
Starts per-worktree local infra on available host ports, applies migrations, seeds the default admin, then runs Next.js and the Docker worker with prefixed logs. | Default daily local development, including parallel worktrees. |
pnpm dev:local:down |
Stops this worktree's local Docker services but keeps data volumes. | End the day and stop Postgres/MinIO too. |
pnpm dev:local:reset |
Stops this worktree's services and deletes its local Docker data volumes. | Reset the local database and MinIO bucket from scratch. |
pnpm dev:infra |
Starts Postgres and MinIO, waits for them to be ready, then runs the one-shot bucket initializer. | Start local backing services without starting the worker. |
pnpm worker:docker |
Builds if needed and runs the local worker container. | Debug the worker separately from Next.js. |
pnpm dev:infra:logs |
Follows Docker logs for Postgres, MinIO, and worker. | Debug local services. |
pnpm worker:docker:down |
Stops only the worker container. | Pause scan processing while keeping Postgres and MinIO running. |
pnpm dev:infra:down |
Stops the local Docker services but keeps data volumes. | End the day without deleting local data. |
pnpm dev:infra:reset |
Stops services and deletes local Docker data volumes. | Reset the local database and MinIO bucket from scratch. |
pnpm db:migrate:startup |
Applies checked-in Drizzle migrations and Graphile Worker migrations to the current DATABASE_URL. |
Run manually after migration changes. Be careful which env file is active. |
pnpm seed:admin --email ... --password ... |
Creates or updates an admin user in the current DATABASE_URL. |
Add a different local admin account. |
The Docker setup uses separate containers because each service has a different lifecycle:
postgres: long-running database and Graphile Worker job storeminio: long-running S3-compatible object storeminio-init: short-lived helper that creates thestackray-devbucket, then exitsworker: long-running Graphile Worker process withhttpx,nuclei, andsubfinderinstalled
The app is intentionally not containerized for local development. Keeping it on the host gives the best Next.js dev-server experience and uses the same source files as the worker through the mounted repo.
httpx, nuclei, and subfinder are not Node dependencies and do not appear in package.json. They are pinned as worker-image inputs in worker/scanner-pins.json and mirrored into worker/Dockerfile plus worker/Dockerfile.dev:
httpxis built from the pinnedCarlosCommits/httpxcommitnucleiis installed from the pinned release tagsubfinderis installed from the pinned release tag- nuclei templates are cloned at the pinned
projectdiscovery/nuclei-templatescommit and then overlaid withworker/nuclei-templates
Run pnpm scanners:update to refresh the pins without changing the Stackray version. The scheduled Update scanner pins GitHub Action refreshes scanner pins without bumping the Stackray version.
Scanner update PRs are validated by the standard CI workflow. After Quality, Scanner Docker build, and E2E smoke pass, .github/workflows/trusted-scanner-pin-auto-merge.yml may merge the trusted automation/update-scanner-pins PR directly. That workflow exists because repository-level GitHub auto-merge, branch protection, and rulesets are unavailable for the current private repository plan; it compensates with branch, author, file, semantic diff, check, and head-SHA gates.
Release PRs are managed by release-please. Use Conventional Commit PR titles for squash merges, such as fix: correct Railway update copy, feat: add CSV export, or feat!: change scan result API shape. After releasable commits merge to main, .github/workflows/release-please.yml opens or updates the pending release PR. Merge that release PR when you are ready to publish; release-please then creates the annotated tag and GitHub Release with generated notes.
The in-app update banner compares the deployed Stackray version to the latest GitHub Release from CarlosCommits/stackray by default. Raw semver tags are ignored so self-hosted deployments only see structured releases with release notes. The update dialog surfaces the GitHub Release notes when available. Set STACKRAY_RELEASE_REPOSITORY=owner/repo if a deployment should check a different repository. STACKRAY_GITHUB_TOKEN is optional for public repositories and required if a private release source should be checked from a deployment. See docs/releases.md and docs/railway-updates.md for the full release and self-hosted update flow.
pnpm dev:local prints the actual service URLs after it picks available ports. The first stack normally uses:
- app:
http://localhost:3000 - Postgres:
postgresql://postgres:postgres@127.0.0.1:5432/stackray - MinIO API:
http://127.0.0.1:9000 - MinIO console:
http://127.0.0.1:9001 - MinIO credentials:
minioadmin/minioadmin
You can force specific ports when needed:
$env:STACKRAY_DEV_APP_PORT=3010
$env:STACKRAY_DEV_POSTGRES_PORT=5540
$env:STACKRAY_DEV_MINIO_PORT=9010
$env:STACKRAY_DEV_MINIO_CONSOLE_PORT=9011
pnpm dev:local.env.local takes precedence over .env for local development. The checked-in .env.local.example points everything at the local Docker services:
DATABASE_URLpoints at local PostgresAWS_ENDPOINT_URLpoints at local MinIOAWS_S3_FORCE_PATH_STYLE=trueenables MinIO-compatible S3 addressingSTACKRAY_ENABLE_DEV_ACTOR=trueenables local development actor behavior
Do not use the Railway .env values for worker development unless the Railway worker is stopped and you intentionally want to mutate the Railway database and bucket.
For normal schema changes:
- edit
drizzle/schema.ts - run
pnpm db:generate - run
pnpm db:migrate:startupagainst local Postgres - test with
pnpm dev:local - commit the generated migration files and updated Drizzle metadata
Do not rewrite the checked-in 0000_* baseline for normal schema evolution.