Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/dev-stack-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Dev stack check

on:
pull_request:
paths:
- "dev-stack.sh"
- "docker-compose.dev-stack.yml"
- "deploy/nginx/dev-stack.conf.template"
- "deploy/scripts/test-dev-stack.sh"
- "apps/ingest/internal/config/**"
- ".github/workflows/dev-stack-check.yml"
- "apps/docs/docs/development/dev-stack.md"
- "apps/docs/docs/.vuepress/config.ts"
push:
branches:
- main
paths:
- "dev-stack.sh"
- "docker-compose.dev-stack.yml"
- "deploy/nginx/dev-stack.conf.template"
- "deploy/scripts/test-dev-stack.sh"
- "apps/ingest/internal/config/**"
- ".github/workflows/dev-stack-check.yml"
- "apps/docs/docs/development/dev-stack.md"
- "apps/docs/docs/.vuepress/config.ts"

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: actions/setup-node@v6
with:
node-version: "20"

- uses: actions/setup-go@v6
with:
go-version: "1.25"
cache-dependency-path: apps/ingest/go.sum

- name: Validate dev stack scripts
run: |
set -euo pipefail
bash -n dev-stack.sh
bash -n deploy/scripts/test-dev-stack.sh
bash deploy/scripts/test-dev-stack.sh

- name: Test ingest config
working-directory: apps/ingest
run: go test ./internal/config
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ out
.env.*.local
!.env.example

# Local dev-stack runtime config and secrets
.dev/

# OS
.DS_Store
Thumbs.db
Expand Down
1 change: 1 addition & 0 deletions apps/docs/docs/.vuepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export default defineUserConfig({
text: 'Development',
collapsible: true,
children: [
'/development/dev-stack.md',
'/development/testing.md',
],
},
Expand Down
112 changes: 112 additions & 0 deletions apps/docs/docs/development/dev-stack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Local Dev Stack

Use `./dev-stack.sh` from the repository root when you want fast local feedback
without waiting for a release, image publication, or a test-server update.

The command starts the production-adjacent dependencies in Docker and runs the
web app in a Docker dev container with your source checkout bind-mounted for hot
reload:

- `http://localhost:3000` — local dev proxy and the only browser URL you need
- `http://localhost:3001` — direct Next.js dev server with hot reload inside Docker
- `localhost:9443` — ingest gRPC for agents
- `localhost:55432` — CT-Ops Postgres, bound to loopback only
- Password Manager API and database in Docker

## Start

Prerequisite:

- Docker Desktop or Docker Engine with the Compose plugin

```bash
./dev-stack.sh
```

The first run creates `.dev/dev.env` and `apps/web/.env.local` with local-only
secrets and URLs. The root `.env` used by the release/test-server path is not
modified.

The `web-migrate` and `web-dev` containers run `pnpm install --frozen-lockfile`
inside Docker using named volumes for `node_modules`, so Linux container
dependencies do not overwrite host dependencies. The `ingest-dev` container uses
Go module and build-cache volumes for the same reason.

Open:

```text
http://localhost:3000
```

The dev proxy forwards:

- `/` to the Next.js dev server
- `/password-manager-api/` to the bundled Password Manager API container
- `/ws/terminal/` to the local ingest container

## Stop Or Reset

Stop the stack while preserving local database volumes:

```bash
./dev-stack.sh --down
```

Remove local dev volumes and generated local config:

```bash
./dev-stack.sh --reset
```

## Checks

After the stack is running, check the proxy, Password Manager API, and ingest
health endpoints:

```bash
./dev-stack.sh --check
```

Show current local service status:

```bash
./dev-stack.sh --status
```

## Rebuilds

The script builds agent binaries if they are missing. Rebuild them explicitly
after changing agent code:

```bash
./dev-stack.sh --rebuild-agents
```

## Next.js Flags

By default the script runs Next.js with Turbopack inside Docker:

```text
CT_OPS_DEV_NEXT_FLAGS=--turbopack
```

If you need the webpack dev server instead, set this in `.dev/dev.env` before
starting:

```text
CT_OPS_DEV_NEXT_FLAGS=--webpack
```

## Environment Separation

The local dev stack deliberately uses separate config, ports, and Docker
volumes from the release bundle:

- local dev config: `.dev/dev.env` and `apps/web/.env.local`
- release/test-server config: root `.env`
- local Compose project: `ct-ops-dev`
- release Compose project: the bundle directory default

This means you can use `./dev-stack.sh` locally and still use `./update.sh` or
`./start.sh` on a test server without manually changing environment variables
between modes.
7 changes: 7 additions & 0 deletions apps/ingest/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,13 @@ func applyEnv(cfg *Config) {
cfg.GRPCPort = port
}
}
if v := os.Getenv("INGEST_HTTP_PORT"); v != "" {
var port int
_, _ = fmt.Sscanf(v, "%d", &port)
if port > 0 {
cfg.HTTPPort = port
}
}
if v := os.Getenv("INGEST_TLS_CERT"); v != "" {
cfg.TLS.CertFile = v
}
Expand Down
13 changes: 13 additions & 0 deletions apps/ingest/internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@ func TestLoadAppliesTerminalTrustedOriginsEnv(t *testing.T) {
}
}

func TestLoadAppliesHTTPPortEnv(t *testing.T) {
t.Setenv("INGEST_HTTP_PORT", "18080")

cfg, err := Load("/tmp/does-not-exist.yaml")
if err != nil {
t.Fatalf("Load() error = %v", err)
}

if cfg.HTTPPort != 18080 {
t.Fatalf("cfg.HTTPPort = %d, want 18080", cfg.HTTPPort)
}
}

func TestLoadBuildsEncodedDatabaseURLFromPostgresEnv(t *testing.T) {
t.Setenv("POSTGRES_USER", "ctops")
t.Setenv("POSTGRES_PASSWORD", "Pyth)n2475##")
Expand Down
6 changes: 5 additions & 1 deletion apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ export default async function RootLayout({
suppressHydrationWarning
>
<head>
<script nonce={nonce} dangerouslySetInnerHTML={{ __html: themeInitScript }} />
<script
nonce={nonce}
suppressHydrationWarning
dangerouslySetInnerHTML={{ __html: themeInitScript }}
/>
</head>
<body className="min-h-full antialiased">
<QueryProvider>
Expand Down
83 changes: 83 additions & 0 deletions deploy/nginx/dev-stack.conf.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
map $http_upgrade $connection_upgrade {
default "";
~*^websocket$ upgrade;
}

upstream ct_ops_dev_web {
server ${CT_OPS_DEV_WEB_UPSTREAM};
keepalive 16;
}

upstream ct_ops_dev_ingest_ws {
server ${CT_OPS_DEV_INGEST_UPSTREAM};
keepalive 8;
}

upstream ct_password_manager_api {
server password-manager-api:8080;
keepalive 8;
}

server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;

client_max_body_size 100m;

location = /nginx-healthz {
access_log off;
return 200 "ok\n";
}

location /ws/terminal/ {
proxy_pass http://ct_ops_dev_ingest_ws;
proxy_http_version 1.1;
proxy_set_header Upgrade websocket;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto http;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}

location ~ ^/api/hosts/[^/]+/stream$ {
proxy_pass http://ct_ops_dev_web;
proxy_http_version 1.1;
proxy_set_header Upgrade "";
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto http;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
chunked_transfer_encoding on;
}

location /password-manager-api/ {
proxy_pass http://ct_password_manager_api/;
proxy_http_version 1.1;
proxy_set_header Upgrade "";
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto http;
}

location / {
proxy_pass http://ct_ops_dev_web;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto http;
proxy_read_timeout 3600s;
}
}
Loading
Loading