# How to Run the Temporal Dev Server in a Cloud Sandbox

> Install the Temporal CLI, run temporal server start-dev with --db-filename under systemd, and host the Web UI on port 8233 with sbx host --localhost. On a small sandbox the server was ready in 1.5 s and a one-activity workflow took about 100 ms. History survived stop, resume and a cold boot.

Source: https://sandboxer.com/guides/run-temporal-dev-server-in-a-sandbox. Updated 2026-10-27.

**Short answer:** Install the Temporal CLI, run temporal server start-dev with --db-filename under systemd, and host the Web UI on port 8233 with sbx host --localhost. On a small sandbox the server was ready in 1.5 s and a one-activity workflow took about 100 ms. History survived stop, resume and a cold boot.

## At a glance

Everything below ran on production on October 27, 2026: Temporal CLI 1.9.1 (Server 1.32.0, UI 2.54.1) and the Python SDK `temporalio` 1.34.0, first on a `small` sandbox (2 vCPU, 4 GB), then resized to `default` (4 vCPU, 8 GB). Single runs unless noted; your numbers will differ.

| Step | Result |
| - | - |
| Install the Temporal CLI | 1.3 s |
| Server start to `SERVING` (3 restarts) | 1.43, 1.53, 1.49 s |
| One workflow with one activity, inside the sandbox (50 runs) | median 98 ms, min 47 ms |
| Same workflow from a laptop through `sbx forward` (20 runs) | median 254 ms |
| Web UI on a private hosted URL | 401 without the token, 302 then 200 with it |
| Memory after start | server 185 MB RSS, Python worker 60 MB |
| `sbx stop --wait` then `sbx resume` (memory restored) | Server and worker kept running; history intact |
| Resume on a bigger size (boots from disk) | systemd restarted both; all 54 workflows still there |
| Restart without `--db-filename` | 1 workflow before, 0 after |
| Timer that came due while stopped | Fired about 96 s after a memory-restored resume; within 1 to 2 s after a server restart or cold boot |

The last row is the one to read before you rely on timers. It is explained in [Timers that come due while the sandbox is stopped](#timers-that-come-due-while-the-sandbox-is-stopped).

## 1. Install the CLI and run the server under systemd

Temporal's dev server is a single binary inside the Temporal CLI ([install the CLI](https://docs.temporal.io/cli/setup-cli), [`server start-dev` reference](https://docs.temporal.io/cli/server#start-dev)). Use its own install script:

```bash
sbx new --name temporal --type small --no-env
id=sb_0fcf3wsqh3    # the id that sbx new printed

sbx exec $id "curl -sSf https://temporal.download/cli.sh | sh && ~/.temporalio/bin/temporal --version"
```

```text
temporal: Temporal CLI installed at /home/user/.temporalio/bin/temporal
temporal version 1.9.1 (Server 1.32.0, UI 2.54.1)
```

Run it as a systemd unit, not a detached shell command, so it comes back when the sandbox boots from disk. Save this as `temporal.service`:

```ini
[Unit]
Description=Temporal dev server
After=network-online.target

[Service]
User=user
ExecStart=/home/user/.temporalio/bin/temporal server start-dev --ip 127.0.0.1 --db-filename /home/user/temporal.db --ui-disable-news-fetch
Restart=always

[Install]
WantedBy=multi-user.target
```

```bash
sbx scp ./temporal.service $id:/home/user/
sbx exec $id "sudo mv temporal.service /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl enable --now temporal"
sbx exec $id "until ~/.temporalio/bin/temporal operator cluster health 2>/dev/null | grep -q SERVING; do sleep 0.2; done; ss -ltn | grep -E '7233|8233'"
```

```text
LISTEN 0      4096       127.0.0.1:7233       0.0.0.0:*
LISTEN 0      4096       127.0.0.1:8233       0.0.0.0:*
```

Three choices in that unit:

- `--db-filename`. Without it, the dev server keeps everything in memory. We checked: a second server without the flag had 1 workflow before a restart and 0 after. With the flag, history lives in a SQLite file on the sandbox disk (1.7 MB after 56 workflows). It didn't slow anything down: 50 workflows took a median 98 ms with the file and 100 ms without it.
- `--ip 127.0.0.1`. The gRPC port (7233) and the UI (8233) listen inside the VM only. The Web UI has no login in dev mode (`"Auth":{"Enabled":false}` in its settings), so you reach it through a private hosted URL (step 3), and the gRPC port through a tunnel (step 4).
- `--ui-disable-news-fetch`. The UI stops calling out for its news feed. Optional.

## 2. Run a workflow

Save the workflow, the worker and a starter. `ReminderWorkflow` is a durable timer; you will use it in step 5.

```python
# workflows.py
from datetime import timedelta
from temporalio import activity, workflow


@activity.defn
async def greet(name: str) -> str:
    return f"Hello, {name}!"


@workflow.defn
class HelloWorkflow:
    @workflow.run
    async def run(self, name: str) -> str:
        return await workflow.execute_activity(
            greet, name, start_to_close_timeout=timedelta(seconds=10)
        )


@workflow.defn
class ReminderWorkflow:
    @workflow.run
    async def run(self, minutes: int) -> str:
        started = workflow.now()
        await workflow.sleep(timedelta(minutes=minutes))
        waited = (workflow.now() - started).total_seconds()
        return f"woke after {waited:.0f} s of workflow time"
```

```python
# worker.py
import asyncio
from temporalio.client import Client
from temporalio.worker import Worker
from workflows import HelloWorkflow, ReminderWorkflow, greet


async def main():
    client = await Client.connect("127.0.0.1:7233")
    worker = Worker(client, task_queue="demo",
                    workflows=[HelloWorkflow, ReminderWorkflow], activities=[greet])
    print("worker started on task queue 'demo'", flush=True)
    await worker.run()

asyncio.run(main())
```

```python
# starter.py
import asyncio, os, statistics, sys, time, uuid
from temporalio.client import Client
from workflows import HelloWorkflow


async def main():
    client = await Client.connect(os.environ.get("TEMPORAL_ADDRESS", "127.0.0.1:7233"))
    n = int(sys.argv[1]) if len(sys.argv) > 1 else 1
    times = []
    for i in range(n):
        t0 = time.perf_counter()
        result = await client.execute_workflow(
            HelloWorkflow.run, "sandbox", id=f"hello-{uuid.uuid4().hex[:8]}", task_queue="demo")
        times.append((time.perf_counter() - t0) * 1000)
    print(result)
    print(f"{n} workflows, median {statistics.median(times):.1f} ms, "
          f"min {min(times):.1f} ms, max {max(times):.1f} ms")

asyncio.run(main())
```

Install the SDK ([Temporal Python SDK](https://docs.temporal.io/develop/python)) in a virtual environment and run the worker as a second unit. `Requires=temporal.service` restarts the worker whenever you restart the server:

```ini
# temporal-worker.service
[Unit]
Description=Temporal worker (task queue demo)
After=temporal.service
Requires=temporal.service

[Service]
User=user
WorkingDirectory=/home/user/app
ExecStart=/home/user/app/.venv/bin/python worker.py
Restart=always
RestartSec=2

[Install]
WantedBy=multi-user.target
```

```bash
sbx exec $id "mkdir -p app"
for f in workflows.py worker.py starter.py temporal-worker.service; do sbx scp ./$f $id:/home/user/app/; done
sbx exec $id --timeout 600 "cd app && python3 -m venv .venv && .venv/bin/pip install -q temporalio"
sbx exec $id "sudo mv app/temporal-worker.service /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl enable --now temporal-worker"
sbx exec $id "cd app && .venv/bin/python starter.py 50"
```

```text
Hello, sandbox!
50 workflows, median 98.0 ms, min 47.0 ms, max 169.9 ms
```

That time covers start, a workflow task, the activity, a second workflow task and the result. The `pip install` took 2.5 s; `sbx exec` stops a command after 30 s by default, so keep `--timeout 600` on installs.

The CLI talks to the same server:

```bash
sbx exec $id "export PATH=\$PATH:~/.temporalio/bin
temporal workflow start --type ReminderWorkflow --task-queue demo --workflow-id reminder-1 --input 3
temporal workflow list --limit 3"
```

```text
   Status      WorkflowId          Type          StartTime
  Running    reminder-1      ReminderWorkflow  now
  Completed  hello-6a249bde  HelloWorkflow     8 seconds ago
  Completed  hello-846bcce8  HelloWorkflow     8 seconds ago
```

## 3. Open the Web UI on a private URL

The UI listens on `127.0.0.1:8233`, so host it with `--localhost` ([hosting docs](/docs/hosting#apps-that-listen-on-localhost)):

```bash
sbx host $id 8233 --localhost --title "Temporal UI"
```

```text
{"ok":true,"type":"port.hosted","sandboxId":"sb_0fcf3wsqh3","port":8233,"url":"https://direct-humble-cabin-8233.box.sandboxer.com?_token=<token>","access":"private","isProtected":true,"localhost":true}
```

Open that URL in a browser. We checked it with curl, using a cookie jar for the session cookie that the token sets:

```bash
B=https://direct-humble-cabin-8233.box.sandboxer.com
curl -s -o /dev/null -w "no token: %{http_code}\n" $B/
curl -s -o /dev/null -w "with token: %{http_code}\n" -c jar.txt "$B/?_token=<token>"
curl -s -b jar.txt "$B/api/v1/namespaces/default/workflows?query=WorkflowType%3D%22ReminderWorkflow%22"
```

```text
no token: 401
with token: 302
{
  "executions": [
    {
      "execution": {
        "workflowId": "reminder-1",
…
      "status": "WORKFLOW_EXECUTION_STATUS_RUNNING",
```

The UI's own `/api/v1` passes through the same URL, which is how the browser UI lists, describes and can cancel or terminate workflows. Anyone with the full URL can do all of that, so keep it private. `--public` would hand that control to anyone who finds the hostname.

## 4. Connect from your laptop

A worker or client on your laptop needs the gRPC port, not the UI. Forward it with [`sbx forward`](/docs/ssh#forward-ports) and point the client at the local port:

```bash
sbx forward $id --remote 7233 --local 17233 &
TEMPORAL_ADDRESS=127.0.0.1:17233 python starter.py 20
```

```text
Hello, sandbox!
20 workflows, median 254.1 ms, min 253.1 ms, max 262.2 ms
```

The extra 155 ms is the network path from our laptop to the sandbox, which depends on where you are. For the worker, keep it next to the server in the sandbox, as above.

## 5. What survives stop and resume

We ran three cases, each with a `ReminderWorkflow` sleeping when the sandbox stopped.

**Memory restored.** `sbx stop $id --wait` (2.2 s) and `sbx resume $id` (1.5 s, `memoryRestored: true`). systemd's `ActiveEnterTimestamp` for the server was unchanged: the processes were never restarted, the history was all there, and the hosted URL and its cookie worked again. While the sandbox was stopped, the URL answered `404`.

**Boot from disk.** `sbx resume $id --type default` (`memoryRestored: false`, `resumeReason: "resized"`). Both units started at boot, the server read `temporal.db`, and `temporal workflow count` still said `Total: 54`. This is the case `--db-filename` and systemd are for. It also happens when no machine with the same CPU type is free. Moving to a smaller size isn't supported yet; to shrink, create a new sandbox and copy `temporal.db`.

**Without `--db-filename`.** A restart alone emptied the server. A boot from disk would do the same.

### Timers that come due while the sandbox is stopped

No timer was lost in any run. But in our runs on October 27, 2026, a timer that came due while the sandbox was stopped didn't always fire at once. This is what we observed, not documented behavior:

| Timer | Stopped for | How the sandbox came back | Fired |
| - | - | - | - |
| 3 min | 2 min 49 s | Memory restored | 96 s after resume |
| 3 min | 2 min 58 s | Memory restored | 96 s after resume |
| 1 min | 1 min 33 s | Memory restored | within 2 s of resume |
| 1 min | 1 min 14 s | Boot from disk | within 1 s of the server starting |
| 3 min | 2 min 49 s | Memory restored, then `sudo systemctl restart temporal` | 2 s after the restart |

The general rule: after a resume with memory, timers based on elapsed time don't count the stopped time; restart time-sensitive services or schedule by wall clock. In our session, `uptime -s` moved 108 s later across a 169 s stop, so elapsed time inside the VM counted only about 61 s of it. The server's in-process wake-up for the next timer runs on elapsed time, so it came late. On a fresh start, the server reads its due timers from the database and fires the overdue ones at once.

The workflow result shows the effect: `workflow.now()` is wall-clock time, so a 3-minute reminder reported `woke after 296 s of workflow time`.

If timers must fire promptly after a resume, restart the server as part of your resume step. It takes about 1.5 s, and the worker restarts with it:

```bash
sbx resume $id
sbx exec $id "sudo systemctl restart temporal"
```

## 6. Clean up

```bash
sbx delete $id --yes
```

A `small` sandbox bills at half the `default` rate of $0.036/hour while it runs, and nothing while it's stopped.

## FAQ

### Is the dev server fit for production?

No. Temporal documents `start-dev` as a local development server with a SQLite store. Use it for development, tests, demos and agent experiments. For production, use Temporal Cloud or a self-hosted cluster as described in [Temporal's docs](https://docs.temporal.io/).

### Which size do I need?

`small` (2 vCPU, 4 GB) was enough here. After about 120 workflows, the server used 185 MB of memory and the Python worker 60 MB. Size up for your own activities, not for the server.

### Why does the UI return 401?

The hosted URL is private by default. Open the full URL with `?_token=` once; it sets a 30-day cookie and the UI works after that. A request without the token never reaches Temporal.

### Do I lose my workflow history when the sandbox auto-stops?

Not with `--db-filename`. Auto-stop is a normal stop: a memory-restored resume keeps the running server, and a boot from disk reloads the SQLite file. Without the flag, history survives only a memory-restored resume.

### Can my laptop's worker poll the sandbox server?

Yes, through `sbx forward` to port 7233, as in step 4. The hosted URL is HTTPS for the UI; we didn't test gRPC through it.

## Related

- [Run a long job in a cloud sandbox](/guides/run-a-long-job-in-a-cloud-sandbox)
- [Give a port in a sandbox an HTTPS URL](/guides/give-a-sandbox-port-an-https-url)
- [Pause and resume a sandbox with memory](/guides/pause-and-resume-a-sandbox-with-memory)
- [Snapshots and resume](/docs/snapshots)
