Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
6b672a3
feat(console): web console MVP + /v1 OpenAPI spec (Phase 2)
tastyeffectco Jun 22, 2026
2bf9836
feat(console): route the console through Traefik by Host (console.<do…
tastyeffectco Jun 22, 2026
930b8e9
test(api): guard that /v1 routes and the OpenAPI spec stay in sync
tastyeffectco Jun 23, 2026
7eeb41a
chore(console): gitignore the local pnpm store
tastyeffectco Jun 23, 2026
edb5f6c
ci: run on the console integration branch too
tastyeffectco Jun 23, 2026
0469353
feat(config): app-scoped config and secrets, encrypted at rest
tastyeffectco Jun 23, 2026
d021ed4
test(config): PATCH preserves a secret without value; audit logs key …
tastyeffectco Jun 23, 2026
7c2baf1
merge: app-scoped config & secrets (#33) into console (v0.3.0)
tastyeffectco Jun 23, 2026
ba240c0
merge: web console + /v1 OpenAPI spec (#32) into console (v0.3.0)
tastyeffectco Jun 23, 2026
a45e4e2
docs(config): document app config & secrets in the /v1 OpenAPI spec +…
tastyeffectco Jun 23, 2026
8414ed2
feat(console): Config & Secrets panel on the app detail screen
tastyeffectco Jun 23, 2026
7f00ce7
feat(console): honest access policies + replace-secret action
tastyeffectco Jun 23, 2026
3eb9b98
test(config): consolidated no-leak CI safety guard for app secrets
tastyeffectco Jun 23, 2026
868b8a8
docs(console): document the optional web console in README + AGENTS
tastyeffectco Jun 23, 2026
67ee649
fix(console): give the Snapshot button visible feedback
tastyeffectco Jun 23, 2026
e1493a9
fix(console): app-list badge reflects real sandbox status
tastyeffectco Jun 23, 2026
4c5c464
feat(snapshots): app-scoped history, restore, and fork (Phase 4 backend)
tastyeffectco Jun 23, 2026
2e017dd
feat(console): snapshot history, restore, and fork on app detail (Pha…
tastyeffectco Jun 23, 2026
dc94138
docs(v0.4.0): note restore/fork orchestration tested, live preview NO…
tastyeffectco Jun 23, 2026
a8eac6e
docs(v0.4.0): Ubuntu isolated-host install script + test runbook
tastyeffectco Jun 23, 2026
2e20e77
feat(v0.4.0 installer): shared-host-safe defaults (uncommon ports, no…
tastyeffectco Jun 23, 2026
11a6ee7
fix(preview): include the host-facing port in preview URLs
tastyeffectco Jun 23, 2026
804c0d0
feat(observability): durable app/task event timeline (Phase 5)
tastyeffectco Jun 23, 2026
780a238
fix(observability): sanitize event payloads, monotonic ULIDs, prune i…
tastyeffectco Jun 23, 2026
1c5a9d7
merge: v0.4.0 snapshots/fork/restore (feat/snapshots-fork)
tastyeffectco Jun 23, 2026
fae9044
merge: v0.4.0 observability event timeline (feat/observability-events)
tastyeffectco Jun 23, 2026
f335a66
feat(runtime): sandbox.yaml manifest + generalized process model (Pha…
tastyeffectco Jun 23, 2026
36bf386
fix(runtime): harden the sandbox.yaml manifest core (Phase 7 safety)
tastyeffectco Jun 23, 2026
4d28718
docs(runtime): Phase 7 live e2e passed on rebuilt base image
tastyeffectco Jun 23, 2026
3034f67
feat(runtime): Phase 7B — process model in public API + console panel
tastyeffectco Jun 23, 2026
032b078
feat(runtime): Phase 7C-1 — runtime presets (template + sandbox.yaml …
tastyeffectco Jun 23, 2026
aeffad0
docs(runtime): accept Phase 7C-1 runtime presets (live-verified)
tastyeffectco Jun 23, 2026
9cda784
fix(preset): Next.js post-task .next poisoning; defer full 7C-1 accep…
tastyeffectco Jun 23, 2026
8e1d42d
feat(task): honest build/health semantics; document snapshot ignore-l…
tastyeffectco Jun 23, 2026
6bc68eb
feat(snapshot): ignore-list for generated/dependency dirs in capture …
tastyeffectco Jun 23, 2026
9bc2498
fix(manifest): explicit build.command "" skips the build check (relea…
tastyeffectco Jun 23, 2026
cdc7ae1
feat(runtime): web.restart_after_task — heal dev server poisoned by a…
tastyeffectco Jun 23, 2026
869cfc3
fix(loopback): normalize ownership on fork/restore (release blocker)
tastyeffectco Jun 23, 2026
b41fc1b
fix(preset): FastAPI on port 3000 + --reload (release blocker)
tastyeffectco Jun 24, 2026
08c73c2
feat(runtime): restart_after_task for node-express + worker reload gaps
tastyeffectco Jun 24, 2026
0e2d936
docs(runtime): accept Phase 7C-1 runtime presets (final acceptance)
tastyeffectco Jun 24, 2026
521707e
docs(release): v0.4.0 release notes for release/v0.4-apps-console
tastyeffectco Jun 24, 2026
ecd414f
test: pre-release test foundation (sandboxd-first; console is a client)
tastyeffectco Jun 24, 2026
d7e1986
test: tighten v0.4 test foundation (CI console job, delete wording, p…
tastyeffectco Jun 24, 2026
7090ae2
feat(console): Phase 8A — read-only Settings/Admin foundation
tastyeffectco Jun 24, 2026
37f06c2
feat(settings): Phase 8B — editable lifecycle tunables (safe, allowli…
tastyeffectco Jun 24, 2026
d5ea91d
docs(base-image): Custom Base Image Contract (v0.4, docs only)
tastyeffectco Jun 25, 2026
41f13fc
docs(release): v0.4.0 release pass — positioning, CHANGELOG, release …
tastyeffectco Jun 25, 2026
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
13 changes: 13 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ SANDBOXD_API_BIND=127.0.0.1:9090
SANDBOXD_API_AUTH_DISABLED=true
SANDBOXD_API_TOKENS=

# Master key for encrypting sensitive app config/secrets at rest
# (base64 of 32 bytes). Leave empty to auto-generate a 0600 keyfile at
# <data dir>/secrets.key. Back this up — losing it makes secrets
# unrecoverable; treat it as sensitive as the secrets themselves.
SANDBOXD_SECRETS_KEY=

# ── Resource policy (advanced) ───────────────────────────────────────
# Write cgroup v2 memory.high after start (soft throttle). Needs host
# cgroup access the control-plane container usually lacks, so it is OFF
Expand All @@ -56,3 +62,10 @@ SANDBOXD_SET_MEMORY_HIGH=false
# Idle window before a sandbox is stopped (docker stop) to free RAM.
# 2100s = 35 min. Workspace is preserved; next request wakes it.
SANDBOXD_IDLE_THRESHOLD_SECONDS=2100

# ── Observability (Phase 5) ──────────────────────────────────────────
# The durable activity timeline (app_events table) is append-only and
# lives in the control-plane SQLite DB. Retention is NOT pruned yet; the
# table is small at current volume. Future knob (not implemented):
# SANDBOXD_EVENT_RETENTION_DAYS — delete app_events older than N days.
# Until then the timeline grows unbounded; that's fine for current use.
28 changes: 27 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: ci

on:
push:
branches: [main]
branches: [main, console, "release/**"]
pull_request:

jobs:
Expand Down Expand Up @@ -61,3 +61,29 @@ jobs:
SANDBOXD_IMAGE: sandboxed-base:ci
SANDBOXD_BIN: /tmp/sandboxd
run: bash scripts/e2e.sh

# Console is a /v1 client: typecheck, unit-test, and build it directly (not
# only via docker) so a TS/test/build break is caught with clear output.
console:
runs-on: ubuntu-latest
defaults:
run:
working-directory: console
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: enable pnpm
run: corepack enable && corepack prepare pnpm@9.15.0 --activate
- name: install (frozen lockfile)
run: pnpm install --frozen-lockfile
- name: typecheck
run: pnpm exec tsc --noEmit
- name: unit tests
run: pnpm test
- name: build
run: pnpm build
- name: build console image
working-directory: .
run: docker build -t sandboxd-console:ci ./console
12 changes: 12 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ curl -s -XPOST $API/sandbox/$ID/exec -H 'content-type: application/json' \
docker exec -it -e ANTHROPIC_API_KEY=sk-ant-... s-$ID bash # then: claude
```

## Web console (optional)
A web UI over the public `/v1` API — apps, live preview, agent tasks + logs,
start/stop, and per-app config & secrets. Start it with the `console` profile:
```bash
docker compose --profile console up -d
```
Open `http://console.${PREVIEW_DOMAIN:-localhost}:${HTTP_PORT:-80}` (i.e.
http://console.localhost by default). It shares Traefik with the previews
(routed by Host: `console.<domain>`), talks only to `/v1`, never touches the DB
or workspaces, and needs no extra config in the single-user default. Plain
`docker compose up -d` omits it. Details: `console/README.md`.

## Operate
```bash
docker compose logs -f sandboxd # control-plane logs
Expand Down
13 changes: 12 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,20 @@ data directory mounted. It:

### runtimed (in-sandbox supervisor)
Built into the base image as the container's main process (`cmd/runtimed`). It
supervises the user's dev server and runs coding tasks submitted through the
supervises the user's processes and runs coding tasks submitted through the
API. It's compiled in the base image's build stage, so the host needs no Go.

On boot runtimed reads an optional **`sandbox.yaml`** (the runtime manifest) from
the app dir and runs a generalized process model: one optional `web` process
(previewed) plus N background `workers`, each supervised with restart-on-exit
backoff. No manifest = the default Vite app (backward compatible). It also runs a
post-task `build` check and, for runtimes without live reload, a per-process
`restart_after_task` that bounces the process after each task so agent edits go
live. **Runtime presets** (`internal/preset`) are the create-time bundles —
template + generated `sandbox.yaml` + capabilities — behind `GET /v1/presets` and
the `runtime_preset` create field. Process state and per-process logs are exposed
over the API. Schema + rules: `docs/sandbox-manifest.md`.

### Traefik (edge)
Docker label provider, scoped by a `sandboxd.managed=true` constraint so it
only routes containers this stack owns. Running sandboxes win on a
Expand Down
115 changes: 115 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,121 @@ All notable changes to sandboxd are documented here. The format is based on
[Semantic Versioning](https://semver.org/) (pre-1.0: a minor bump adds features,
a patch is fixes only).

## [0.4.0] — 2026-06-25

**Self-hosted control plane for AI-built apps** — adds a web console, runtime
presets, snapshots/fork/restore, observability, process logs, and a read-only
(plus editable lifecycle) settings view on top of the v0.3 app/config/secrets
foundation. Integration of all accepted v0.3 + v0.4 work, validated by a
from-zero VPS release-candidate QA pass.

### Highlights
Web console · runtime presets (React/Vite, Next.js, Node/Express, FastAPI,
Worker) · live preview URLs · agent tasks · app config & secrets (write-only
secrets) · snapshots / fork / restore · activity/events timeline · per-process
logs · settings with editable idle/keepalive lifecycle controls.

### Added
- **Snapshots, fork & restore.** Public `POST/GET/DELETE /v1/snapshots`,
app-scoped history `GET /v1/apps/{id}/snapshots`, `POST /v1/apps/{id}/restore`,
and `POST /v1/apps/{id}/fork` (with `source_app_id`). Restore replaces the
app's current sandbox; fork clones into a new app. Console gets a Snapshots
panel with confirm-gated restore/fork.
- **Snapshot ignore-list.** Snapshot capture excludes generated/dependency dirs
(`node_modules`, `.next`, `out`, `.venv`, `__pycache__`, `.cache`) so snapshots
and forks stay small and free of stale build output (symlink-safe copy).
- **Observability / activity timeline.** Durable `app_events` (centralized
recorder, monotonic ULIDs) surfaced at `GET /v1/apps/{id}/events` and rendered
as a newest-first Activity timeline in the console.
- **Runtime manifest (`sandbox.yaml`).** A generalized in-sandbox process model
(one optional `web` process + N `workers`) supervised by runtimed, with a
post-task build check. No manifest = the existing default Vite app.
- **Process API + logs.** `GET /v1/sandboxes/{id}` now includes `processes[]`
(name/kind/running/pid/restarts); `GET /v1/sandboxes/{id}/processes/{name}/logs`
tails a process's log (read-only, name-validated, tail-only). Console shows a
Processes panel and per-process logs; the preview pane reads "Preview /
endpoint" and worker-only apps show preview status `none` (valid, not an error).
- **Runtime presets.** `GET /v1/presets` and `runtime_preset` on
`POST /v1/apps`, `POST /v1/apps/{id}/sandbox`, `POST /v1/sandboxes`. Five
accepted presets, each booting and reloading after agent tasks:
**react-vite** (Vite HMR), **nextjs** (`restart_after_task`, heals an agent
`next build`), **node-express** (`restart_after_task`), **fastapi**
(port 3000 + `uvicorn --reload`), **worker** (no preview; editable `worker.sh`
+ `restart_after_task`). A New-App preset picker in the console.
- **Honest build/health semantics.** Task results expose `build_status`
(`passed`/`failed`/`skipped`), `preview_ok` (omitted for worker-only), and
`app_healthy`; `build_ok` stays for back-compat (true only when `passed`).
- **`web.restart_after_task` / worker `restart_after_task`.** Per-process
reload-by-restart for runtimes without live reload.
- **Settings / instance overview.** `GET /v1/settings` — a read-only, safe
instance summary (version, networking/preview, auth mode, runtime/storage,
lifecycle, egress mode, agent providers, presets, capabilities; never any
secret). A console **Settings** page renders it.
- **Editable lifecycle tunables.** `PATCH /v1/settings` edits ONLY the idle-reap
enable/threshold and keepalive-max (strict allowlist; any other key → 400),
persisted and **hot-applied** to the running reaper/keepalive; the console
Settings page edits them. Everything else stays read-only / env-managed.
- **Base image contract.** `docs/base-image.md` documents what a custom base
image must provide (runtimed, `sandbox` uid/gid 1000, workspace path, no
privileged/Docker socket) and how to select one via `SANDBOXD_IMAGE`.
- **Shared-host installer & preview port.** `scripts/dev/install-v04-ubuntu.sh`
with shared-host mode (loopback API, configurable `HTTP_PORT`), and preview
URLs that include the public port when `HTTP_PORT` ≠ 80.
- **Test foundation.** Public-surface + JSON-shape contract tests (sandboxd is
the contract), a console unit-test runner (vitest) with API-mirroring fixtures,
and `docs/release-checklist.md` for manual VPS sign-off.
- **Docs.** `docs/sandbox-manifest.md`, OpenAPI updates (`/v1/presets`,
`/v1/settings`, process logs, `runtime_preset`, `processes`, task health fields).

### Fixed
- **Explicit `build.command: ""` now skips the build check** (was overwritten by
the default `pnpm build`), so presets can disable build checks.
- **Next.js post-task `.next` poisoning** — the agent's `next build` no longer
leaves `next dev` serving 404/500 on `_next/static` (build skipped + `rm -rf
.next` on start + `restart_after_task`).
- **Fork/restore ownership** — restored/forked workspaces are normalized to the
sandbox user (uid 1000); apps no longer hit EACCES on `~/.cache`, deps, venv.

### Known limitations (non-blocking; see `docs/sandbox-manifest.md`)
- `DELETE /v1/sandboxes/{id}` **purges** the workspace while the legacy internal
`DELETE /sandbox/{id}` **stops and keeps** it — same verb, different data outcome.
- `keepalive_until` is set/honored but not surfaced in `GET /v1/sandboxes/{id}`.
- The wake/warming interstitial returns HTTP `200` (a status-only health check
can't tell "warming" from "ready").
- Per-task `agent.log` transcript can be empty on task timeout — persistence
needs investigation.
- **Docker backend only** — OCI/containerd/Kata are a future runtime provider.
- **Out of scope for v0.4:** Git/GitHub import, managed databases/sidecars, and
Docker-Compose-inside-the-sandbox.

---

> v0.4.0 rolls up the v0.3.0 work below (app config & secrets, web console +
> `/v1` OpenAPI) — kept here for detail.

## [Unreleased] — v0.3.0 (integration: `console`)

Backend and console landing together as one incremental release. Tracked on
the `console` integration branch; `main` stays at 0.2.0 until v0.3.0 is cut.

### Added
- **App-scoped config & secrets.** Per-app key/value config under
`/v1/apps/{id}/config` (`POST` / `GET`) and `/v1/apps/{id}/config/{key}`
(`PATCH` / `DELETE`). Sensitive values are AES-256-GCM-encrypted at rest and
write-only over the API (a `GET` returns metadata and `value_set`, never the
plaintext); non-sensitive values are returned. An `access_policy`
(`control_plane_only` default) records who may later read a value through the
broker. Documented in `docs/openapi.yaml`. (#33)
- **Web console + `/v1` OpenAPI spec.** An optional Vite/React console (served
through Traefik at `console.<domain>`) over the public `/v1` API, plus
`docs/openapi.yaml` and a contract test that keeps the spec and the routes in
sync. The app detail screen now includes a **Config & Secrets** panel. (#32)

### Note
- The secrets **broker** (Slice 2 — delivering values to agents/runtimes per
`access_policy`) is intentionally deferred and tracked separately; it does not
block this release.

## [0.2.0] — 2026-06-22

Reliability fixes across the core, and durable "apps" as first-class entities
Expand Down
75 changes: 73 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@


<p align="center">
<b>The open-source engine for AI app-builder products.</b><br/>
<b>Self-hosted control plane for AI-built apps.</b><br/>
Give every user an isolated cloud dev environment, a built-in coding agent,
and a live preview URL — self-hosted, on one machine, in one command.
and a live preview URL — with a web console to drive it all. Self-hosted, on
one machine, in one command.
</p>

<p align="center">
Expand All @@ -18,6 +19,17 @@

<img width="1100" height="816" alt="sandboxd-demo" src="https://github.com/user-attachments/assets/f794ff9b-8ffe-47e8-bd30-22541f870f09" />

## What you get

- **Web console** — create and open apps, watch previews, run agent tasks, manage everything from a browser (or drive the same `/v1` API directly).
- **Runtime presets** — one-step React/Vite, Next.js, Node/Express, FastAPI, or Worker apps (`GET /v1/presets`); each boots and reloads after agent edits.
- **Live preview URLs** — each app is reachable at its own shareable link; sleeps when idle, wakes on request.
- **Agent tasks** — submit a prompt, stream the agent's progress, get the build/health result.
- **App config & secrets** — per-app key/values; sensitive values are write-only (set once, encrypted at rest, never returned).
- **Snapshots / fork / restore** — capture a workspace, fork it into a new app, or restore in place.
- **Activity / events** — a durable per-app timeline of what happened.
- **Process logs** — per-process status (web + workers) and tail-able logs.
- **Settings / lifecycle controls** — a read-only instance overview, with editable idle-reap / keepalive tuning, applied live.

## What is sandboxd? (start here)

Expand Down Expand Up @@ -209,6 +221,50 @@ real domain you get `https://s-<id>-3000.preview.yourdomain.com`
> `curl -XPOST $API/sandbox/$ID/exec -d '{"cmd":["bash","-lc","cd ~/workspace/app && python3 -m http.server 3000"]}'`
> then open the same preview URL.

## Web console (optional UI)

Prefer a UI to curl? sandboxd ships an optional web console — a small React SPA
that talks **only** to the public `/v1` API. From it you can create and open
apps, watch the live preview, submit agent tasks and stream their logs,
start/stop the sandbox, and manage per-app **config & secrets** (sensitive
values are write-only: set once, never shown again).

```bash
docker compose --profile console up -d # core stack + console
```

Then open **http://console.localhost** (or `console.<PREVIEW_DOMAIN>:<HTTP_PORT>`
if you changed them). It's routed through the same Traefik as the previews, by
Host header — `console.<domain>` → console, `*.preview.<domain>` → previews — so
it shares one entrypoint, no extra port. Plain `docker compose up -d` (no
profile) runs sandboxd without the console.

The console never touches the database or workspaces — it's a pure `/v1` client
(contract in [`docs/openapi.yaml`](docs/openapi.yaml)). More detail:
[`console/README.md`](console/README.md).

## Runtime presets & `sandbox.yaml`

Create a working app of a common type in one step. `GET /v1/presets` lists the
built-in presets and you pass `runtime_preset` when creating an app/sandbox (the
console has a New-App picker):

| Preset | Serves | Post-task reload |
|---|---|---|
| `react-vite` | Vite SPA on :3000 | Vite HMR |
| `nextjs` | Next.js on :3000 | restart (also heals an agent `next build`) |
| `node-express` | Express API on :3000 (`/health`) | restart |
| `fastapi` | FastAPI on :3000 (`/health`) | `uvicorn --reload` |
| `worker` | no public endpoint | restart (editable `worker.sh`) |

A preset seeds starter files and writes a **`sandbox.yaml`** describing how the
app runs — its `web` process (command/port/health_path), background `workers`, a
post-task `build` check, and `restart_after_task`. Advanced users edit
`sandbox.yaml` directly; it lives in the workspace so snapshots preserve it.
Process status is on `GET /v1/sandboxes/{id}` (`processes[]`) and per-process
logs at `GET /v1/sandboxes/{id}/processes/{name}/logs`. Full schema:
[`docs/sandbox-manifest.md`](docs/sandbox-manifest.md).

## API

Base URL = `http://127.0.0.1:9090` (set by `SANDBOXD_API_BIND`). Auth is **off
Expand Down Expand Up @@ -320,6 +376,21 @@ Plain version:
machine**. Everything else above is a config change, not a rewrite. Start lean,
revisit these as you grow — and PRs are very welcome ([`CONTRIBUTING.md`](CONTRIBUTING.md)).

### Known limitations (v0.4.0)

Tracked, non-blocking — details in [`docs/sandbox-manifest.md`](docs/sandbox-manifest.md):

- `DELETE /v1/sandboxes/{id}` **purges** the workspace, while the legacy internal
`DELETE /sandbox/{id}` **stops and keeps** it.
- `keepalive_until` is honored but not surfaced in `GET /v1/sandboxes/{id}`.
- The wake/warming interstitial returns HTTP `200` (callers can't distinguish
"warming" from "ready" by status code alone).
- Per-task `agent.log` can be empty on task timeout (transcript persistence WIP).
- **Docker backend only** (OCI/containerd/Kata are a future provider; see
[`docs/sandbox-manifest.md`](docs/sandbox-manifest.md)).
- **Not yet:** Git/GitHub import, a managed-database/sidecar story, and
Docker-Compose-inside-the-sandbox are deliberately out of scope for v0.4.

## License

[MIT](LICENSE). Use it, ship it, sell what you build on it.
5 changes: 5 additions & 0 deletions console/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
dist
test-results
playwright-report
.git
6 changes: 6 additions & 0 deletions console/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
dist
test-results
playwright-report
.playwright
.pnpm-store
13 changes: 13 additions & 0 deletions console/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Build the SPA, then serve it from nginx with /v1 proxied to sandboxd.
FROM node:20-slim AS build
RUN corepack enable
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM nginx:stable-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
Loading
Loading