Skip to content
Guides · Recipes

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.com

The 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 clone of Dify ran past sbx exec's default wait (exit 124). With --depth 1 it took 9 seconds.
  • DIFY_AGENT_LOCAL_SANDBOX_AUTH_TOKEN is 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:8080 works 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: in EXPOSE_PLUGIN_DEBUGGING_PORT. We tried, and the api container 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 with ports: !override above 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=Lax

In 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=Lax

The __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: target

Save 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: true

While 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 ms

All 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.

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.

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