Docs / Start

Install & self-host

Isoward runs as a gateway process (marketing site, dashboard, /v1 API) plus a Docker-based runtime for customer workloads. Production URL: https://isoward.com (local default port 9876).

Self-host at no platform license fee — you bring the metal. Isoward Cloud is paid hosted service. See Pricing.

Quickstart

After you have the Isoward source tree on the host:

bash
# On your metal — after you have the Isoward source tree
cp .env.example .env
docker compose up -d --build
# Verify local gateway
curl -sI http://127.0.0.1:9876/api/health

Prefer a guided path for Cloudflare tunnel production, local ./scripts/start.sh, and isolation hardening? Continue below.

Production (Cloudflare tunnel)

Run the gateway on isoward-net so isoward-tunnel can reach it. See Cloudflare tunnel setup for wildcard routing.

bash
cd /path/to/isoward
cp .env.example .env   # set ISOWARD_PUBLIC_URL=https://isoward.com
docker network create isoward-net 2>/dev/null || true
docker compose --profile tunnel up -d --build
docker restart isoward-tunnel

Verify: curl -sI https://isoward.com/api/health200

Service URL TLS: run ./scripts/verify-public-routing.sh --brand isoward before customer https://srv-….isoward.com URLs work in browsers.

Production Docker (with deploy capability)

The gateway container mounts the Docker socket to build and run customer workloads on the host:

bash
docker network create isoward-net 2>/dev/null || true
docker compose up -d --build

Customer traffic uses the gateway host proxy via Cloudflare tunnel: not host Traefik. Traefik Docker labels are reserved for a future edge option.

Local development

bash
cd /path/to/isoward
./scripts/start.sh

First run installs dependencies, builds all apps, and starts the API gateway on port 9876.

URLs (local)

  • Marketing: http://127.0.0.1:9876/
  • Dashboard: http://127.0.0.1:9876/dashboard/
  • API: http://127.0.0.1:9876/v1/services
  • Health: http://127.0.0.1:9876/api/health

Warded workloads (gVisor)

Isoward Cloud already runs customer app containers under gVisor (runsc). On your own metal, enable the same path for stronger kernel isolation. Managed Postgres and Key Value stay on the default Docker runtime in this release.

Public services attach to the edge network first so the gateway can reach them under gVisor.

bash
# On the host (requires sudo / Docker restart)
./scripts/setup-gvisor.sh

# Enable in .env, then restart the gateway
ISOWARD_DOCKER_RUNTIME=runsc

# After redeploying a web service:
docker inspect -f '{{.HostConfig.Runtime}}' <container>
# expect: runsc

If ISOWARD_DOCKER_RUNTIME=runsc is set but runsc is not registered with Docker, deploys fail with a clear error (no silent fallback to runc).

SMTP egress lockout

Governance requires blocking outbound TCP ports 25, 465, and 587 from customer containers so workloads cannot turn the host IP into a spam relay. Docker CapDrop alone does not enforce this. On the host, install a persistent DOCKER-USER DROP (covers all Docker-forwarded egress, including workspace and edge networks):

bash
# On the host (requires sudo)
./scripts/setup-smtp-egress-block.sh

# Optional verify (should fail / time out, not connect):
docker run --rm --network isoward-net alpine:3.20 \
  sh -c 'timeout 3 nc -zv smtp.gmail.com 587 || echo blocked_ok'

Host-originated mail (not forwarded through Docker) is unaffected. Match host firewall rules on the Linux bridge iface (iws-* or br-<id>), not the Docker network name.

Workspace east-west isolation

Per-workspace Docker networks are soft isolation until the host firewall drops forwarding between tenant bridges. The runtime creates workspace bridges with short Linux iface names iws-* (com.docker.network.bridge.name). On the host, install a persistent DOCKER-USER / ISOWARD-EASTWEST chain that DROPs iws-+iws-+ traffic and blocks customer↔customer hairpins on the shared edge network (gateway still allowed):

bash
# On the host (requires sudo)
./scripts/setup-workspace-isolation.sh

# Optional: recreate empty workspace nets so they pick up iws-* bridge names

Do not match Docker network names in iptables; use the iws-* host iface. See Workspace private network.

User namespace remapping (userns-remap)

By default, root inside a container is the same UID as root on the host. Governance requires Docker userns-remap so customer container root maps to an unprivileged host user (dockremap). This is a per-host operator step (portable script in the repo — not automatic just because you clone Isoward).

bash
# On the host (requires sudo / Docker restart)
./scripts/setup-userns-remap.sh

# Recreate gateway (compose sets userns_mode: host so docker.sock keeps working)
docker compose up -d --force-recreate isoward-gateway

The gateway is intentionally exempt via userns_mode: host so it can use the Docker socket and bind-mounted ./data. Customer workloads inherit daemon remap. Enabling remap may require recreating Postgres/KV bind mounts if host path ownership no longer matches.

Environment

Copy .env.example to .env. Set ISOWARD_API_TOKEN (or legacy BREACHFIX_CLOUD_API_TOKEN), GitHub App credentials, and billing API keys as needed. See Connect GitHub for App setup.

systemd

bash
sudo cp systemd/isoward.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now isoward