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:
- Astro dev server β serves your site with hot reload
- 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
- Docker Desktop installed and running
- macOS: install for Mac
- Windows: install for Windows
- Linux: install for Linux
- A PhantomWP account with at least one project
- A modern browser (Chrome, Edge, or Firefox)
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:
| Flag | Purpose |
|---|---|
--name phantomwp-my-site | Names the container for easy start/stop/remove |
-p 14321:4321 | Maps the Astro dev server to http://localhost:14321 |
-p 14322:8080 | Maps the WebSocket server to ws://localhost:14322 |
-v phantomwp-my-site:/app | Persistent 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:
- Pull the released
ghcr.io/phantomwp/local-devimage (Node 22, PM2, git, node-pty) - Call
POST /api/local/initon phantomwp.com with your setup token - Either clone your GitHub repository (if one exists for the project) or write the generated template files to
/app - Cache the JWT public key that authenticates IDE WebSocket traffic
- Configure git remotes and credentials for push/pull from the IDE
- Install npm dependencies (uses a pre-cached layer for speed)
- 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:/appWith 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-siteMost 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 on14322 - Site B: Astro on
14323, WebSocket on14324
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
| Component | Purpose |
|---|---|
| Node.js 22 | Runtime |
| PM2 | Process manager for Astro + WebSocket |
| tini | PID 1 init, proper signal forwarding |
| node-pty | Full terminal emulation in the IDE |
| git | Version control from the IDE terminal |
| ripgrep | Fast code search |
| Prettier + Astro plugin | Code 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
phantomwpuser, 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
Originheader isn't the PhantomWP dashboard - Path traversal protection β
.git,node_modules,.env, and similar paths are blocked - Command allowlist β
execactions 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 β
tiniforwards 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/healthIt 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-siteIDE stuck on "Waiting for containerβ¦"
- Confirm the container is running:
docker ps(or Docker Desktop β Containers) - Test the health endpoint:
curl http://localhost:<ws-port>/health - Check logs:
docker logs phantomwp-my-site - In Chrome or Edge, make sure you allowed local network access when prompted. Without it, the browser blocks requests from
phantomwp.comtolocalhostand the dashboard can't tell the container is up.
Preview pane is blank
-
Open the logs:
docker logs phantomwp-my-siteand look for Astro output -
Try hitting the preview URL directly in a browser tab:
http://localhost:<astro-port> -
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-siteThen 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
| Codespaces | Docker Local | |
|---|---|---|
| Runs on | GitHub's servers | Your computer |
| Setup time | ~3 minutes (first time) | ~1 minute (first time, most of it pulling the image) |
| Requires installing software | No | Docker Desktop |
| Billing | GitHub Codespaces minutes | Free |
| Works offline | No | Yes, once the project is on disk |
| Files on your filesystem | No | Optional (bind mount) |
| Multiple browser tabs | Supported | Supported |
| Same IDE & features | Yes | Yes |
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
- Quick Start β create your first project
- IDE Overview β tour the editor
- Deploying to Vercel β go live when you're ready
- Troubleshooting β general issue catalog