Skip to content
Guides · Recipes

How to Run WordPress in a Cloud Sandbox With an HTTPS URL

Updated October 27, 2026 .md

Short answer: Run the official wordpress and mariadb images with Docker Compose in a Sandboxer sandbox, bind WordPress to 127.0.0.1 and host it with sbx host --localhost. Install with WP-CLI using the hosted HTTPS URL as the site URL. The image already trusts X-Forwarded-Proto, so login, wp-admin and permalinks work.

What we ran#

Everything below ran on production on October 27, 2026, in one small sandbox (2 vCPU, 4 GB): WordPress 7.1.3 from the wordpress:7.1-php8.3-apache image, MariaDB 11.8 and WP-CLI 2 from wordpress:cli-2-php8.3. The requests came from a laptop, single runs unless noted.

Step Result
sbx new --type small 3.2 s to ready
docker compose up -d (pulls both images) 19.8 s
wp core install 2.0 s
wp plugin install classic-editor --activate 3.1 s
Post page through the hosted URL, 20 requests 288 ms median (279 to 583 ms)
Same page from inside the sandbox 69 ms median
/wp-admin/ through the hosted URL, 10 requests 335 ms median
Memory used by both containers about 220 MB
sbx stop --wait then sbx resume 2.2 s stop, 1.3 s resume, memoryRestored: true, still logged in
Resume with --type default (boots from disk) Both containers back in under 10 s, site and wp-admin 200

1. The Compose file#

Docker and Compose are preinstalled in every sandbox. This file follows the official WordPress image docs and adds three things: WordPress is published on 127.0.0.1 only, both services restart on their own, and a cli service runs WP-CLI against the same files and database.

# wp/compose.yaml
x-wp-env: &wp-env
  WORDPRESS_DB_HOST: db
  WORDPRESS_DB_NAME: wordpress
  WORDPRESS_DB_USER: wp
  WORDPRESS_DB_PASSWORD: ${DB_PASSWORD}

services:
  db:
    image: mariadb:11.8
    restart: unless-stopped
    environment:
      MARIADB_DATABASE: wordpress
      MARIADB_USER: wp
      MARIADB_PASSWORD: ${DB_PASSWORD}
      MARIADB_RANDOM_ROOT_PASSWORD: "1"
    volumes:
      - db:/var/lib/mysql
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 5s
      retries: 30

  wordpress:
    image: wordpress:7.1-php8.3-apache
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "127.0.0.1:8080:80"
    environment: *wp-env
    volumes:
      - wp:/var/www/html

  cli:
    image: wordpress:cli-2-php8.3
    profiles: ["cli"]
    user: "33:33"
    depends_on:
      - wordpress
    environment:
      <<: *wp-env
      HOME: /tmp
    volumes:
      - wp:/var/www/html

volumes:
  db:
  wp:

Why these choices:

  • 127.0.0.1:8080:80. The sandbox has a public IPv6 address. Its firewall (ufw) is closed except SSH, but Docker can bypass ufw rules on some setups. Binding to 127.0.0.1 keeps WordPress reachable only through the hosted URL, whatever the firewall does.
  • user: "33:33" on cli. The CLI image runs as a different user ID than the Apache image, whose files belong to www-data (33). Without it, WP-CLI can't write plugins into the shared volume.
  • The cli environment repeats the database variables. The image's wp-config.php reads them at run time, so WP-CLI needs them too.
  • The service_healthy condition waits for MariaDB before WordPress starts. healthcheck.sh ships in the mariadb image.

2. Start it#

Copy the folder in, write random passwords to files (they are never printed), and start the stack:

sbx new --name wordpress --type small
id=sb_p4esgbgh3g    # the id that sbx new printed

sbx scp -r ./wp $id:/home/user/
sbx exec $id --timeout 600 "cd wp && umask 077 \
  && echo DB_PASSWORD=\$(openssl rand -hex 16) > .env \
  && openssl rand -hex 12 | tr -d '\n' > admin-pass \
  && docker compose up -d --quiet-pull \
  && docker compose ps --format 'table {{.Service}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'"
 Container wp-db-1 Healthy
 Container wp-wordpress-1 Starting
 Container wp-wordpress-1 Started
SERVICE     IMAGE                         STATUS                   PORTS
db          mariadb:11.8                  Up 5 seconds (healthy)   3306/tcp
wordpress   wordpress:7.1-php8.3-apache   Up Less than a second    127.0.0.1:8080->80/tcp

3. Host the port before you install#

WordPress stores its own address in the database. So get the URL first, then install with it.

sbx host $id 8080 --localhost --title wordpress
{"ok":true,"type":"port.hosted","sandboxId":"sb_p4esgbgh3g","port":8080,"url":"https://crisp-copper-lime-8080.box.sandboxer.com?_token=<token>","access":"private","isProtected":true,"localhost":true}

--localhost makes the sandbox relay to 127.0.0.1:8080, which is where Compose published it. The URL is private: keep the _token secret. See Hosting for public URLs.

4. Install WordPress with WP-CLI#

Save this as wp/install.sh and copy it in. It passes the admin password from the file, so it never shows in the output:

#!/bin/bash
# usage: bash install.sh https://<subdomain>-8080.box.sandboxer.com
set -e
cd ~/wp
docker compose run -T --rm cli wp core install --url="$1" --title="Sandbox blog" \
  --admin_user=admin --admin_email=admin@example.com --skip-email \
  --admin_password="$(cat admin-pass)"
sbx scp wp/install.sh $id:/home/user/wp/install.sh
sbx exec $id --timeout 300 "bash wp/install.sh https://crisp-copper-lime-8080.box.sandboxer.com"
sbx exec $id "cd wp && docker compose run -T --rm cli wp option get siteurl"
Success: WordPress installed successfully.
https://crisp-copper-lime-8080.box.sandboxer.com

Use the base URL, without ?_token=. Our first try used --prompt=admin_password to read the password from stdin. WP-CLI echoed the password back in its output, so we switched to the form above.

Read the password with sbx exec $id "cat wp/admin-pass" when you log in.

5. The two settings that make it work behind a proxy#

siteurl and home. WordPress redirects every request to the host in its home option. To show what goes wrong, we set both options to http://localhost:8080, as a local install would:

/            301 -> https://crisp-copper-lime-8080.box.sandboxer.com:8080/
/wp-admin/   302 -> https://localhost:8080/wp-login.php?redirect_to=…&reauth=1

The home page sent the browser to port 8080 on the hosted name, and wp-admin sent it to localhost. Neither loads. If you moved a site in, fix every stored URL with wp search-replace:

sbx exec $id "cd wp && docker compose run -T --rm cli wp search-replace \
  http://localhost:8080 https://crisp-copper-lime-8080.box.sandboxer.com --all-tables --report-changed-only"
Table	Column	Replacements	Type
wp_options	option_value	2	PHP
Success: Made 2 replacements.

Both paths then returned 200.

HTTPS behind the proxy. TLS ends at the Sandboxer edge, so Apache in the sandbox sees plain HTTP. The hosting proxy keeps the original Host header and sends X-Forwarded-Proto: https. The official image's wp-config.php already turns that header into $_SERVER['HTTPS'] = 'on':

sbx exec $id "docker compose -f wp/compose.yaml exec -T wordpress grep -n -A2 HTTP_X_FORWARDED_PROTO /var/www/html/wp-config.php"
122:if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strpos($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') !== false) {
123-	$_SERVER['HTTPS'] = 'on';
124-}

We commented out line 123 to see what happens without it:

src="http://crisp-copper-lime-8080.box.sandboxer.com/wp-includes/js/hoverintent-js.min.js?ver=2.2.1"
/wp-admin/        302 -> https://crisp-copper-lime-8080.box.sandboxer.com/wp-admin/
login POST        302 -> https://crisp-copper-lime-8080.box.sandboxer.com/wp-login.php

Pages linked their scripts over http://, which browsers block as mixed content. wp-admin redirected to itself in a loop, and the login form sent us back to the login page. If you build your own image or use another WordPress setup, add those three lines near the top of wp-config.php. WordPress documents the same fix in Administration over SSL. We restored the file before going on.

6. Log in and open wp-admin through the URL#

We checked the private URL and the login with curl from a laptop. $U is the base URL, $T the token, and pass holds the admin password:

curl -s -o /dev/null -w "no token: %{http_code}\n" $U/
curl -s -c jar -b jar -o /dev/null -w "with token: %{http_code}\n" "$U/?_token=$T"
curl -s -c jar -b jar -o /dev/null $U/wp-login.php    # sets WordPress's test cookie
curl -s -c jar -b jar -o /dev/null -w "login: %{http_code} -> %{redirect_url}\n" \
  --data-urlencode log=admin --data-urlencode "pwd@pass" \
  --data-urlencode "redirect_to=$U/wp-admin/" --data testcookie=1 $U/wp-login.php
curl -s -b jar $U/wp-admin/ | grep -o '<title>[^<]*'
no token: 401
with token: 302
login: 302 -> https://crisp-copper-lime-8080.box.sandboxer.com/wp-admin/
<title>Dashboard &lsaquo; Sandbox blog &#8212; WordPress

The token request sets a cookie and redirects to /, so a browser needs the token only once. The home page's links all use https://crisp-copper-lime-8080.box.sandboxer.com.

The image ships Apache with mod_rewrite and writes the standard .htaccess, so changing the structure is one command:

sbx exec $id --timeout 300 "cd wp
docker compose run -T --rm cli wp rewrite structure '/%postname%/'
docker compose run -T --rm cli wp post create --post_title='Hello from a sandbox' --post_status=publish --porcelain
docker compose run -T --rm cli wp plugin install classic-editor --activate
docker compose run -T --rm cli wp plugin list --fields=name,status,version"
Success: Rewrite structure set.
Success: Rewrite rules flushed.
6
Installing Classic Editor (1.7.0)
…
Plugin 'classic-editor' activated.
Success: Installed 1 of 1 plugins.
name	status	version
akismet	inactive	5.7.2
classic-editor	active	1.7.0
hello	inactive	1.7.2

Through the hosted URL, /hello-from-a-sandbox/ returned 200, /?p=6 returned 301 to it, /wp-json/wp/v2/posts/6 returned the post with "link": "https://crisp-copper-lime-8080.box.sandboxer.com/hello-from-a-sandbox/", and /wp-admin/plugins.php listed Classic Editor.

The post page took 288 ms median through the URL and 69 ms median from inside the sandbox. Most of the difference is the network path from our laptop to the sandbox, which depends on where you are.

8. Stop and resume#

sbx stop $id --wait
sbx resume $id
sbx exec $id "cat /proc/uptime; cd wp && docker compose ps --format '{{.Service}} {{.Status}}'"
164.74 283.15
db Up 2 minutes (healthy)
wordpress Up 2 minutes

sbx info showed memoryRestored: true. The containers never restarted, and the same cookie jar still opened /wp-admin/plugins.php with no new login. The URL and token stayed the same.

A resume boots from disk when no machine with the same CPU type is free or when you resize. To test that path, we stopped again and resumed with sbx resume $id --type default:

type default memoryRestored False resumeReason resized
2.59 6.20
db Up 9 seconds (healthy)
wordpress Up 9 seconds

An uptime of 2.6 seconds shows a fresh boot. Docker started with the VM and restart: unless-stopped brought both containers back. The post, the plugin and the login cookie all survived, because they live in the volumes and the database. See Snapshots for the resume rules.

9. Clean up#

sbx delete $id --yes

After the delete, sbx info returned not_found and the URL returned 404. A small sandbox costs $0.018/hour while it runs and nothing while it is stopped.

FAQ#

Why does WordPress redirect to localhost or to port 8080?#

The home and siteurl options hold a different address than the one in your browser. Install with --url set to the hosted URL, or run wp search-replace from the old URL to the new one across all tables.

Why does wp-admin loop or load without styles behind the URL?#

WordPress doesn't know the request was HTTPS. The official image handles this with an X-Forwarded-Proto check in wp-config.php. Other setups need the same three lines, and Sandboxer's proxy always sends that header.

Can I make the site public?#

Yes: sbx host $id 8080 --localhost --public. Switching between private and public mints a new token for private links. Keep the WordPress admin password strong, because anyone can then reach /wp-login.php.

Do I need the web installer?#

No. wp core install does the same in about 2 seconds and lets you script the site URL, admin user and plugins. We didn't test the browser installer.

Is small enough?#

For a test site, yes. Both containers used about 220 MB of the 4 GB, and wp-admin pages answered in about a third of a second through the URL. Size up for heavy plugins or imports.

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