* feat(deploy): add Azure deployment templates (App Service + ACI) Adds a deploy/azure/ lane so Open Design can be deployed to Microsoft Azure from the published runtime image, alongside the existing Docker Compose and Helm options. Covers the App Service + ACI scope of #1028. Two Bicep templates run the same single-port Alpine image used by deploy/docker-compose.yml and charts/open-design: - app-service.bicep: App Service for Containers with managed HTTPS, Always On, and health checks on /api/health. - aci.bicep: a single Azure Container Instances group with a public FQDN and an /api/health liveness probe. These are evaluation deployments: state lives on the container's local disk and is ephemeral. Open Design stores SQLite under OD_DATA_DIR, and SQLite needs real file locking, which the Azure Files (SMB) storage behind both App Service and ACI persistence cannot provide without corruption. App Service sets WEBSITES_ENABLE_APP_SERVICE_STORAGE=false to keep the data dir on local disk. Durable self-hosting stays on the Compose named volume or the Helm PVC. Both wire the daemon's env contract (OD_BIND_HOST/OD_PORT/OD_WEB_PORT/ OD_DATA_DIR/OD_PUBLIC_BASE_URL/OD_ALLOWED_ORIGINS/OD_API_TOKEN) and take the API token as a secure parameter so it never appears in deployment outputs. deploy-azure.sh wraps `az` to create the resource group, generate a token when one isn't supplied, deploy a lane, and print the URL. README.md documents both lanes, the ephemeral-data caveat, and the security trade-offs. deploy/tests/azure-bicep.test.ts guards the runtime contract and that the data dir is never mounted to Azure Files; when the bicep CLI is present it also compiles both templates. * docs(deploy): add ACI health-check example to Azure quick start
Docker deployment
This deployment ships Open Design as a single Alpine-based runtime image. The daemon serves both the API and the built Next.js static export, so there is no separate nginx container.
Local compose
Before starting:
-
Copy the environment template:
cp .env.example .env -
Generate a secure token (recommended unless your reverse proxy will both authenticate every request and set
OPEN_DESIGN_DISABLE_API_AUTH=1):openssl rand -hex 32 -
Open
.envin your editor and choose one auth mode:- default: paste the token into
OD_API_TOKEN= - trusted reverse proxy that already authenticates every request: leave
OD_API_TOKEN=empty and setOPEN_DESIGN_DISABLE_API_AUTH=1
- default: paste the token into
Then pull and start the service:
OPEN_DESIGN_IMAGE=ghcr.io/nexu-io/od:latest docker compose pull
OPEN_DESIGN_IMAGE=ghcr.io/nexu-io/od:latest docker compose up -d --no-build
Use ghcr.io/nexu-io/od:latest for the latest stable image, or
ghcr.io/nexu-io/od:<version> to pin a supported release.
Defaults:
- Host port:
127.0.0.1:7456(OPEN_DESIGN_PORT=8080to publish on127.0.0.1:8080) - Runtime data: before documenting, changing, or choosing persistent daemon
storage, you MUST read root
AGENTS.md→ Daemon data directory contract. This README MUST NOT restate it. - Node heap cap:
--max-old-space-size=192 - Compose memory cap:
384m(OPEN_DESIGN_MEM_LIMIT=256mto override)
Do not publish the daemon directly on a public or shared LAN interface. The API is unauthenticated for non-browser clients, so remote deployments should keep Compose bound to localhost and put an authenticated reverse proxy, SSH tunnel, or VPN in front of it.
When exposing the service through an authenticated public IP, domain, or reverse
proxy, set OPEN_DESIGN_ALLOWED_ORIGINS to the exact browser origins that should
be allowed to call /api:
OPEN_DESIGN_ALLOWED_ORIGINS=https://od.example.com,http://203.0.113.10:7456 docker compose up -d --no-build
If the reverse proxy already authenticates every request and you do not want it
to inject Authorization: Bearer <OD_API_TOKEN> upstream, set:
OPEN_DESIGN_DISABLE_API_AUTH=1
Use this only for trusted deployments where the daemon is reachable strictly
through that authenticated proxy. It disables daemon-side bearer enforcement for
all /api/* requests, so direct access to the daemon must remain blocked. The
Compose variable maps to daemon env OD_DISABLE_API_AUTH.
Pin a specific published image with a digest instead of the mutable latest tag:
OPEN_DESIGN_IMAGE=ghcr.io/nexu-io/od@sha256:<digest> docker compose up -d --no-build
The image intentionally does not bundle Claude/Codex/Gemini CLI binaries. Keep those outside the image, or build a separate private runtime layer if a server deployment needs local code-agent CLIs installed in the container.
If you install Codex inside an unprivileged Linux container and it fails while
creating its workspace-write sandbox, opt into Codex's full-access mode for
all Codex runs in that deployment:
OD_CODEX_SANDBOX=danger-full-access docker compose up -d --no-build
Only the exact value danger-full-access is supported; unknown values are
ignored. Use this only for trusted, single-user deployments. It lets Codex run
without the workspace-write sandbox, which is useful when the container host
blocks unprivileged user namespaces, but it gives the Codex process broader
filesystem access inside the container.
Manual image publish override
deploy/scripts/publish-images.sh --image_tag latest
Useful overrides:
IMAGE_NAMESPACE=your-ghcr-org deploy/scripts/publish-images.sh --arch arm64
deploy/scripts/publish-images.sh --image ghcr.io/your-org/od:0.1.0
The script defaults to:
ghcr.io/nexu-io/od:<tag>linux/amd64,linux/arm64skopeopush strategy with registry credentials read from~/.docker/config.json- preloading base images through
skopeoto reduce Docker Hub pull flakiness
If 127.0.0.1:7890 is available and no proxy is already set, the script uses it
for registry access and passes host.docker.internal:7890 into Docker builds. The
host-gateway alias is only added for builds that need this local proxy mapping.
Colima swap helper for Apple Silicon
deploy/scripts/prepare-colima-build-swap.sh is for manual Docker image
publishing from an Apple Silicon macOS host that uses Colima as the Docker VM.
The helper is intentionally Apple Silicon-only because the failure mode it covers
is local arm64 Colima builds exhausting a small Linux VM while preparing
multi-arch images. It exits before touching Colima on non-macOS or
non-Apple-Silicon hosts.
Low-memory Colima VMs can run out of RAM during multi-arch image builds. The
helper checks the VM memory and swap status, then creates and enables a temporary
swap file only when the VM has no swap and less than 4 GiB of RAM. The 4 GiB
threshold is a conservative default for short-lived manual publishes on small
Colima profiles; raise COLIMA_BUILD_SWAP_MEMORY_THRESHOLD_KIB if larger builds
still OOM, or lower it if you only want swap for very small VMs.
Prefer increasing the Colima VM memory (colima start --memory <GiB> or the
profile config) when you want a persistent build machine. Use this helper when
you need a temporary, reversible boost for one manual publish without resizing
or recreating the VM.
Run it before a manual publish if Docker builds fail with out-of-memory errors,
or if status shows a small Colima VM with no swap. The swap remains active
until cleanup or VM restart, so use a shell trap for one-off sessions:
deploy/scripts/prepare-colima-build-swap.sh status
deploy/scripts/prepare-colima-build-swap.sh
trap 'deploy/scripts/prepare-colima-build-swap.sh cleanup' EXIT
deploy/scripts/publish-images.sh --image_tag latest
Useful overrides:
COLIMA_BUILD_SWAP_SIZE=6G deploy/scripts/prepare-colima-build-swap.sh
COLIMA_BUILD_SWAP_MEMORY_THRESHOLD_KIB=6291456 deploy/scripts/prepare-colima-build-swap.sh
COLIMA_BIN=/opt/homebrew/bin/colima deploy/scripts/prepare-colima-build-swap.sh status
COLIMA_BUILD_SWAP_CLEANUP_FORCE=1 COLIMA_BUILD_SWAPFILE=/custom-swapfile deploy/scripts/prepare-colima-build-swap.sh cleanup
cleanup removes the default helper path and the old helper path. If you set a
custom COLIMA_BUILD_SWAPFILE, cleanup refuses to remove it unless
COLIMA_BUILD_SWAP_CLEANUP_FORCE=1 is also set.
Docker Desktop on macOS
When running Docker Compose on macOS with OD_API_TOKEN enabled, Docker Desktop bridge networking may cause the daemon to see API requests as non-loopback peers. In that case, the web UI can fail with:
Authorization: Bearer <OD_API_TOKEN> required
Workaround:
-
Enable host networking in Docker Desktop:
Docker Desktop → Settings → Resources → Network → Enable host networking → Apply and restart -
Use a local override to docker-compose.yml:
services: open-design: network_mode: host ports: [] -
Recreate the container:
docker compose down docker compose up -d --force-recreate -
Verify:
docker inspect open-design --format '{{.HostConfig.NetworkMode}}' # host