Docs/Docker Local Development

Docker Local Development

Run your PhantomWP site on your own machine using Docker as an alternative to GitHub Codespaces.

Docker Local Development

PhantomWP can run your generated Astro site entirely on your own computer inside a single Docker container. The dashboard and IDE still live at phantomwp.com in your browser, but the dev server, terminal, git, and file operations all run locally. This is a first-class alternative to GitHub Codespaces and is perfect if you want:

  • Zero Codespace billing
  • Full offline editing once the project is downloaded
  • The exact same IDE experience you already know
  • Access to your project files on your local filesystem (for VS Code, Claude Code, or other local tools)

How It Works

Browser (phantomwp.com) | |-- IDE connects to ws://localhost:<ws-port> (WebSocket, file ops, terminal, git) |-- Preview iframe loads http://localhost:<astro-port> | Docker Container (ghcr.io/phantomwp/local-dev) |-- tini (PID 1, proper signal forwarding) |-- PM2 | |-- Astro dev server (:4321 inside container) | |-- WebSocket server (:8080 inside container) | |-- Named volume or bind mount at /app (persistent) |-- Git repository cloned or initialized on first boot

One container runs two services under PM2:

  1. Astro dev server β€” serves your site with hot reload
  2. WebSocket server β€” the same server the Codespace uses for file operations, terminal sessions, git, and code formatting

Your browser connects to both over localhost. Nothing about your project is stored on PhantomWP servers β€” the code lives only on your machine and in your GitHub repository.

πŸ’‘

The dashboard still runs at phantomwp.com. Only the development runtime (code, preview, terminal) moves to your computer. This keeps your login, billing, AI features, and GitHub integration working normally.

Prerequisites

No command line knowledge required beyond pasting a single docker run command the first time.

First-Time Setup

Step 1: Pick Docker as the run mode

When creating a new project, select Run Locally (instead of Codespace or Fly.io) in the environment picker. For existing projects, open the dashboard and click Start Locally on your project card.

PhantomWP generates a one-time setup token (valid for 2 hours, single use) and shows you a ready-to-paste docker run command.

Step 2: Run the command

The command looks like this:

docker run --name phantomwp-my-site \
  -p 14321:4321 \
  -p 14322:8080 \
  -v phantomwp-my-site:/app \
  -e PHANTOMWP_TOKEN=<your-token> \
  ghcr.io/phantomwp/local-dev:<release-tag>

What each part does:

FlagPurpose
--name phantomwp-my-siteNames the container for easy start/stop/remove
-p 14321:4321Maps the Astro dev server to http://localhost:14321
-p 14322:8080Maps the WebSocket server to ws://localhost:14322
-v phantomwp-my-site:/appPersistent volume holding your project files
-e PHANTOMWP_TOKEN=…One-time setup token for fetching your project

Paste it into Terminal (macOS/Linux) or PowerShell / Windows Terminal (Windows). On the first run the container will:

  1. Pull the released ghcr.io/phantomwp/local-dev image (Node 22, PM2, git, node-pty)
  2. Call POST /api/local/init on phantomwp.com with your setup token
  3. Either clone your GitHub repository (if one exists for the project) or write the generated template files to /app
  4. Cache the JWT public key that authenticates IDE WebSocket traffic
  5. Configure git remotes and credentials for push/pull from the IDE
  6. Install npm dependencies (uses a pre-cached layer for speed)
  7. Start Astro + WebSocket server under PM2

Step 3: Open the editor

Back in the browser, click Open Editor. The IDE auto-detects local mode and connects directly to your container over ws://localhost:<ws-port>. The preview iframe loads http://localhost:<astro-port>.

πŸ’‘

The first time Chrome or Edge sees a request from phantomwp.com to localhost, it may ask you to allow local network access. Click allow β€” this is how the browser connects to the container you just started.

Mapping to a Local Folder (Optional)

By default the setup command uses a named Docker volume (-v phantomwp-my-site:/app). Your files live inside Docker and are easiest to reach from the PhantomWP IDE itself.

If you want to open the project in VS Code, Cursor, Claude Code, or any other local tool, you can tick "Map to a local folder" in the setup screen. That replaces the named volume with a bind mount:

-v ~/phantomwp/my-site:/app

With a bind mount, all your project files appear as normal files in the chosen folder on your machine, and any edit you make there is reflected instantly in the container (and in the PhantomWP IDE preview).

Pick one or the other β€” don't use both at the same time for the same project.

Daily Workflow

After the first run the container and its data persist. You never need the setup token again. Start, stop, and view logs just like any Docker container:

# Start
docker start phantomwp-my-site

# Stop
docker stop phantomwp-my-site

# View logs
docker logs -f phantomwp-my-site

Most users do this visually in Docker Desktop β†’ Containers: one click to start, one click to stop.

Multiple sites at once

Each site gets its own pair of ports assigned by the PhantomWP API, starting at 14321:

  • Site A: Astro on 14321, WebSocket on 14322
  • Site B: Astro on 14323, WebSocket on 14324

So you can keep several local sites running simultaneously, each in its own container and volume.

Multiple browser tabs

The IDE supports multiple simultaneous browser tabs or windows connected to the same container. Terminal sessions are shared: open a terminal in one tab, reconnect from another, and you get the same session with scrollback preserved.

What's Inside the Container

ComponentPurpose
Node.js 22Runtime
PM2Process manager for Astro + WebSocket
tiniPID 1 init, proper signal forwarding
node-ptyFull terminal emulation in the IDE
gitVersion control from the IDE terminal
ripgrepFast code search
Prettier + Astro pluginCode formatting

The WebSocket server

The same ws-server.js that powers Codespaces runs inside the container. It handles:

  • File operations β€” read, write, create, delete, rename, mkdir, list
  • Terminal sessions β€” full PTY terminals shared across browser tabs
  • Git operations β€” status, diff, commit, push, pull (with credential refresh)
  • Code formatting β€” Prettier with Astro, TypeScript, CSS, JSON support
  • Command execution β€” an allowlist of safe commands (npm, npx, node, astro, git, and common shell utilities)

Authentication uses the same RS256 JWT tokens as Codespaces. The public key is either baked into /app/.jwt-public-key on first setup or refreshed from the PhantomWP API every few hours.

Security

Running locally doesn't mean less secure β€” the container is locked down by default:

  • Non-root β€” services run as the phantomwp user, never as root
  • JWT auth on every WebSocket message β€” the same check Codespaces use
  • Setup tokens are single-use β€” 32 random bytes, 2-hour expiry, timing-safe comparison, consumed atomically on first use
  • Strict origin checks β€” the WebSocket server rejects connections whose Origin header isn't the PhantomWP dashboard
  • Path traversal protection β€” .git, node_modules, .env, and similar paths are blocked
  • Command allowlist β€” exec actions reject shell operators (&&, ||, ;, |, backticks)
  • Localhost-only ports β€” Docker binds ports to 127.0.0.1, not your LAN
  • No secrets in the image β€” the JWT public key and project files are fetched at runtime, never baked in
  • Graceful shutdown β€” tini forwards SIGTERM, PM2 drains services, WebSocket closes cleanly

See WordPress Security for security considerations on the CMS side.

Health Checks

The container exposes a /health endpoint on the WebSocket port:

curl http://localhost:14322/health

It returns JSON with server status, connected clients, active terminals, whether the JWT key is loaded, and memory usage. The PhantomWP dashboard and create flow probe this endpoint from your browser to tell when the container is actually ready β€” that way the status you see always reflects your real local runtime, not a server-side guess.

Docker also runs a built-in HEALTHCHECK every 15 seconds, and PM2 auto-restarts crashed services with backoff.

Troubleshooting

"Token expired" or "Token already used"

Setup tokens are single-use and valid for 2 hours. Go back to the dashboard, click Start Locally again on your project, and copy the new command. You only need a token for the very first run β€” restarts don't need one.

Container won't start

# See if it already exists
docker ps -a | grep phantomwp

# Remove it (volume is preserved) and run the docker run command again
docker rm phantomwp-my-site

IDE stuck on "Waiting for container…"

  1. Confirm the container is running: docker ps (or Docker Desktop β†’ Containers)
  2. Test the health endpoint: curl http://localhost:<ws-port>/health
  3. Check logs: docker logs phantomwp-my-site
  4. In Chrome or Edge, make sure you allowed local network access when prompted. Without it, the browser blocks requests from phantomwp.com to localhost and the dashboard can't tell the container is up.

Preview pane is blank

  1. Open the logs: docker logs phantomwp-my-site and look for Astro output

  2. Try hitting the preview URL directly in a browser tab: http://localhost:<astro-port>

  3. If dependencies failed, reinstall:

    docker exec -it phantomwp-my-site bash
    cd /app && npm install

Git push fails with "GitHub authentication failed"

GitHub credentials are refreshed from the PhantomWP API automatically, but if the refresh token expires, the IDE will surface an auth error when you try to push. Go back to the dashboard, click Start Locally to get a fresh setup token, stop the container, and re-run the setup command. Your files and history are preserved because the volume stays intact.

WebSocket auth fails

If you see a JWT error in the logs, the public key probably failed to load. The server retries fetching it from the PhantomWP API on startup. Make sure the container has outbound internet access, then docker restart phantomwp-my-site.

Resetting a site completely

To wipe everything and start over:

docker stop phantomwp-my-site
docker rm phantomwp-my-site
docker volume rm phantomwp-my-site

Then click Start Locally in the dashboard to generate a new setup command. Your GitHub repo is untouched β€” the next container will clone it again.

Compared to Codespaces

CodespacesDocker Local
Runs onGitHub's serversYour computer
Setup time~3 minutes (first time)~1 minute (first time, most of it pulling the image)
Requires installing softwareNoDocker Desktop
BillingGitHub Codespaces minutesFree
Works offlineNoYes, once the project is on disk
Files on your filesystemNoOptional (bind mount)
Multiple browser tabsSupportedSupported
Same IDE & featuresYesYes

Both modes use the exact same IDE, AI assistant, WordPress integration, and deployment flows. You can switch a project between modes at any time from the dashboard β€” your GitHub repo is the single source of truth.

Next Steps