# Which Way of Starting a Service Survives Resume, Reboot and Kill?

> Every start method survives a resume that restores memory. Only an enabled systemd unit with Restart=always, or a Docker container with --restart unless-stopped, also comes back after a cold boot, a reboot and a killed process. nohup, sbx exec --detach and systemd-run are gone after any boot.

Source: https://sandboxer.com/guides/run-a-service-that-comes-back-after-resume. Updated 2026-10-27.

**Short answer:** Every start method survives a resume that restores memory. Only an enabled systemd unit with Restart=always, or a Docker container with --restart unless-stopped, also comes back after a cold boot, a reboot and a killed process. nohup, sbx exec --detach and systemd-run are gone after any boot.

This page is the tested reference behind the "make it a service" step in our app recipes. We started the same tiny HTTP app five ways in one sandbox and broke it four ways. For what a resume restores, see [pause and resume with memory](/guides/pause-and-resume-a-sandbox-with-memory). For how an agent tells a warm resume from a cold boot, see [detect a cold boot after resume](/guides/detect-a-cold-boot-after-resume).

## The matrix

We ran this on prod on 2026-10-27, on a `small` sandbox ($0.018/hour) that became a `default` one in step 2. Each app served one file on its own port, bound to `127.0.0.1` and hosted with `sbx host --localhost`. "Back" means the private hosted URL returned `200`. Times are from one run each, polled every 0.5 seconds from a laptop, and start when the triggering command was sent.

| Started with | 1. Resume, memory restored | 2. Cold boot (resize) | 3. `sudo reboot` | 4. `kill -9` |
| - | - | - | - | - |
| `nohup … &` | Kept, same PID | Gone | Gone | Gone |
| `sbx exec --detach` | Kept, same PID | Gone (status `lost`) | Gone | Gone |
| `systemd-run -p Restart=always` | Kept, same PID | Gone (unit `inactive`) | Gone | Back in 0.7 s |
| Enabled unit, `Restart=always` | Kept, same PID | Back in 3.8 s | Back in 11.2 s | Back in 0.6 s |
| `docker run --restart unless-stopped` | Kept, same PID | Back in 3.8 s | Back in 12.1 s | Back in 0.7 s |

- **Resume with memory:** `sbx resume` took 1.35 s, and all five URLs answered on the first poll, 1.8 s after we sent it. Nothing restarted.
- **Cold boot:** `sbx resume --type default` took 3.4 s. The unit and the container answered on the first poll after it returned.
- **Reboot:** the URL of the unit was back 11.2 s after `sudo reboot`, the container's 0.9 s later.
- **Kill:** systemd and Docker each restarted the app once (`NRestarts=1`, `RestartCount=1`).

While nothing listens on the port, the hosted URL returns `502` with a "Nothing is answering" page. That's normal during a restart. Retry until you get `200`.

## The five ways we started it

The app is Python's built-in file server. Swap in your own command.

```bash
id=$(sbx new --type small --name my-svc --json | jq -r 'select(.event == "ready") | .id')
sbx exec $id 'mkdir -p ~/www && echo ok > ~/www/index.html'

# 1. nohup
sbx exec $id 'nohup python3 -m http.server 8001 --bind 127.0.0.1 --directory /home/user/www > ~/web1.log 2>&1 &'

# 2. a detached command
sbx exec $id --detach 'python3 -m http.server 8002 --bind 127.0.0.1 --directory /home/user/www'

# 3. a transient systemd unit
sbx exec $id 'sudo systemd-run --unit web3 --uid user -p Restart=always python3 -m http.server 8003 --bind 127.0.0.1 --directory /home/user/www'

# 5. Docker with a restart policy
sbx exec $id --timeout 300 'docker run -d --name web5 --restart unless-stopped -p 127.0.0.1:8005:8005 -v /home/user/www:/www:ro python:3.12-alpine python -m http.server 8005 --directory /www'
```

```text
Running as unit: web3.service; invocation ID: 8b47e1f12ddb42179735a13c5a965bb4
…
e6025768dffda4a246d6fd390087f4481bcd1c15e26fccce98d735299c8df44f
```

The `nohup` process outlived the `sbx exec` call that started it. [`systemd-run`](https://www.freedesktop.org/software/systemd/man/latest/systemd-run.html) makes a transient unit: `Restart=always` works on it, but the unit isn't written to disk, so a boot forgets it. Inside the container the app binds `0.0.0.0`, and `-p 127.0.0.1:8005:8005` keeps it on localhost in the sandbox.

## The minimal unit file that survives all four

This is method 4, as we ran it:

```bash
sudo tee /etc/systemd/system/web4.service >/dev/null <<'UNIT'
[Unit]
Description=Tiny web app
After=network-online.target
Wants=network-online.target

[Service]
User=user
ExecStart=/usr/bin/python3 -m http.server 8004 --bind 127.0.0.1 --directory /home/user/www
Restart=always

[Install]
WantedBy=multi-user.target
UNIT
sudo systemctl daemon-reload
sudo systemctl enable --now web4
```

```text
enabled
active
Created symlink /etc/systemd/system/multi-user.target.wants/web4.service → /etc/systemd/system/web4.service.
```

Each line has a job:

| Line | Without it |
| - | - |
| `WantedBy=multi-user.target` + `enable` | The unit runs now but doesn't start on the next boot |
| `Restart=always` | A crash or kill leaves it stopped ([`Restart=`](https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#Restart=)) |
| `User=user` | It runs as root, and files it writes in `/home/user` belong to root |

Put it on a URL once. The route and its token are kept across stop, resume and reboot:

```bash
sbx host $id 8004 --localhost
```

```json
{"ok":true,"type":"port.hosted","sandboxId":"sb_jfvxy0tfuo","port":8004,"url":"https://direct-broad-otter-8004.box.sandboxer.com?_token=…","access":"private","isProtected":true,"localhost":true}
```

Without the token, the URL returned `401`. With it, `200`.

To see why a unit restarted, read its journal with `sudo`. As `user` you see the app's own output only, not systemd's messages. Sandboxes created before 10:44 UTC on 2026-10-27 also need `-m`; it does no harm on newer ones:

```bash
sbx exec $id 'sudo journalctl -m -u web4 --no-pager -n 4'
```

```text
Oct 27 10:50:05 direct-broad-otter systemd[1]: web4.service: Main process exited, code=killed, status=9/KILL
Oct 27 10:50:05 direct-broad-otter systemd[1]: web4.service: Failed with result 'signal'.
Oct 27 10:50:05 direct-broad-otter systemd[1]: web4.service: Scheduled restart job, restart counter is at 1.
Oct 27 10:50:05 direct-broad-otter systemd[1]: Started web4.service - Tiny web app.
```

## How we broke it, and what to check

**1. Resume with memory.** Stop, then resume:

```bash
sbx stop $id --wait
sbx resume $id
sbx info $id --json | jq -c '.sandbox | {memoryRestored, resumeReason}'
```

```json
{"memoryRestored":true,"resumeReason":null}
```

All five PIDs were unchanged and `uptime -s` still showed the original boot time.

**2. Cold boot.** A resume boots from disk when it can't restore memory: on a different CPU type, or when you change the size. The size change is the one you can trigger:

```bash
sbx stop $id --wait
sbx resume $id --type default
sbx info $id --json | jq -c '.sandbox | {type, memoryRestored, resumeReason}'
```

```json
{"type":"default","memoryRestored":false,"resumeReason":"resized"}
```

Moving to a smaller size isn't supported yet, so this leaves you on the bigger size. Test it on a throwaway sandbox. The detached command reported its fate:

```bash
sbx exec $id --status 559
```

```json
{"ok":true,"type":"command.status",…,"pid":559,"processId":559,"running":false,…,"status":"lost",…}
```

**3. Reboot.** This tests the same boot path and keeps the size:

```bash
sbx exec $id 'sudo reboot'
```

```text
{"event":"error","error":"The machine did not answer.","code":"sandbox_direct_failed","status":502,…}
```

That error is expected: the agent that ran the command went down with the VM. A reboot inside the guest doesn't change `memoryRestored`. It still read `false` / `resized` from the last resume, so use the boot id to spot reboots (see [detect a cold boot](/guides/detect-a-cold-boot-after-resume)).

**4. Kill.** We killed all five apps at once:

```bash
sbx exec $id 'sudo pkill -9 -f "http.server 800[1-5]"'
sbx exec $id 'systemctl show web3 web4 -p Id -p NRestarts; docker inspect web5 --format "RestartCount={{.RestartCount}}"'
```

```text
NRestarts=1
Id=web3.service

NRestarts=1
Id=web4.service
RestartCount=1
```

## Which one to use

| You want | Use |
| - | - |
| A job that ends, and you check its exit code | `sbx exec --detach` ([long jobs](/guides/run-a-long-job-in-a-cloud-sandbox)) |
| A quick server for this session only | `nohup` or `sbx exec --detach` |
| A server that must survive anything short of deletion | An enabled unit with `Restart=always` |
| An app that ships as an image | `docker run --restart unless-stopped`, or Compose with `restart: unless-stopped` |

Under Docker's [`unless-stopped` policy](https://docs.docker.com/engine/containers/start-containers-automatically/), a container you stop yourself with `docker stop` stays stopped, even after a reboot.

## FAQ

### Do processes started with nohup survive a sandbox stop?

Yes, when the resume restores memory: all five of our apps kept their PIDs. They don't survive a cold boot or a reboot, because nothing starts them again.

### How do I force a cold boot to test my service?

Run `sudo reboot` inside the sandbox; it keeps the size. From outside, `sbx resume --type <bigger size>` always boots from disk, but you can't go back to the smaller size afterwards.

### How long until the hosted URL works after a reboot?

In our run, 11.2 seconds after `sudo reboot` for the systemd unit and 12.1 seconds for the Docker container, on a `default` sandbox. Until then the URL returns `502` "Nothing is answering"; retry.

### Does systemd-run with Restart=always survive a reboot?

No. It restarted the app after `kill -9` in 0.7 seconds, but after the cold boot and the reboot the unit was gone. Write a unit file and `enable` it instead.

## Related

- [Detect a cold boot after resume](/guides/detect-a-cold-boot-after-resume)
- [Pause and resume a sandbox with memory](/guides/pause-and-resume-a-sandbox-with-memory)
- [Run a long job in a cloud sandbox](/guides/run-a-long-job-in-a-cloud-sandbox)
- [Services that should always run](/docs/long-running#services-that-should-always-run)
