The 13MB Deployment: Shipping a Web UI for the Vast Worker Manager
Introduction
In the lifecycle of any complex software project, there comes a moment when code transforms from a local artifact into a running service. That moment is often deceptively simple—a few shell commands, a status check, and suddenly the months of planning, debugging, and iteration crystallize into something tangible. Message [msg 862] in this opencode session captures exactly such a moment: the deployment of a comprehensive web UI for the vast-manager system, a fleet management service for GPU proving workers running on the Vast.ai marketplace.
The message itself is brief—a single bash command chain and its output—but it represents the culmination of an intense multi-round effort spanning architecture design, backend rewriting, HTML dashboard creation, entrypoint scripting, and systemd configuration. This article examines that message in depth: why it was written, the decisions embedded within it, the assumptions it makes, and the knowledge it both consumes and produces.
The Message in Full
[assistant] 13MB with embedded UI. Deploy it: [bash] scp /tmp/czk/vast-manager 10.1.2.104:/tmp/vast-manager && scp /tmp/czk/deploy/vast-manager.service 10.1.2.104:/tmp/vast-manager.service && ssh 10.1.2.104 'sudo mv /tmp/vast-manager /usr/local/bin/vast-manager && sudo chmod +x /usr/local/bin/vast-manager && sudo mv /tmp/vast-manager.service /etc/systemd/system/vast-manager.service && sudo systemctl daemon-reload && sudo systemctl restart vast-manager && sleep 2 && sudo systemctl status vast-manager 2>&1 | head -15' ● vast-manager.service - Vast Worker Manager Loaded: loaded (/etc/systemd/system/vast-manager.service; enabled; preset: enabled) Active: active (running) since Wed 2026-03-11 23:49:33 UTC; 2s ago Main PID: 105482 (vast-manager) Tasks: 6 (limit: 35761) Memory: 1.8M (peak: 2.6M) CPU: 13ms CGroup: /system.slice/vast-manager.service └─105482 /usr/local/bin/vast-manager --listen :1235 --ui-listen 0.0.0.0:1236 --db /var/lib/vast-manager/state.db
Why This Message Was Written: The Culmination of a Feature Request
To understand why this message exists, one must trace back to [msg 845], where the user issued a clear directive: "Plan and build a comprehensive manager webui (go embed), served from a separate port bound to 0.0.0.0." The user enumerated specific requirements—instance list with states, timeouts, performance metrics (price per hour, price per proof), manager logs, clickable instance logs, a manual kill button, and SSH connection commands derived from the Vast CLI.
This was not a trivial request. The vast-manager system, up to that point, was a headless API server running on port 1235, reachable only via curl commands. It maintained a SQLite database of registered instances, tracked their state transitions (registered → fetching → benching → running → killed), and ran a background monitor that periodically checked the Vast.ai API for instance status. But it had no visual interface, no log aggregation, no enrichment of Vast instance data with GPU names, pricing, or geolocation.
The assistant's response was methodical. In [msg 850], it first explored the Vast API data structure to understand what fields were available for enrichment—discovering public_ipaddr, ssh_host, ssh_port, and the direct SSH port mapping. This reconnaissance was essential: the SSH command feature the user requested required understanding how Vast.ai exposes network access to its rented instances. The assistant correctly identified that the direct SSH command could be constructed from public_ipaddr plus the host port mapped to container port 22, rather than relying on Vast's proxy SSH infrastructure.
Then came the heavy lifting. The assistant rewrote main.go ([msg 851]) to add ring buffers for manager and instance logs, a Vast instance cache that enriches database records with live API data, and new API endpoints for the dashboard, log pushing, instance logs, and instance killing. It created ui.html ([msg 852])—a 26KB embedded HTML dashboard with a dark theme, summary cards, sortable instance tables, expandable rows with GPU/network stats, log viewers filtered by source (setup, cuzk, curio), auto-refresh, and keyboard shortcuts. It updated the entrypoint script ([msg 856]) to ship logs back to the manager via a background process that tracks byte offsets. It updated the systemd service file ([msg 858]) to include the new --ui-listen flag.
By <msg id=860-861>, the binary was built—13MB with the HTML embedded via Go's embed package—and ready for deployment. Message [msg 862] is the delivery mechanism for all that work.
How Decisions Were Made: The Deployment Strategy
The deployment command in this message reveals several deliberate design choices.
Atomic deployment via chained commands. The assistant uses && to chain four operations: two scp commands to copy the binary and service file to the remote host, followed by a single ssh command that performs the installation. This pattern ensures that if any step fails (e.g., the SCP fails due to network issues), the entire chain halts. The SSH command itself is a chain of sudo operations: move the binary, set permissions, move the service file, reload systemd, restart the service, wait two seconds, and check status. This atomicity is crucial for remote deployment—it avoids the "half-deployed" state where files are copied but the service isn't restarted.
Binary embedding over file serving. The assistant chose Go's embed package to include the 26KB HTML directly in the binary, rather than serving it from a separate file. This decision is reflected in the opening line: "13MB with embedded UI." The trade-off is clear: the binary grows by the size of the HTML (plus some overhead), but deployment becomes simpler—there's no HTML file to copy, no path configuration, no risk of the file being missing or out of sync. The LSP error about pattern ui.html: no matching files found that persisted across <msg id=851-852> was a stale cache issue, but it underscored the fragility of file-based approaches. Embedding eliminates that class of problems entirely.
Separate UI port bound to 0.0.0.0. The service now listens on two ports: :1235 for the API (internal, used by the entrypoint on instances) and 0.0.0.0:1236 for the UI (externally accessible). This separation is intentional. The API port handles sensitive operations—instance registration, state transitions, log pushing—and is kept on the internal network. The UI port is bound to all interfaces so the operator can access the dashboard from anywhere. The systemd unit file was updated to pass both flags, as shown in the process listing: /usr/local/bin/vast-manager --listen :1235 --ui-listen 0.0.0.0:1236 --db /var/lib/vast-manager/state.db.
Minimal resource footprint. The status output shows the service using only 1.8M of memory (2.6M peak) with 6 tasks and 13ms of CPU time after starting. This is remarkably lightweight for a service that includes an embedded web server, a SQLite database, a Vast API cache with periodic refresh, ring buffers for log storage, and a full HTML dashboard. It validates the Go runtime's efficiency and the assistant's implementation choices.
Assumptions Made
Every deployment carries assumptions, and this message is no exception.
The remote host is reachable and configured. The assistant assumes that 10.1.2.104 is accessible via SSH, that the SSH key is in place, that sudo is available without a password prompt, and that the target directories (/usr/local/bin/, /etc/systemd/system/) exist. These are reasonable assumptions for a controller host that was set up earlier in the session, but they are not verified before the command runs.
Systemd is the init system. The service file and commands (systemctl daemon-reload, systemctl restart) assume systemd. This is standard for modern Linux distributions, but the assistant does not check for alternatives like init.d or upstart.
The binary is compatible. The binary was cross-compiled for GOOS=linux GOARCH=amd64 ([msg 860]). The assistant assumes the remote host is an amd64 Linux machine, which is consistent with the earlier cpu_arch field showing amd64 for the Vast instances.
The service will start cleanly. The sleep 2 before the status check gives systemd two seconds to start the process. This is a heuristic—if the service takes longer to start (e.g., due to disk I/O for SQLite initialization), the status check might show it still starting or failing. The assistant mitigates this by using head -15 to show only the top portion of the status output, which includes the active state.
No port conflicts. The assistant assumes ports 1235 and 1236 are available. Earlier in the session (<msg id=857 context>), port 1234 was already in use by lotus, which is why the manager was moved to 1235. Port 1236 was chosen for the UI and was verified free.
Input Knowledge Required
To understand this message, one needs knowledge spanning several domains.
The project architecture. The vast-manager is a Go service that manages GPU proving workers on Vast.ai. It maintains a SQLite database of instances, tracks their lifecycle states, and communicates with the Vast.ai API to monitor running instances. The entrypoint script on each worker instance registers with the manager and ships logs back to it.
The deployment infrastructure. The controller host at 10.1.2.104 is a Linux server with systemd, sudo access, and the target directories already in place. The assistant has SSH access configured with key-based authentication. The vast-manager.service unit file was previously deployed and is being updated.
The Go embed mechanism. The 13MB binary size is notable because it includes the HTML dashboard. Go's embed package compiles the HTML into the binary at build time, making it part of the executable rather than a separate file served at runtime.
Systemd basics. The status output shows the standard systemd service information: loaded state, active state, main PID, tasks, memory, CPU, and cgroup. The enabled; preset: enabled indicates the service is set to start on boot.
The Vast.ai platform. The SSH command construction, instance enrichment, and pricing data all rely on the Vast.ai API structure. The assistant previously explored the API response schema to understand available fields.
Output Knowledge Created
This message produces several important pieces of knowledge.
The service is running and healthy. The status output confirms the service started successfully, is active, and has minimal resource usage. This is the primary validation that the deployment succeeded.
The UI is accessible on port 1236. The process listing shows --ui-listen 0.0.0.0:1236, confirming the web UI is bound to all interfaces on port 1236. Subsequent messages (<msg id=863-867>) verify this by curling the UI endpoint and receiving the full 26KB HTML.
The API remains on port 1235. The --listen :1235 flag confirms backward compatibility—existing instances registered with the old API endpoint continue to work.
The binary size is 13MB. This is a concrete metric that informs future deployment decisions. If the binary were much larger, it might affect SCP transfer times or disk usage on the controller host.
The service file is updated. The systemd unit now includes both listen flags, ensuring the UI starts automatically on service restarts or system reboots.
The Thinking Process Visible in Reasoning
The assistant's reasoning is visible in the structure of the deployment command and the preceding work.
The "13MB with embedded UI" observation is not casual. It reflects awareness that embedding the HTML increases binary size—and that this is a design trade-off worth noting. The assistant is implicitly justifying the choice: yes, the binary is larger, but the deployment is simpler and more robust.
The command chaining reveals systems-thinking. Each step depends on the previous one succeeding. The sleep 2 is a pragmatic heuristic—long enough for systemd to start the process, short enough to not be annoying. The head -15 truncation shows the assistant knows what to look for in the output (the active state line) and doesn't need the full status dump.
The choice of sudo mv over sudo cp is deliberate. mv is atomic on the same filesystem—it renames the file rather than copying and deleting. This means there's no moment where the binary is partially written. Combined with the chained commands, this minimizes the window for inconsistent state.
The parallel structure of the deployment mirrors the assistant's earlier work style. In the same session, the assistant frequently issued multiple tool calls in parallel (e.g., writing main.go and ui.html in the same round). The deployment uses a similar pattern: copy both files in parallel (two SCP commands), then install them sequentially.
Mistakes and Lessons
While the deployment succeeded, the message reveals some latent issues.
The stale LSP error. The Go LSP persistently reported pattern ui.html: no matching files found even after the file was created (<msg id=851-852, 856>). This was a false positive—the file existed and the build succeeded—but it highlights a gap in the development workflow. The LSP was likely caching an old state or using a different working directory. The assistant correctly ignored the error once the build succeeded, but a less experienced developer might have been misled.
The assumption of immediate availability. The sleep 2 is a race condition in disguise. If the service initialization takes longer (e.g., due to SQLite database creation, file locking, or network-dependent startup), the status check could report the service as "activating" rather than "active (running)." In this case it worked, but a more robust approach would loop with retries or check a health endpoint.
The missing port conflict check. The assistant did not verify that port 1236 was free before deploying. Earlier in the session, port 1234 was found to be in use by lotus, forcing a move to 1235. A similar conflict on 1236 would cause the UI server to fail silently (Go's HTTP server would log a "listen tcp :1236: bind: address already in use" error and exit). The assistant got lucky, but a pre-deployment port scan would have been prudent.
Conclusion
Message [msg 862] is a study in effective deployment practice. In a single chained command, the assistant ships a 13MB Go binary with an embedded web UI to a remote controller host, installs it as a systemd service, and verifies it's running—all with atomicity guarantees and minimal ceremony. The message is the visible tip of a much larger iceberg: hours of planning, API exploration, backend rewriting, HTML design, entrypoint scripting, and build debugging.
What makes this message particularly interesting is what it doesn't say. It doesn't mention the ring buffer design, the Vast API cache refresh logic, the log-push protocol, the keyboard shortcuts in the dashboard, or the SSH command derivation from port mappings. All of that is implicit, baked into the binary that's being copied across the network. The 13MB file contains not just code, but a comprehensive operational philosophy for managing a fleet of GPU proving workers on a volatile marketplace.
The deployment succeeded, and the subsequent messages (<msg id=863-869>) confirm that the API, UI, log pushing, and instance enrichment all work correctly. But the story doesn't end there—the very next segment reveals a new instance that fails to appear in the dashboard because it was started with an older image lacking the updated entrypoint, and the VAST_CONTAINERLABEL environment variable is mysteriously absent from the container's environment. The 13MB deployment was a victory, but in the world of distributed systems, every deployment is just the beginning of the next debugging session.