This document provides a complete, single-host container deployment for Purple Sentinel SOC Studio using Docker Compose.
It includes:
- a production-style build for the backend services (Node/Express) and Web UI (Vite build served by nginx)
- an isolated local Docker network
- persisted storage for PostgreSQL data and server key material
- operational recommendations and hardening notes
Goal: serve the UI and API from the same origin to avoid CORS complexity.
Topology:
web(nginx): serves static UI + reverse proxies:/api/auth/*,/api/profile/*,/api/audit/*,/api/settings/*→auth-service/api/iocs/*,/api/ioc-enrichment/*→ioc/api/tools/*→tools- everything else under
/api/*→core-api
core-api(node): runs incidents/health API routes on port4000auth-service(node): runs auth/profile/audit/settings API routes on port4001ioc(node): runs IOC and IOC enrichment API routes on port4005tools(node): runs analyst tool APIs on port4010postgres(PostgreSQL): persists platform statepss-pgdatavolume: persists PostgreSQL data filespss-datavolume: persists key material fallback (only used when secrets are not provided via env)
flowchart LR
B["Browser"] -->|"https://host:8443 (TLS 1.3, self-signed)"| W["web (nginx)"]
W -->|"/api/auth/*, /api/profile/*, /api/audit/*, /api/settings/*"| AS["auth-service (node) :4001"]
W -->|"/api/iocs/*, /api/ioc-enrichment/*"| IS["ioc (node) :4005"]
W -->|"/api/tools/*"| TS["tools (node) :4010"]
W -->|"/api/* (other paths)"| CA["core-api (node) :4000"]
AS -->|SQL| P["postgres :5432"]
IS -->|SQL| P
TS -->|SQL| P
CA -->|SQL| P
P --> PG["pss-pgdata volume (PostgreSQL data)"]
AS --> D["pss-data volume (key material fallback)"]
IS --> D
TS --> D
CA --> D
IaC files (in repo root):
docker-compose.yml.dockerignore.env.exampleserver/Dockerfileclient/Dockerfiledeploy/nginx.conf
Prereqs:
- Docker Desktop (macOS/Windows) or Docker Engine + Compose (Linux)
Steps:
- Create environment file:
cd /Users/mark/Documents/dev/PurpleSentinelStudio
cp .env.example .env- Generate strong secrets (examples):
# JWT signing secret (HS256)
export JWT_SECRET="$(openssl rand -hex 64)"
# 32-byte key for integration key encryption (AES-256-GCM)
export PSS_MASTER_KEY="$(openssl rand -base64 32)"
echo "JWT_SECRET=\"$JWT_SECRET\"" >> .env
echo "PSS_MASTER_KEY=\"$PSS_MASTER_KEY\"" >> .env- Set an initial admin password (optional but recommended):
echo "DEFAULT_ADMIN_PASSWORD=\"change-this-to-a-strong-password\"" >> .env- Build + run:
docker compose up -d --build- Verify health:
curl -sk https://localhost:8443/api/health- Open UI:
https://localhost:8443(self-signed; your browser will warn)http://localhost:8080(redirects to HTTPS)
If you did not set DEFAULT_ADMIN_PASSWORD, the server will generate one and print it in logs:
docker compose logs -f auth-serviceThe platform stores durable state in two places:
- PostgreSQL data files under
/var/lib/postgresql/data(inside thepostgrescontainer) - Optional key material under
server/data/(inside the backend containers at/app/server/data)
The API data directory is only used when you do not set:
JWT_SECRET(fallback.jwt-secretis created)PSS_MASTER_KEY(fallback.settings-keyis created)
In Compose, these live in the named volumes:
pss-pgdata(PostgreSQL data)pss-data(API key material fallback)
Recommended: use pg_dump for a consistent DB backup:
docker compose exec -T postgres sh -lc 'PGPASSWORD="$POSTGRES_PASSWORD" pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' > pss-db-backup.sqlAlternative (volume snapshot; stop containers first for consistency):
docker compose down
docker run --rm -v pss-pgdata:/data -v "$PWD":/backup alpine \
sh -c "cd /data && tar -czf /backup/pss-pgdata-backup.tgz ."
docker run --rm -v pss-data:/data -v "$PWD":/backup alpine \
sh -c "cd /data && tar -czf /backup/pss-data-backup.tgz ."# Restore pg_dump (recommended)
docker compose up -d postgres
cat pss-db-backup.sql | docker compose exec -T postgres sh -lc 'PGPASSWORD="$POSTGRES_PASSWORD" psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"'
# (Optional) restore key material volume
docker run --rm -v pss-data:/data -v "$PWD":/backup alpine \
sh -c "rm -rf /data/* && tar -xzf /backup/pss-data-backup.tgz -C /data"
docker compose up -d --buildCompose creates a dedicated bridge network pss-net (explicitly named, so it is stable).
Exposure:
webpublishes:8080to the host (HTTP redirect → HTTPS)8443to the host (HTTPS, TLS 1.3)
core-apiis not published to the host (reachable only viawebonpss-net)auth-serviceis not published to the host (reachable only viawebonpss-net)iocis not published to the host (reachable only viawebonpss-net)toolsis not published to the host (reachable only viawebonpss-net)
This reduces the external attack surface: the API is not directly reachable from outside the Docker host.
deploy/nginx.conf:
- proxies:
/api/auth/*,/api/profile/*,/api/audit/*,/api/settings/*→http://auth-service:4001/api/iocs/*,/api/ioc-enrichment/*→http://ioc:4005/api/tools/*→http://tools:4010- everything else under
/api/*→http://core-api:4000
- serves the SPA and supports client-side routing via
try_files ... /index.html - adds basic response headers for static UI responses
- caches hashed build assets for performance
This repo ships as a developer-focused tool; for real environments, do these:
- TLS
- Terminate TLS in front of
web(preferred: load balancer / ingress / Caddy/Traefik). - Enable HSTS at the TLS termination layer.
- Secrets
- Always set:
JWT_SECRETPSS_MASTER_KEY
- Store them in a secrets manager (not in
.envcommitted to git).
- Reverse proxy trust
- When running behind a proxy/load balancer, enable proxy trust so audit
ip_addressis meaningful. - This compose file sets
TRUST_PROXY=1.
- Outbound HTTP tooling (SSRF surface)
- The HTTP Request Generator (
POST /api/tools/http-requests) can be abused if exposed to untrusted users. - For production:
- restrict it by role/permission, and/or
- add SSRF protections (block private IP ranges + DNS rebinding), and/or
- use network policy/egress controls to prevent access to internal networks/metadata.
- Scaling constraints
- PostgreSQL enables multi-replica API deployments.
- If you scale
core-api,auth-service,ioc, and/ortools, ensureJWT_SECRETandPSS_MASTER_KEYare identical across replicas (do not rely on per-container fallback files).
- Observability
- Add request IDs and structured logs for production.
- Ship logs from
web,core-api,auth-service,ioc, andtoolsto your log pipeline (ELK, Loki, CloudWatch, etc.).
Rebuild after updates:
docker compose up -d --buildStop:
docker compose downWipe everything (danger: destroys DB):
docker compose down -v- “Permission denied” writing to
/app/server/data
- If the volume was created with unexpected ownership, fix it:
docker run --rm -v pss-data:/data alpine sh -c "chown -R 10001:10001 /data || true"- UI loads but API calls fail
- Check:
docker compose ps
docker compose logs --tail=200 core-api
docker compose logs --tail=200 auth-service
docker compose logs --tail=200 ioc
docker compose logs --tail=200 tools
curl -i http://localhost:8080/api/health
curl -ik https://localhost:8443/api/health