How Do I Run Dify in a Cloud Sandbox on a Private URL?
Updated October 27, 2026 .md
Short answer: Clone Dify's release into a default Sandboxer sandbox, set CONSOLE_API_URL, CONSOLE_WEB_URL, SERVICE_API_URL, APP_API_URL, APP_WEB_URL and FILES_URL to the hosted address, bind nginx to 127.0.0.1:8080, run docker compose up -d and sbx host 8080 --localhost. Callers send the __sbx_auth cookie plus Dify's app key.
At a glance#
One default sandbox (4 vCPU, 8 GB, 50 GB disk), Dify 1.18.0, October 27, 2026. Single runs, not benchmarks. No model provider was added: the test app is a workflow with a Code node, so it needs no LLM.
| Step | What we saw |
|---|---|
docker compose up -d (pull 12 images, start 16 containers) |
62 seconds |
Console API answering after up -d |
58 seconds |
| Memory with the stack idle | 2.8 GB used of 7.9 GB; api, worker, worker_beat and api_websocket about 500 MiB each |
| Disk | Images 8.0 GB; root disk went from 5.5 GB to 15 GB used |
| Private URL, no token | 401 from the Sandboxer proxy |
| Workflow API from a laptop, cookie + app key | 200; median 303 ms over 20 calls (min 284, max 345) |
sbx stop --wait, then sbx resume |
2.8 s and 2.0 s; memoryRestored: true, same container PIDs, API answered 0.3 s after resume returned |
sudo systemctl restart docker |
All containers came back (restart: always); API answered 48.6 s after the restart began |
Size it#
Dify's Docker Compose guide asks for at least 2 CPU cores and 4 GiB of RAM. small (2 vCPU, 4 GB) is exactly that minimum, and the idle stack already used 2.8 GB, so we used default ($0.036 per hour). Moving to a smaller size isn't supported yet, so pick the size up front. To get a smaller machine, create a new sandbox and copy dify/docker/volumes across.
sbx new --name dify --type default
id=sb_4odwgbje0l # from sbx new
# "subdomain":"broad-humble-crater" -> https://broad-humble-crater-8080.box.sandboxer.comThe hosted URL is https://<subdomain>-<port>.box.sandboxer.com, and the subdomain is in the sbx new output. It never changes across stop and resume, so you can write it into Dify's config before Dify starts.
Configure Dify for the hosted URL#
Dify builds absolute URLs from six variables in docker/.env. Per Dify's environment variable reference:
| Variable | What Dify uses it for |
|---|---|
CONSOLE_API_URL |
Public URL of the backend API. Also decides whether cookies are Secure. |
CONSOLE_WEB_URL |
Public URL of the console. Email links and OAuth redirects. |
SERVICE_API_URL |
The "API Base URL" the console shows developers. |
APP_API_URL |
Backend URL the published web apps call. |
APP_WEB_URL |
Where published web apps live. |
FILES_URL |
File preview and download links. |
Behind Sandboxer's proxy, all six are the same HTTPS address, because Dify's nginx routes /console/api, /api, /v1, /files and the web pages on one port. We set them with a script (setup.sh, copied in with sbx scp):
set -e
git clone -q --depth 1 --branch "$(curl -s https://api.github.com/repos/langgenius/dify/releases/latest | jq -r .tag_name)" https://github.com/langgenius/dify.git
cd dify/docker
cp .env.example .env
URL=https://broad-humble-crater-8080.box.sandboxer.com
set_env() { sed -i "s|^$1=.*|$1=$2|" .env; }
for k in CONSOLE_API_URL CONSOLE_WEB_URL SERVICE_API_URL APP_API_URL APP_WEB_URL FILES_URL TRIGGER_URL; do set_env $k $URL; done
set_env NEXT_PUBLIC_SOCKET_URL wss://broad-humble-crater-8080.box.sandboxer.com
set_env ENDPOINT_URL_TEMPLATE "$URL/e/{hook_id}"
set_env EXPOSE_NGINX_PORT 127.0.0.1:8080
set_env EXPOSE_NGINX_SSL_PORT 127.0.0.1:8443
cat > docker-compose.override.yaml <<YAML
services:
plugin_daemon:
ports: !override
- "127.0.0.1:5003:5003"
YAML
set_env DIFY_AGENT_LOCAL_SANDBOX_AUTH_TOKEN "$(openssl rand -base64 32)"
set_env SECRET_KEY "$(openssl rand -base64 42)"sbx scp setup.sh $id:/home/user/setup.sh
sbx exec $id "bash setup.sh"
sbx exec $id --detach "cd dify/docker && docker compose up -d > ~/up.log 2>&1"Notes from the run:
- Clone shallow. A full
git cloneof Dify ran pastsbx exec's default wait (exit124). With--depth 1it took 9 seconds. DIFY_AGENT_LOCAL_SANDBOX_AUTH_TOKENis required in 1.18. Dify's guide says the agent backend doesn't start without it.- Loopback binding.
EXPOSE_NGINX_PORT=127.0.0.1:8080works because the compose file uses it as the host side of the port mapping. Port 80 is free for something else, and nothing reaches Dify except through the hosted URL. - Don't put
127.0.0.1:inEXPOSE_PLUGIN_DEBUGGING_PORT. We tried, and theapicontainer went into a restart loop with:PLUGIN_REMOTE_INSTALL_PORT must be a bare port number, got '127.0.0.1:5003'. A 'host:port' value usually means EXPOSE_PLUGIN_DEBUGGING_PORT was set to a compose publish spec like '127.0.0.1:5003'; bind loopback via a docker-compose.override.yaml instead. The override withports: !overrideabove does that.
docker compose up -d pulled and started everything in 62 seconds. Database migrations run on the api container's first start, so poll until the console API answers:
sbx exec $id "until curl -sf http://127.0.0.1:8080/console/api/setup; do sleep 2; done"
# {"step":"not_started","setup_at":null} (58 s after up -d)Host port 8080#
sbx host $id 8080 --localhost --title dify
# https://broad-humble-crater-8080.box.sandboxer.com?_token=<token>--localhost is needed because nginx listens on 127.0.0.1 only; the sandbox relays the connection (see Hosting). From a laptop:
B=https://broad-humble-crater-8080.box.sandboxer.com
curl -s -o /dev/null -w "%{http_code}\n" $B/console/api/setup
# 401
curl -s -D - -o /dev/null "$B/install?_token=<token>" | grep -iE "^HTTP|^location|^set-cookie"
# HTTP/2 302
# location: /install
# set-cookie: __sbx_auth=<token>; Path=/; Max-Age=2592000; HttpOnly; Secure; SameSite=LaxIn a browser, open $B/install?_token=<token> once. The cookie lasts 30 days, and the page redirects to Dify's admin setup.
Set up the admin and sign in#
The first account created on /install becomes the workspace owner. Do it while the port is private. We used the same endpoints the setup page calls, from a laptop, with the Sandboxer cookie in a header:
SBX="Cookie: __sbx_auth=<token>"
curl -s -H "$SBX" -H "Content-Type: application/json" \
-d '{"email":"admin@example.com","name":"Admin","password":"<password>"}' $B/console/api/setup
# {"result":"success"} (201)Login was different. With the plain password, /console/api/login returned 401 {"code":"authentication_failed","message":"Invalid encrypted data"}. Dify 1.18's login form sends the password base64-encoded, so a script must do the same:
curl -s -c jar.txt -D - -H "$SBX" -H "Content-Type: application/json" \
-d "{\"email\":\"admin@example.com\",\"password\":\"$(printf %s '<password>' | base64)\",\"remember_me\":true}" \
$B/console/api/login | grep -i set-cookie
# set-cookie: __Host-csrf_token=…; Max-Age=3600; Secure; Path=/; SameSite=Lax
# set-cookie: __Host-access_token=…; Max-Age=3600; Secure; HttpOnly; Path=/; SameSite=Lax
# set-cookie: __Host-refresh_token=…; Max-Age=2592000; Secure; HttpOnly; Path=/; SameSite=LaxThe __Host- prefix and Secure flag come from CONSOLE_API_URL starting with https://. To check, we blanked the six URL variables and recreated the containers: login then set plain csrf_token, access_token and refresh_token cookies without Secure, and the console reported "api_base_url": null for apps. With the variables set, it reported https://broad-humble-crater-8080.box.sandboxer.com/v1. Later console calls send Dify's cookies, the __sbx_auth cookie in the same Cookie header, and the CSRF token in X-CSRF-Token.
Create an app that needs no model#
Dify's chat and agent apps need a model provider. We didn't add one. Instead we imported a workflow app in Dify's DSL: Start takes name, a Python Code node builds a greeting, End returns it. The Code node runs in Dify's own sandbox container.
app:
name: hello-sandbox
mode: workflow
description: Greets the caller with a Code node. No model needed.
kind: app
version: 0.7.0
workflow:
features: {}
environment_variables: []
conversation_variables: []
graph:
nodes:
- id: start
type: custom
position: {x: 0, y: 0}
data:
type: start
title: Start
variables:
- variable: name
label: name
type: text-input
required: true
max_length: 64
- id: code
type: custom
position: {x: 300, y: 0}
data:
type: code
title: Greet
code_language: python3
code: |
def main(name: str) -> dict:
return {"greeting": f"hello {name}", "length": len(name)}
variables:
- variable: name
value_selector: [start, name]
outputs:
greeting: {type: string, children: null}
length: {type: number, children: null}
- id: end
type: custom
position: {x: 600, y: 0}
data:
type: end
title: End
outputs:
- variable: greeting
value_selector: [code, greeting]
- variable: length
value_selector: [code, length]
edges:
- id: start-code
source: start
target: code
sourceHandle: source
targetHandle: target
- id: code-end
source: code
target: end
sourceHandle: source
targetHandle: targetSave it as hello.yml. In the console, use Import DSL file. Or call the endpoints the console uses: POST /console/api/apps/imports with {"mode":"yaml-content","yaml_content":"…"}, then POST /console/api/apps/<app_id>/workflows/publish and POST /console/api/apps/<app_id>/api-keys. The import answered "status":"completed". With version: 0.4.0 it answered "completed-with-warnings" and "current_dsl_version":"0.7.0", so match the version your Dify reports. The key call returned "token":"app-…".
Call the app's API from a laptop#
Two credentials go on every request: the Sandboxer cookie gets you through the proxy, and Dify's app key authorizes the call. Each check below hit /v1/workflows/run, the workflow run endpoint:
BODY='{"inputs":{"name":"Ada"},"response_mode":"blocking","user":"laptop-1"}'
curl -s -X POST -H "Authorization: Bearer app-…" -H "Content-Type: application/json" \
-d "$BODY" -o /dev/null -w "%{http_code}\n" $B/v1/workflows/run
# 401 (Sandboxer: no cookie, Dify never sees it)
curl -s -X POST -H "$SBX" -H "Content-Type: application/json" -d "$BODY" $B/v1/workflows/run
# {"code":"unauthorized","message":"Authorization header must be provided and start with 'Bearer'","status":401}
curl -s -X POST -H "$SBX" -H "Authorization: Bearer app-…" -H "Content-Type: application/json" \
-d "$BODY" $B/v1/workflows/run{"task_id": "fc1d67f2-…", "workflow_run_id": "c6f418f9-…", "data": {"status": "succeeded", "outputs": {"greeting": "hello Ada", "length": 3}, "error": null, "elapsed_time": 0.14384610700000167, "total_tokens": 0, "total_steps": 3, …}}total_tokens: 0 confirms no model ran. Twenty more blocking calls from the laptop (each a new TLS connection) all returned 200, median 303 ms. "response_mode":"streaming" streamed Server-Sent Events (workflow_started, node_started, text_chunk, node_finished, workflow_finished) through the proxy unchanged.
The published web app at $APP_WEB_URL/workflow/<code> loaded (<title>Dify</title>, 200) with the cookie and returned 401 without it. Anyone you share it with needs the token link first.
What survives stop and resume#
We noted the PIDs of the api, nginx and db_postgres containers, then:
sbx stop $id --wait # 2.8 s
sbx resume $id # 2.0 s
sbx info $id # memoryRestored: trueWhile stopped, the URL returned 404. After resume, a workflow run from the laptop succeeded 0.3 seconds after sbx resume returned. All three containers had the same PIDs and start times: nothing restarted. The hosted route and token were kept.
If a resume lands on a different CPU type, the sandbox boots from disk (files kept, processes lost), and sbx info shows memoryRestored: false. Then Docker, an enabled systemd service, starts at boot and brings back containers that have a restart policy. Every long-running service in Dify's compose file has restart: always. We didn't force a cold boot. To test that restart path, we restarted Docker inside the sandbox:
sbx exec $id "sudo systemctl restart docker"
# restart returned after 11856 ms
# v1/info 200 after 48560 msAll 15 services came back by themselves, and the workflow returned "hello after docker restart". The admin account, app and API key were still there, because they live in Postgres under dify/docker/volumes. We didn't time a real cold boot, so plan for at least this wait after one. The Docker Compose guide shows a real cold boot of a smaller stack.
FAQ#
Do I need an OpenAI or other model key to try Dify?#
No, for workflow apps built from Code, HTTP and template nodes, like the one above. Chat, agent and LLM nodes need a model provider. To keep it inside the sandbox, run a small model on CPU with Ollama and add it as an Ollama provider in Dify. We didn't test that part.
Why does the console call the wrong address, or show the wrong API base URL?#
One of the URL variables is still empty or points at localhost. Set all six to https://<subdomain>-8080.box.sandboxer.com, then run docker compose up -d again so the changed containers are recreated. Dify's guide says to run docker compose down first after changing variables.
Can a server call the Dify API without the Sandboxer cookie?#
Not while the port is private. Send Cookie: __sbx_auth=<token> along with Authorization: Bearer app-…. If a caller can't set that header, sbx host $id 8080 --localhost --public makes the port public, and Dify's app keys and login are then the only guard. Switching back to private mints a new token.
Does the URL change after stop or resume?#
No. The subdomain and hosted routes are kept, so the six URL variables stay valid. A fork gets a new subdomain and no routes, so change the variables and host the port again there.
Related#
More guides
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.
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.
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.
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.