Skip to content
Guides · Agent cookbook

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

Updated October 27, 2026 .md

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. For how an agent tells a warm resume from a cold boot, see 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.

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'
Running as unit: web3.service; invocation ID: 8b47e1f12ddb42179735a13c5a965bb4
…
e6025768dffda4a246d6fd390087f4481bcd1c15e26fccce98d735299c8df44f

The nohup process outlived the sbx exec call that started it. systemd-run 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:

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
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=)
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:

sbx host $id 8004 --localhost
{"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:

sbx exec $id 'sudo journalctl -m -u web4 --no-pager -n 4'
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:

sbx stop $id --wait
sbx resume $id
sbx info $id --json | jq -c '.sandbox | {memoryRestored, resumeReason}'
{"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:

sbx stop $id --wait
sbx resume $id --type default
sbx info $id --json | jq -c '.sandbox | {type, memoryRestored, resumeReason}'
{"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:

sbx exec $id --status 559
{"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:

sbx exec $id 'sudo reboot'
{"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).

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

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}}"'
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)
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, 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.

More guides

Top AI sandbox platforms for code execution in 2026, ranked

For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.

Read the guide
Best sandboxes for running coding agents (Claude Code, Codex, Gemini CLI) in 2026, ranked

For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.

Read the guide
Best Daytona sandbox alternatives in 2026, ranked

The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.

Read the guide
Best E2B alternatives in 2026, ranked

If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.

Read the guide