Pocker
Many PocketOne instances in one process (like Docker, but for PocketOne), routed by hostname. Each instance is a normal app folder and starts on its first request, in tens of milliseconds. Pocker is a runtime provider for Multisite; its peer Docker runs each instance in a container instead.
Install · Configure · Use · Integrations · Example
Install
Pocker is built into pocketone. Name it as the provider in pocketone.yaml and pocketone serve hosts many
instances instead of one app:
provider: pocker
In your own Go build, register it as a provider (examples/pocketone/multi.go
is the stock binary's version), or run a pocker.Server on its own as the example does:
multisite.RegisterProvider(&pocker.Provider{Resolve: cfg.ResolvePath, Extensions: []pocker.Extension{
pitr.New(pitr.Config{}), // any PocketOne extension, on every instance
}})
The module is github.com/pockethost/pocketone/ext/multisite-provider-pocker.
Configure
The pocker section sets where instances live, the JS pool, outbound network and the default limits for every
instance (keys below). Listeners, instances and the firewall are the Multisite sections (server,
instances, firewall).
provider: pocker
instances: { file: ./instances.yaml }
pocker:
root: ./pb_root # one folder per instance: <root>/<id>
hooks_pool_size: 2
egress_allow: [] # private networks instances may still reach
max_js_execution: 30s # default limits; each instance can override them
max_js_allocation: 64MiB
An instance overrides any limit in its own limits (missing fields keep the default):
instances:
- id: shop
hosts: [shop.example.com]
limits: { max_js_execution: 60s, max_concurrent_js: 32 }
Every instance runs the PocketOne code in the binary; an instance's version is ignored.
pocketone.yaml reference
pocker:
Runtime provider pocker: instances run in-process (root folder, hooks pool, egress allow list, instance limit defaults).
| Key | Type | Default | Description |
|---|---|---|---|
root |
string | ./pb_root |
The folder holding one folder per instance: |
hooks_pool_size |
int | 2 |
JS runtimes kept ready per instance (0: a new one for each hook run). |
max_js_execution |
duration | 30s |
Longest any single JS call (hook, route, cron job, migration) may run. |
max_js_allocation |
int | 67108864 |
Largest result a JS built-in may allocate in one step, such as 64MiB. |
max_call_stack_size |
int | 2000 |
Deepest JS recursion allowed. |
max_sleep |
duration | 10s |
Longest a JS sleep() call may wait. |
max_query_duration |
duration | 30s |
Longest an SQL statement without its own deadline may run. |
http_send_max_timeout |
duration | 1m0s |
Longest timeout a JS $http.send call may use. |
http_send_max_body |
int | 10485760 |
Largest response body a JS $http.send call may read, such as 10MiB. |
max_concurrent_js |
int | 16 |
JS calls an instance runs at once; more wait up to max_js_execution, then fail. |
egress_allow |
list of string | Private networks (CIDRs) instances may still connect to. Default none: public addresses only. |
Use
Try it with any hostname
# pocketone.yaml
server: { http: "127.0.0.1:8090" }
provider: pocker
instances: { dev: true }
pocker: { root: ./pb_root }
pocketone serve
curl -H 'Host: blog.localhost' http://127.0.0.1:8090/api/health # creates and serves ./pb_root/blog.localhost
In dev mode a hostname no instance maps becomes an instance named after it, its folder is made on first use, and it runs in dev mode. In production only the hostnames in the instances file are served (see Multisite).
Each instance
- A normal app folder,
<root>/<id>:pb_data,pb_hooks,pb_migrations,pb_public. Its dashboard is athttps://<its hostname>/_/. - Its JS hooks see only its own
secrets, through$os.getenvandprocess.env. - A change to its
secrets,encryption_key,access,limits,dev,dir,superusers,private,extensionsorremoterestarts its app on the next request, after in-flight requests finish. A hostname change doesn't restart it. - A change in
pb_hooksrestarts just that instance. A broken hooks file affects only that instance (below).
Broken hook files
In dev mode, a pb_hooks file that fails to load is logged and skipped, and the rest of the instance runs, so you can
fix it live. Otherwise an instance doesn't start with a broken file.
When a running instance's pb_hooks change, the new code starts beside the running app and takes over only once it
has started. If it fails to load, the previous code keeps serving and the error goes to the instance log, so a bad
edit never takes a working instance down.
Logs
The app's own logs (the dashboard's Logs) are per instance, in its pb_data. console.* from JS hooks goes to
<instance>/logs/exec.log as JSON lines (message, stream, time), rotated to exec.log.1 at 10 MiB. A start
failure is written there too.
Integrations
Works with
| Extension | How they fit |
|---|---|
| Multisite | Routing, listeners, instances file and gates. Pocker is one of its runtime providers. |
| Sandbox | Every instance's JS hooks, SQL and outbound network run in it, so one instance can't reach another or the host. |
| Firewall | Peer allow-list and rate limits in front of every instance; per-instance limits in extensions.firewall. |
| Auto TLS | In the stock binary: certificates for the hostnames instances map (autotls section). |
| Server Timing | In the stock binary: Server-Timing headers on every response. |
| Mailtrap | In the stock binary: dev instances with SMTP off send mail to a local inbox. |
| Edge | Instances another edge runs are forwarded there instead of started here. |
| Stats | Measures disk use and last request for each instance under root. |
| PocketOne extensions | In your own build, any extension such as PITR, S3 Fallback or Mail Relay runs on every instance, and an instance's extensions.<name> settings override the extension's defaults. enabled: false turns it off for that instance. |
Building on Pocker
- Extensions.
pocker.Provider.Extensions(orpocker.Server.Extensions) takes anyextkit.Extension, registered on each instance's app before it starts with that instance'sextensions.<name>settings. Host-level code can implementConfigurer,AppIniter,JSIniter,Starter, or a hostGate,WrapperorTLSProvider. See POCKER.md. - Setup per instance.
SetupApp(inst, app)runs after migrations and before the app serves, e.g. to add a route checked againstinst.Private. - Manager.
OnStarthands host code theManager:WithInstanceStopped(ctx, id, fn)changes an instance's files while it's stopped (deploys, restores),StartInstance,Running,InstanceDir,LogInstance. - Restarts.
app.Restart()(a backup restore, PITR) restarts only that instance.
Example
A Pocker build with one extension: a Go route and an admin dashboard page on every instance, in dev mode. Run it from
this folder with go run ./example, then open http://blog.localhost:8090/_/ (login [email protected] /
password).
// Command pocker-extension is a Pocker build with one extension: a Go route and an admin dashboard page,
// added to every instance. See docs/extensions.md.
//
// Logins (ext/devusers, local testing only): dashboard [email protected] / password, app user [email protected] / password.
package main
import (
"context"
"embed"
"io/fs"
"log"
"net/http"
"os"
"os/signal"
"ganigeorgiev.com/pocketbase/pocketbase/apis"
"ganigeorgiev.com/pocketbase/pocketbase/core"
"github.com/pockethost/pocketone/ext/devusers"
"github.com/pockethost/pocketone/ext/multisite-provider-pocker"
)
//go:embed ui
var uiFiles embed.FS
func main() {
cfg := pocker.DefaultConfig()
cfg.Dev = true // any hostname; drop this for production (instances.yaml required)
ui, _ := fs.Sub(uiFiles, "ui")
srv := &pocker.Server{
Config: cfg,
// Runs for every instance's app after its migrations, before it serves.
SetupApp: func(inst pocker.Instance, app core.App) error {
// local testing: every instance gets superuser [email protected] and user [email protected], password "password"
if err := devusers.Ensure(app); err != nil {
return err
}
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
// a Go route, superuser only
se.Router.GET("/api/hello", func(e *core.RequestEvent) error {
return e.JSON(http.StatusOK, map[string]string{"instance": inst.ID})
}).Bind(apis.RequireSuperuserAuth())
// an admin dashboard extension: ui/main.js is loaded by the dashboard before its router starts
se.UIExtensions = append(se.UIExtensions, core.UIExtension{Name: "hello", FS: ui})
return se.Next()
})
return nil
},
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
if err := srv.Run(ctx); err != nil {
log.Fatal(err)
}
}
Untrusted instances
Pocker runs every instance's JS hooks in the sandbox. For why that's safe enough to host code from people you don't know, and where it isn't, see How is this safe?. Memory is the main limit: only one-shot allocations are capped, so a hook that grows memory slowly affects the whole process. POCKER.md lists what the sandbox covers.
Reference
POCKER.md is the full guide: production pocketone.yaml, every instance field, extension interfaces, the
sandbox, PocketBase changes and versions.
License
MIT
Questions and comments
Comments are a GitHub Discussions thread; sign in with GitHub to post.