Deploy your first Virtual Server
Chapter 5 · about 8 minutes to read
This is chapter 5 of Stage 2 — the "aha moment" chapter. You're going to ask Zyra to start a real container on the Compute Node you just enrolled and watch it transition from creating to running with a working access URL, all in about a minute.
Time: about 10 minutes including reading. Prerequisites: at least one Compute Node in Ready state, Docker available on that node (it almost always is by now — Docker Desktop on Windows/macOS, Docker Engine on Linux).
What a Virtual Server actually is
A Virtual Server (VS) is a persistent Docker container managed by the Zyra orchestrator. It has a lifecycle (creating → pulling_image → starting → running), a target Compute Node, declared resources, and zero or more access endpoints (web terminal, SSH, RDP). Schema-wise it's the virtual_servers table in backend/app/models/virtual_server.py: name, docker_image, vcpus, memory_mb, disk_gb, exposed_ports, volumes, target_device_id, status.
Unlike a job (chapter 6), a Virtual Server is designed to keep running indefinitely. You stop it when you're done.
Step 1 — Open the create form
In the dashboard sidebar, click Virtual Servers → Create Virtual Server. The button is also one of the welcome cards on the empty-state dashboard you saw in Stage 1, and it unlocks the moment your first Compute Node is in Ready state.
Step 2 — Fill in the form
The form maps directly to the VirtualServerCreate schema. Required fields:
- Name — anything 1-255 characters. Try
first-testfor this walkthrough. - Docker image — the container image to run. For your first deploy, use one of these known-working images:
nginx:latest— smallest, fastest, lowest-risk first deploy. Comes up on port 80.scottyhardy/docker-remote-desktop:latest— a known-working RDP image used in Zyra's own production validation. Larger pull (~2 GB) but gives you a full desktop session.ubuntu:24.04with no command — sits idle, useful for testing the web terminal.
- Target Compute Node — pick the device from chapter 4. You can also leave it as "Any available" and the scheduler will pick the highest-capability Ready device.
- vCPUs — 1-128, default 2. Two is fine for the first deploy.
- Memory (MB) — 512-131072 (128 GB), default 2048.
- Disk (GB) — 10-4096, default 100.
Optional fields:
- Exposed ports — list of
{container_port, host_port, protocol}tuples. Fornginx:latest, add one: container 80, host 8080, TCP. - Volumes — named persistent volumes mounted into the container. Skip for the first deploy.
- Enable web terminal — on by default. Lets you
docker execinto the container from your browser. - Enable SSH / Enable RDP — opt-in toggles. Skip unless you picked the docker-remote-desktop image, in which case turn RDP on.
Step 3 — Click Deploy
The dashboard calls POST /api/v1/virtual-servers/ with your form values. The backend (backend/app/routers/virtual_servers/create.py) validates the payload, creates the virtual_servers row in creating status, and dispatches deploy_to_device(vs, db) which messages the agent on the target Compute Node.
You're redirected to the VS detail page. The status pill cycles through states you can watch in real time:
creating— row inserted, dispatch sent.pulling_image— agent isdocker pull-ing the image. This is the slow step for big images (the RDP image is ~2 GB).creating_container— image pulled, agent callsdocker createwith the resource limits.starting—docker startissued.running— container is up. The detail page now shows the connect URL.
For nginx:latest the whole pipeline typically completes in 20-30 seconds. For the RDP image, 60-90 seconds on a typical home upload pipe.
Step 4 — Connect
Once the status is running, the Connect panel shows up:
- Web terminal — opens a browser-based shell inside the container (via the
ContainerAccessHandleron the agent side). - HTTP — if you exposed port 8080, you'll see a link like
https://app.getzyra.io/vs/{vs_id}/proxy/8080/. For nginx, opening it shows the default "Welcome to nginx!" page running on hardware you own. - RDP — if enabled, a one-click launcher that downloads an
.rdpfile with the credentials pre-filled.
This is the moment. You asked for compute, Zyra found a device, pulled an image, started a container, and routed a connect URL back to you — and the device is sitting under your desk or in your office rack, not in a hyperscaler datacenter.
Step 5 — Check the meter
Scroll to the Cost panel. The default rate is $0.10/hour (stored as cost_per_hour Numeric(10,6) on the virtual_servers row). The meter increments while the VS is running and stops when you stop or terminate it. Stage 3 chapter 4 covers invoices in detail; for now, just notice that the meter started the moment you clicked Deploy.
What just happened
You ran a real, network-reachable Docker container on a device your organization controls. The agent honoured the CPU/RAM/disk caps you set in chapter 4. The orchestrator routed your connect URL through the Zyra control plane so you don't need to expose ports on the Compute Node itself. Billing is per-second, prorated to the configured hourly rate.
Troubleshooting
- Stuck on
pulling_imagefor minutes. Big images on slow uplinks, or a Docker Hub rate limit on the Compute Node. Watch the agent log forpulling image …andpull rate limit exceededlines. creating_container→error_message: Cannot connect to the Docker daemon. Docker isn't running on the Compute Node, or the agent user isn't in thedockergroup. Fix and re-deploy; the existing row will retry.- Web terminal won't connect. The
ContainerAccessHandleropens a WebSocket back to the backend; corporate proxies that stripUpgradeheaders will block it. Try from an unfiltered network. - "No Ready devices match constraints". The scheduler couldn't find a Compute Node with enough free vCPU/RAM/disk. Check the capability caps you set in chapter 4 — they may be tighter than this VS needs.
Last reviewed: 2026-05-21