Documentation
Everything you need to install WOR and run your first services. For the story of why, see the home page.
Install
A single static Go binary — no runtime dependencies. The installer script supports Debian/Ubuntu; on macOS and Windows, install the bundled binary manually.
One-liner (latest release)
$ curl -fsSL https://wor.worapong.com/download/installer.sh | bash
Specific version
Any release tag listed on the download page, including beta builds.
$ curl -fsSL https://wor.worapong.com/download/installer.sh | bash -s -- v1.0.2-b74
Manual install
Both .tar.gz and .zip contain the same files; the folder inside is always wor-host/.
$ curl -fsSL https://wor.worapong.com/download/releases/v1.0.2-b74.tar.gz -o wor.tar.gz $ tar -xzf wor.tar.gz $ cd wor-host $ sudo ./install.sh
install.sh detects your distro (Debian/Ubuntu only for now), reports which runtime packages are already installed, and asks before installing only the missing ones — it never upgrades or removes anything already present.Download & install the binary
The archive bundles binaries for every platform. Apple Silicon uses wor-macos-arm64; Intel Macs use wor-macos-amd64.
$ curl -fsSL https://wor.worapong.com/download/releases/latest.tar.gz -o wor.tar.gz $ tar -xzf wor.tar.gz && cd wor-host $ sudo cp bin/wor-macos-arm64 /usr/local/bin/wor # Intel: bin/wor-macos-amd64 $ sudo chmod +x /usr/local/bin/wor
Install the runtimes you need
There is no automated package install on macOS — use Homebrew for whatever your services require. wor doctor reports exactly what's missing.
$ brew install nginx node php go python # pick what you need $ npm install -g pm2 # process manager for services
Verify & set up
$ wor version $ wor doctor $ wor setup
go and python services run under PM2 instead of systemd, and PHP-FPM pools all run as your login user (no per-service privilege separation). Everything else works the same as on Linux.Download & extract
Grab the .zip from the download page, or in PowerShell:
PS> Invoke-WebRequest https://wor.worapong.com/download/releases/v1.0.2-b74.zip -OutFile wor.zip PS> Expand-Archive wor.zip -DestinationPath . PS> cd wor-host
Put wor.exe on your PATH
PS> New-Item -ItemType Directory -Force "$env:LOCALAPPDATA\wor\bin" | Out-Null PS> Copy-Item bin\wor-windows-amd64.exe "$env:LOCALAPPDATA\wor\bin\wor.exe" PS> [Environment]::SetEnvironmentVariable("Path", "$env:Path;$env:LOCALAPPDATA\wor\bin", "User")
Verify & set up
Open a new terminal so the PATH change takes effect. Managing hosts entries requires an elevated (Administrator) terminal.
PS> wor version PS> wor doctor PS> wor setup
npm install -g pm2). PHP services share a single PHP_FPM_ENDPOINT — there are no per-service PHP-FPM pools, since PHP-FPM has no official Windows build. wor doctor reports any missing runtimes.Staying up to date
WOR upgrades itself. wor upgrade asks the download site what it currently publishes, shows you that release next to the one you are running, and installs the newer one once you confirm — --yes skips the confirmation. It hands installation to the install.sh inside the downloaded archive rather than reimplementing it, so there is only one tested install path; replacing the running binary is safe, because install(1) unlinks the target before writing and your process keeps running from the old inode.
$ wor upgrade
Not available on Windows, where install.sh cannot run: download the new release and replace wor.exe by hand.
Quick start
Verify & set up
As your non-root operator user, check the install, let doctor inspect the machine, then initialize the WOR home directory.
$ wor version $ wor doctor $ wor setup
Create your first host
An interactive wizard walks you through service type, domain and hosts entry.
$ wor create myapp.example.com
Clone your code & deploy
Point a service at a git repository, then deploy — pull, install dependencies, rebuild, restart.
$ wor source clone myapp https://github.com/you/myapp.git $ wor deploy myapp.example.com
Secure & monitor
Issue a certificate and keep an eye on the fleet. WOR asks whether plain HTTP should redirect to HTTPS — pass --redirect or --no-redirect to answer without prompting.
$ wor ssl issue myapp.example.com --provider=letsencrypt $ wor health
Service templates
| Template | Runtime | Process provider | Default entry point |
|---|---|---|---|
static | none | web server serves public/ | — |
node | Node.js | PM2 (every OS) | app.js |
go | Go | systemd (Linux) / PM2 (else) | app (compiled binary) |
python | Python | systemd (Linux) / PM2 (else) | app.py |
php | PHP-FPM | Dedicated PHP-FPM pool per service | public/index.php |
A php service gets its own pool — its own socket and its own selectable PHP version — whenever exactly one PHP-FPM version is detected; --php-version= picks one when several are, and --no-php-pool falls back to the shared host-wide endpoint. On Linux each pool also runs as its own dedicated unix user, so services are isolated from each other. On macOS they are not: Homebrew's php-fpm master runs unprivileged as your login user, and an unprivileged master cannot switch a worker to a different account.
Per-service configuration
Every service has a .wor directory in its own source tree for configuration WOR reads but your application does not. It sits above the document root, so the web server never serves it.
$WOR_HOME/domains/<domain>/<service>/.wor/ ├── nginx/*.conf web-server snippets (any service type) ├── apache/*.conf web-server snippets (any service type) ├── php.ini PHP ini settings (php services with their own pool) └── php-fpm.ini pool tuning (php services with their own pool)
Web-server snippets
Every *.conf you drop into .wor/nginx or .wor/apache is included inside that service's server { } block or <VirtualHost>, right after WOR's own directives — then wor host reload. You may add directives and locations; on nginx you may not redefine a location WOR already emits, because nginx rejects duplicates on reload. With the HTTPS redirect on, snippets land in the :443 block only — a snippet in a block that does nothing but redirect could never run.
WOR writes a default.conf.example beside them showing the current generated config for that exact service. It does not end in .conf, so it is never loaded, and it is regenerated on every host write — copy it to build on it rather than editing it in place.
PHP settings
A php service with its own pool configures itself through two files. php.ini holds PHP ini settings; php-fpm.ini holds php-fpm pool directives. Two files because they are two different layers: the first becomes php_value[...] your application can still ini_set() past, the second is written into the pool as-is and governs the worker processes.
$ cat myapp/.wor/php.ini memory_limit = 512M upload_max_filesize = 64M post_max_size = 64M $ cat myapp/.wor/php-fpm.ini pm = static pm.max_children = 40 pm.max_requests = 500 $ wor service reload myapp/web
WOR reads these files; PHP does not. A PHP-FPM pool has no way to include a php.ini of its own — the only per-pool mechanism is php_value/php_admin_value inside the pool's own config, and PHP_INI_SCAN_DIR cannot stand in for it, because the master parses php.ini once at startup and every worker is a fork of that. So WOR parses these files and renders directives into the pool, which php-fpm -t validates before anything reloads — and rolls back to the previous pool config if it does not, so a bad value can never take down the other pools sharing that master.
Only an allowlist of keys is accepted in each file, and a key outside it fails rather than being skipped — a setting you asked for is never silently dropped. WOR lists the current set in the .example file it writes beside each one. Some keys are refused on purpose and say why: in php.ini, error_log (the master opens it as root), extension and sendmail_path (they name code to run), open_basedir and disable_functions (host-level containment a service must not widen), and opcache.* (allocated once by the master, so a per-pool value is ignored). In php-fpm.ini, everything that defines the pool's identity — user, group, listen and the socket ownership — because a service able to set those could run as another service's account or take over its socket.
Pool tuning replaces WOR's defaults rather than being appended to them: without a php-fpm.ini a pool gets pm = dynamic with pm.max_children = 5, and setting pm = static or pm = ondemand switches the block to the directives that mode actually uses. The pool file lists each directive exactly once, with the value in force.
wor deploy applies both files as part of a normal deploy — skipping the write and the reload entirely when the rendered pool would be identical to what is already there, because reloading the shared master cycles every other service's workers. wor service reload applies them on their own, without reinstalling dependencies, rebuilding or restarting anything. wor info shows the settings and flags a pool that has drifted from them, wor diagnose warns about the same drift and about a file that no longer parses, and wor health reports drift across the whole fleet.
wor source clone replaces a service's whole tree, so it asks what to do with an existing .wor before it does anything — keep it (the default) or take the repository's copy. Committing .wor to your repository is the tidy way to version this configuration alongside the code.TLS certificates
WOR keeps its own copy of every certificate, whatever the provider, and points the generated virtual host at that copy:
$WOR_HOME/ssl/hosts/<host>/fullchain.pem $WOR_HOME/ssl/hosts/<host>/privkey.pem
Mode 0600, owned by whoever owns WOR_HOME. That one rule works on both platforms without any ACL, because the process that reads a certificate is the web server's master — root on Linux, and your login user under Homebrew on macOS. Let's Encrypt certificates are copied out of certbot's store rather than referenced in place: the real key lives in root-only /etc/letsencrypt/archive, which an unprivileged master cannot read at all.
Redirecting HTTP to HTTPS
The redirect is a per-host setting you choose when the certificate is issued, and can change any time without reissuing:
$ wor ssl redirect myapp.example.com on $ wor ssl status myapp.example.com
The default offered is on for a Let's Encrypt or custom certificate and off for a self-signed one or any local hostname — a redirect to a certificate the browser does not trust makes a site unreachable without clicking through a warning every visit.
Renewal
Certificates are obtained with certbot's webroot authenticator, and wor ssl issue registers a deploy hook so each renewal refreshes WOR's copy on its own. Run wor ssl sync <host> by hand to migrate a host issued by an older version, or to repair a copy that has drifted.
wor doctor warns when Let's Encrypt certificates exist with no schedule on the machine, and wor health reports certificate expiry as a warning.