-
-
Notifications
You must be signed in to change notification settings - Fork 0
Deployment Guide
How to install openWCS and deploy it — on a local Linux box, a cloud VM, or as managed containers on AWS, Google Cloud, and Microsoft Azure.
What ships today. openWCS is built from source. A
Dockerfilefor every service and a ready-to-runplatform/docker-compose.ymlare committed, but the project does not yet publish prebuilt images, Helm charts, or Terraform. The cloud sections below are recipes: build the images yourself, push them to your registry, and wire them up with the platform's native primitives. Contributions of charts / IaC are very welcome — see Contributing.
For a developer build-and-run loop, see Getting Started. This page is about standing the system up for real.
- What you're deploying
- 1 · Build & publish the images
- 2 · Local Linux server
- 3 · A cloud VM (any provider)
- 4 · AWS — containers (ECS Fargate)
- 5 · Google Cloud (GKE / Cloud Run)
- 6 · Microsoft Azure (Container Apps / AKS)
- 7 · Kubernetes — starter manifests (horizontal scaling)
- Production checklist
| Tier | Components | Notes |
|---|---|---|
| Edge |
gateway (8080) |
The only service that needs to be publicly reachable. Routes /api/<service>/**, validates JWTs (optional), forwards identity headers. |
| Domain services (Spring Boot, JRE 21) | master-data, inventory, process-engine, order-management, allocation, flow-orchestrator, txlog, iam, notification, integration-host, integration-sap, integration-manhattan | Stateless HTTP; all reach one PostgreSQL database (schema-per-service) and Kafka. Reach each other by hostname over HTTP. |
| Device adapters (Go) | conveyor, asrs, amr-geekplus, autostore, conveyor-sniffer | Talk equipment protocols ↔ the uniform device contract. The sniffer also needs L2 reachability to the conveyor network it learns from. |
| Equipment emulator (Go) | equipment-emulator | Single emulator for all device families (port 9097). flow-orchestrator routes device tasks here when HARDWARE_EMULATOR_ENABLED is ON. Stateless; no DB/Kafka/hardware connection needed. |
| UI (React/Vite) | ui |
A static bundle (npm run build → dist/). Serve it from any static host and point it at the gateway. |
| Infrastructure | PostgreSQL 16 · Apache Kafka · Keycloak 25 (auth) · (optional) Schema Registry | In production, prefer managed Postgres + Kafka. |
Key wiring (env vars). Every Spring service takes:
SPRING_DATASOURCE_URL=jdbc:postgresql://<pg-host>:5432/openwcs
SPRING_DATASOURCE_USERNAME=openwcs
SPRING_DATASOURCE_PASSWORD=<secret>
SPRING_KAFKA_BOOTSTRAP_SERVERS=<broker:9092> # services that use Kafka
The gateway is told where the services live via OPENWCS_URI_<SERVICE> (e.g.
OPENWCS_URI_INVENTORY=http://inventory:8082); services that call each other use
OPENWCS_<TARGET>_BASE_URL. In Compose those are hostnames; in the cloud they become your
platform's internal DNS names (Cloud Map / Container Apps / k8s Service). See the committed
platform/docker-compose.yml
for the full, authoritative set.
Ports are listed in Getting Started.
Every deployment target consumes the same images, so build them once and push to your registry. Prerequisites on the build host: JDK 21, Docker, Go 1.25+, Node 20+.
git clone https://github.com/brettljausn-ai/openwcs.git && cd openwcs
# Pick your registry prefix:
# GHCR: ghcr.io/<org>
# ECR: <acct>.dkr.ecr.<region>.amazonaws.com
# GAR: <region>-docker.pkg.dev/<project>/<repo>
# ACR: <name>.azurecr.io
REG=ghcr.io/your-org
TAG=$(git rev-parse --short HEAD)
# Java jars (the Dockerfiles COPY build/libs/*.jar)
./gradlew bootJar
# Java services — the module directory is the build context
for m in gateway \
services/master-data services/inventory services/process-engine services/order-management \
services/allocation services/flow-orchestrator services/txlog services/iam services/notification \
services/integration-host services/integration-sap services/integration-manhattan; do
name="openwcs-$(basename "$m")"
docker build -t "$REG/$name:$TAG" "$m" && docker push "$REG/$name:$TAG"
done
# Go adapters — multi-stage, built from source
for a in conveyor asrs amr-geekplus autostore conveyor-sniffer; do
docker build -t "$REG/openwcs-$a-adapter:$TAG" "services/adapters/$a" \
&& docker push "$REG/openwcs-$a-adapter:$TAG"
done
# Single equipment emulator (all families)
docker build -t "$REG/openwcs-equipment-emulator:$TAG" "services/equipment-emulator" \
&& docker push "$REG/openwcs-equipment-emulator:$TAG"
# UI — static bundle (host it on a static service, or bake your own nginx image)
( cd ui && npm ci && npm run build ) # → ui/distAuthenticate to your registry first (docker login ghcr.io, aws ecr get-login-password …,
gcloud auth configure-docker …, or az acr login …).
Smallest real install: one box running everything via Docker Compose. Good for a pilot, a single line, or an on-prem edge server next to the equipment.
# Debian/Ubuntu — install Docker Engine + the compose plugin
sudo apt-get update && sudo apt-get install -y ca-certificates curl
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER" && newgrp docker
git clone https://github.com/brettljausn-ai/openwcs.git && cd openwcs
./gradlew bootJar # JDK 21 required to build
docker compose -f platform/docker-compose.yml --profile apps up --build -dOr one command — scripts/setup-demo.sh does all of the above on a fresh Ubuntu
22.04/24.04 box (installs Docker + JDK 21, clones to /opt/openwcs, builds, starts):
curl -fsSL https://raw.githubusercontent.com/brettljausn-ai/openwcs/main/scripts/setup-demo.sh | sudo bash
# add --auto-deploy (clone first, then run it) to also install the auto-deploy timerThis starts PostgreSQL, Kafka (+ZooKeeper), Keycloak (imports the openwcs realm), Schema
Registry, every Java service, the Go adapters, and the gateway. Check health and watch logs:
docker compose -f platform/docker-compose.yml ps
curl localhost:8080/actuator/health # gateway
docker compose -f platform/docker-compose.yml logs -f gatewayMake it production-ish on one host:
-
Run on boot — wrap the compose project in a
systemdunit (ExecStart=docker compose … up,ExecStop=docker compose … down,Restart=always), or addrestart: unless-stoppedto the services. -
Auto-deploy on
main— keep the box in sync with the repo automatically, either with a poll-basedsystemdtimer or a CI-gated self-hosted GitHub Actions runner. Both ship indeploy/and reusescripts/deploy.sh(fast-forward → rebuild → recompose, no-op when unchanged). -
TLS + a real hostname — put Caddy or nginx in front, terminating HTTPS and proxying
to
gateway:8080. Only the gateway (and the UI host) should be exposed; keep Postgres, Kafka, Keycloak, and the services on the internal Docker network. -
Persistence — the
pgdatavolume holds your system of record. Put it on durable storage and back it up (pg_dump/pg_basebackup); snapshot Keycloak's data too. -
Externalise the DB (optional) — point
SPRING_DATASOURCE_URLat a separate PostgreSQL host instead of the in-compose one for easier backups/HA. -
Security — set
OPENWCS_SECURITY_ENABLED=trueand change all default passwords (Postgres, Keycloak admin, realm users). See Security. -
Serve the UI — the
--profile appsstack already builds and serves the UI with nginx on host :80/:443 (it proxies/api→ gateway), so browse the demo athttp://<host>/. For a real HTTPS cert (no browser warning), point DNS at the server and run the built-in helper — pass a comma-separated list to cover several domain names with a single SAN cert:The script runs certbot, installs the cert, and hot-reloads nginx. Add a daily cron to auto-renew (use the same domain list — a shorter list would drop names from the cert):sudo /opt/openwcs/scripts/issue-cert.sh app.openwcs.ai,openwcs.brettljausn.ai you@example.com
For production, put your own reverse proxy / TLS in front, or host# crontab -e (root) 0 3 * * * /opt/openwcs/scripts/issue-cert.sh app.openwcs.ai,openwcs.brettljausn.ai you@example.com >> /var/log/openwcs-cert.log 2>&1
ui/distseparately and point it at the gateway URL.
An EC2 instance, GCE VM, Azure VM, or a droplet is just §2 on rented hardware — the fastest way to get a public, internet-reachable instance.
-
Provision a VM (start with 4 vCPU / 8–16 GB RAM; the full stack with Kafka is the heavy part) running a current Ubuntu/Debian.
-
Open only 443 (and 80 for the ACME challenge) in the firewall / security group. SSH from your IP only. Do not expose 5432/9092/8180.
-
Follow §2. Use Caddy for automatic Let's Encrypt TLS:
wcs.example.com { reverse_proxy /api/* gateway:8080 reverse_proxy /* ui:80 # or serve ui/dist directly }
-
For anything beyond a pilot, move the database to the provider's managed PostgreSQL and point
SPRING_DATASOURCE_URLat it — you get backups, failover, and patching for free.
When you outgrow a single VM (HA, autoscaling, rolling deploys), graduate to the managed-container options below.
Run each service as a Fargate task; use managed data services. (EKS is an alternative if you prefer Kubernetes — see §5's GKE recipe, which translates directly.)
Managed building blocks
| Need | AWS service |
|---|---|
| Images | ECR (one repo per image from §1) |
| PostgreSQL |
RDS for PostgreSQL 16 — one instance, database openwcs
|
| Kafka |
Amazon MSK (native Kafka) — set SPRING_KAFKA_BOOTSTRAP_SERVERS to the broker string |
| Service-to-service DNS |
ECS Service Connect / Cloud Map private namespace (e.g. *.openwcs.local) |
| Public entry |
ALB → the gateway service only (target port 8080, health check /actuator/health) |
| Secrets | Secrets Manager / SSM Parameter Store → injected as task env vars |
| UI |
S3 + CloudFront (upload ui/dist) |
Shape of it
-
aws ecr create-repositoryfor each image; push (§1) withREG=<acct>.dkr.ecr.<region>.amazonaws.com. -
Create the RDS instance and (optionally) MSK cluster in a VPC with private subnets.
-
Create an ECS cluster and a task definition per service. Each task's env mirrors the Compose file, but hostnames become Service Connect DNS names:
SPRING_DATASOURCE_URL = jdbc:postgresql://<rds-endpoint>:5432/openwcs SPRING_KAFKA_BOOTSTRAP_SERVERS = <msk-bootstrap-brokers> OPENWCS_URI_INVENTORY = http://inventory.openwcs.local:8082 # on the gateway task OPENWCS_ALLOCATION_BASE_URL = http://allocation.openwcs.local:8091 # on order-management, etc. -
Register each as an ECS Service (desired count ≥1) joined to the Service Connect namespace. Only the
gatewayservice sits behind the ALB; everything else stays private. -
Point CloudFront/Route 53 at the ALB; deploy the UI bundle to S3.
Tip: start the gateway + the services it fronts, prove
/api/.../actuator/healththrough the ALB, then scale out the adapters and integration services. Keep Keycloak as its own Fargate service (or use an existing IdP) and enableOPENWCS_SECURITY_ENABLEDonce tokens flow.
Recommended: GKE — because openWCS needs Kafka and a couple of always-on Kafka consumers
(inventory, the txlog relay), a Kubernetes cluster is the cleanest fit.
| Need | GCP service |
|---|---|
| Images |
Artifact Registry (<region>-docker.pkg.dev/<project>/openwcs) |
| PostgreSQL |
Cloud SQL for PostgreSQL 16, database openwcs
|
| Kafka | self-managed on GKE (e.g. Strimzi/Bitnami) or Confluent Cloud |
| Cluster | GKE Autopilot |
| Public entry |
GKE Ingress / Gateway API → the gateway Service |
| UI | Cloud Storage static site + Cloud CDN |
Each service becomes a Deployment + Service; the k8s Service name is the hostname, so
OPENWCS_URI_* / *_BASE_URL map straight onto http://inventory:8082, etc. — i.e. the Compose
env translates almost 1:1 into manifests. Put the DB password and Kafka creds in a Secret,
expose only the gateway through Ingress, and run Cloud SQL Auth Proxy (or private IP) for the DB.
Cloud Run also works for the stateless HTTP services (gateway + most domain services) with
Cloud SQL and Confluent Cloud for Kafka — use internal ingress + service-to-service IAM,
and set min-instances=1 on the Kafka-consuming services so projections keep up. If you'd rather
not manage that nuance, stay on GKE for the whole stack.
gcloud artifacts repositories create openwcs --repository-format=docker --location=<region>
gcloud auth configure-docker <region>-docker.pkg.dev
# …push images from §1 with REG=<region>-docker.pkg.dev/<project>/openwcs, then apply your manifestsRecommended: Azure Container Apps (ACA) — managed, scale-to-zero-capable containers with a built-in internal DNS, which suits the service mesh well.
| Need | Azure service |
|---|---|
| Images | Azure Container Registry (ACR) |
| PostgreSQL |
Azure Database for PostgreSQL — Flexible Server, database openwcs
|
| Kafka | Azure Event Hubs (Kafka-compatible endpoint) or Kafka on AKS |
| Runtime | Azure Container Apps environment (one app per service) |
| Public entry | external ingress on the gateway app; all others internal ingress |
| UI | Azure Static Web Apps or Blob Storage + CDN |
Within an ACA environment, apps reach each other at https://<app-name>.internal.<env-domain> (or
simply the app name) — so the service URLs become those internal FQDNs. Event Hubs speaks the Kafka
protocol, so SPRING_KAFKA_BOOTSTRAP_SERVERS=<namespace>.servicebus.windows.net:9093 with SASL
config works for the producers/consumers. Store the DB and Event Hubs connection strings in the
environment's secrets.
az acr create -n <acr> -g <rg> --sku Standard && az acr login -n <acr>
# push images from §1 with REG=<acr>.azurecr.io
az containerapp env create -n openwcs -g <rg> -l <region>
az containerapp create -n gateway -g <rg> --environment openwcs \
--image <acr>.azurecr.io/openwcs-gateway:<tag> --target-port 8080 --ingress external
# …repeat per service with --ingress internal and the SPRING_*/OPENWCS_* env varsAKS is the alternative for full Kubernetes control; the manifest model from §5 applies unchanged (swap Cloud SQL→Flexible Server, Artifact Registry→ACR).
Ready-to-apply plain-YAML manifests live in
deploy/k8s/
— a starting point for running the whole estate on Kubernetes. Edit config.yaml (image registry,
DB/Kafka endpoints, DB Secret) before applying. Full conventions and caveats are in
deploy/k8s/README.md and Horizontal Scaling.
deploy/k8s/
├── namespace.yaml # the openwcs namespace
├── config.yaml # shared ConfigMap + DB Secret placeholder
├── services.yaml # Deployment + Service for every Java service
├── adapters.yaml # Deployment + Service for the Go adapters (sniffer pinned to replicas: 1)
└── hpa.yaml # HPA 2→10 on CPU for the high-traffic services
kubectl apply -f deploy/k8s/What needed fixing to scale safely — two areas that assumed a single instance were fixed for this release:
| Area | Problem | Fix |
|---|---|---|
| Outbox relays & off-peak jobs |
@Scheduled on every replica → double-publish/duplicate tasks |
ShedLock @SchedulerLock; one replica runs per tick |
| Conveyor loop capacity check | count-then-enter race between replicas → capacity exceeded | pessimistic row lock on the loop row |
Two operational constraints to know before scaling:
-
Kafka partitions cap consumer replicas. The inventory stock projection and slotting velocity
learner scale only up to the partition count of
txlog.stream— size that topic to your intended max replica count. -
The conveyor-sniffer is pinned to one replica. It holds a long-lived TCP stream per
controller; round-robin load-balancing fragments telegrams. Scale sniffing by running one
instance per controller (separate Deployments with distinct
SNIFFER_LISTEN/allowlists) or use a session-affinity (sticky) L4 load balancer.
See Horizontal Scaling for the full guarantee matrix and ShedLock details.
Regardless of target:
-
One PostgreSQL, many schemas. All services share a single managed database (
openwcs) with a schema per service. Size it for your busiest service, enable automated backups + PITR. - Managed Kafka. Use MSK / Event Hubs / Confluent rather than self-managing brokers. The transaction log → Kafka stream is the backbone; size partitions and retention deliberately.
- Expose only the gateway. Keep services, DB, Kafka, and Keycloak on the private network. Terminate TLS at the edge (LB / ingress / reverse proxy).
-
Turn security on. Set
OPENWCS_SECURITY_ENABLED=true, run Keycloak (or your IdP) with theopenwcsrealm, rotate every default password/secret, and store secrets in the platform's secret manager — never in images. See Security. -
Scaling. All services scale horizontally — ShedLock prevents duplicate scheduled-job
execution across replicas; the conveyor-loop capacity check uses a pessimistic lock. Starter
Kubernetes manifests and HPAs live in
deploy/k8s/(see §7 and Horizontal Scaling). Mind the Kafka-consuming services (inventory,txlogrelay) — their throughput is bound by topic partitions, and they should not scale to zero if you need live projections. -
Adapters live near the equipment. Device adapters (and especially the conveyor-sniffer,
which needs L2 access to the conveyor network and a configured
WAREHOUSE_ID/ALLOWED_IPS) usually run at the edge/on-prem, not in the cloud. They reach the WCS over HTTPS. -
Observability & backups. Ship logs/metrics (
/actuatorendpoints are exposed), alert on consumer lag and gateway 5xx, and rehearse a database restore before go-live.
The public/ directory is the openWCS marketing site — a small Express/EJS app (Node 18+)
deployed separately on Hostinger, Render, or similar. Most of it is self-contained, but the
"Contact us" form requires a Microsoft Graph mailbox to send mail.
Clicking Contact us opens a modal; on submit the browser posts to POST /api/contact.
The handler validates input, applies a honeypot field (company) and a per-IP rate limit
(5 requests / 10 minutes), then calls services/graph.js which:
- Fetches an OAuth2 client-credentials token from Azure AD.
- Calls
POST /users/{mailbox}/sendMailvia the Graph API, with the submitter's address as reply-to (so hitting Reply in your mail client reaches them, not the platform mailbox). - Returns the standard JSON shape:
{ ok: true }on success, or{ ok: false, error }for 400 / 429 / 503 / 500 responses. Graph errors are logged server-side and never surfaced to the browser. On success the UI shows a confirmation message, then automatically closes the modal after ~2 seconds.
Until all four required env vars are set, every submission returns 503 — not configured. The static GitHub Pages mirror has no server, so the form is inert there.
| Variable | Purpose |
|---|---|
MS_GRAPH_TENANT_ID |
Azure AD tenant id |
MS_GRAPH_CLIENT_ID |
App registration (client) id |
MS_GRAPH_CLIENT_SECRET |
App registration client secret — keep secret |
MS_GRAPH_MAIL_ADDRESS |
Sender mailbox; the app registration must have Mail.Send permission for it |
MS_GRAPH_FROM_NAME |
(optional) Display name on the from address (default: openWCS) |
CONTACT_TO |
(optional) Recipient for contact submissions (default: contact@brettljausn.ai) |
See public/.env.example
for the full annotated template. The host (Hostinger / Render / etc.) injects these as process
environment variables — server.js does not load .env files.
- Register an app in Azure AD (App registrations → New registration).
- Under API permissions, add
Microsoft Graph → Application permissions → Mail.Send. Grant admin consent. - Under Certificates & secrets, create a client secret and note the value.
- Note the Application (client) ID and Directory (tenant) ID from the Overview page.
- Set the four required env vars on your hosting platform.
- Getting Started — local build & run, ports
- Architecture · Services — what each component does and owns
- Security — JWT, RBAC, Keycloak realm
- Equipment Integration — adapters & the device contract (edge deployment)
openWCS — open-source Warehouse Control System · summarized from build.md & docs/AS-BUILT.md (the repo docs are authoritative).