Multisite
Many PocketOne instances behind one listener, routed by hostname. Each request's hostname picks an instance, the request passes the gates (such as the firewall), and one runtime provider serves it: pocker runs instances in-process, docker runs each in its own container.
Install · Configure · Use · Integrations · Example
Install
Host is built into pocketone. It takes over serve when pocketone.yaml names a provider; without one,
pocketone serve runs a single app as usual.
provider: pocker # or docker
In your own Go build, register the providers you link, build the host from the config and run it.
examples/pocketone/multi.go is the stock binary's version:
multisite.RegisterProvider(&pocker.Provider{Resolve: cfg.ResolvePath})
h, err := multisite.FromConfig(multisite.Options{Resolve: cfg.ResolvePath})
if errors.Is(err, multisite.ErrNoProvider) {
// no provider in pocketone.yaml: serve a single app instead
}
err = h.Run(ctx)
The module is github.com/pockethost/pocketone/ext/multisite.
Configure
A host reads these pocketone.yaml sections:
provider: pocker # which runtime provider runs the instances
server:
https: ":443" # the main address
http: ":80" # redirects to https (alone: serves plain HTTP)
plain: [":8080"] # extra plain-HTTP addresses, no redirect (e.g. a private network)
tls: { cert: cert.pem, key: key.pem } # reloaded when the files change; or the autotls section
instances:
file: ./instances.yaml # where the instances come from
dev: false # also serve unknown hostnames
pocker: { root: ./pb_root } # the provider's own section (pocker or docker)
firewall: { allow_cidrs: [10.0.0.0/8] } # any gate's section turns that gate on
providerandserverbelong to Config. With nohttpsorhttp, the host listens on127.0.0.1:8090.- The
instancessection is this extension's (keys below). In production the instances file is required; withdev: trueit's optional. - The
appsection is for a single app and is refused when a provider is set: each instance has its own folder under the provider's root.
pocketone.yaml reference
instances:
Where a multi-instance host's instances come from (a YAML file), and dev mode (serve unknown hostnames).
| Key | Type | Default | Description |
|---|---|---|---|
file |
string | instances.yaml |
The instances file (id, hosts and settings for each instance), reloaded when it changes. |
dev |
bool | Also serve unknown hostnames: each becomes an instance named after it, in dev mode, with its folder made on first use. |
Use
The instances file
instances:
- id: blog # also the folder name: <root>/blog
hosts: [blog.example.com, www.blog.example.com]
- id: shop
hosts: [shop.example.com]
secrets: { STRIPE_KEY: sk_live_... } # the app's environment, seen by its JS hooks
encryption_key: 0123456789abcdef0123456789abcdef # encrypts the app's stored settings
access: { cors_origins: [https://shop.example.com] }
limits: { max_js_execution: 60s } # over the provider's defaults
extensions: { firewall: { trusted_ips: [203.0.113.0/24] } } # settings for extensions, by name
- id: old
hosts: [old.example.com]
blocked: { status: 402, message: "Payment required" }
The file is checked for changes every 2 seconds; edits apply without restarting the host. Ids are lowercase
letters, digits, dots and dashes. Other fields: version and runtime (which release or provider runs it), dir
(another folder), dev, superusers (pocker upserts them when the app starts), private (for host Go code, never seen by JS) and remote
(another server runs it).
What a request gets
- A hostname no instance maps gets 404, and no folder is created. In dev mode it becomes a new instance named after the hostname, in dev mode.
- A
blockedinstance answers every request with its status and message. - An instance with
remoteset is forwarded there after the gates (a plain proxy to the URL, or Edge for a fleet). GET /_pocker/healthon any hostname reports the host's status, how many instances it knows and how many are running.
Switching provider
Switching between pocker and docker is a config change: both keep instances in <root>/<id>. Stop the containers
first when leaving docker (they keep running across a host restart on purpose). Data only moves forward: an instance
started on a newer version than wrote its data is upgraded.
Integrations
Works with
| Extension | How they fit |
|---|---|
| Pocker | Runtime provider: every instance runs in this process (provider: pocker). |
| Docker | Runtime provider: each instance runs in its own container (provider: docker). |
| Firewall | A gate: peer allow-list, client IPs and rate limits. On when the config has firewall or rate_limits. |
| Edge | Forwards requests for instances another edge runs (the edges section). |
| Mothership | Its mirror replaces the instances file as the instance source, and lists the other edges. |
| Auto TLS | Certificates for the main listener, issued for the hostnames instances map. |
| Stats | Measures disk use and last request for each instance. |
| Server Timing | Server-Timing headers for each step of a request. |
Building on Host
- Runtime provider. Implement
multisite.Provider(Name,Prepare) and callmultisite.RegisterProvider;provider:picks it by name.Preparesetsh.Runtime, amultisite.Runtime(Name,Serves,ServeInstance,Ready), with optionalPrechecker,Syncer,StatserandShutdowner. - Instance source. Implement
multisite.InstanceSource(Snapshot,Watch) to feed instances from anywhere, such as a control plane, and pass it asmultisite.Options.Source.YAMLSourceis the built-in one. - Gates. A
multisite.Gatechecks the peer (AllowPeer) and admits each request (Admit).RegisterSectionGateturns one on from its own config sections, as the firewall does;ClientIPResolverpasses the real client IP on to the app. - Forwarding.
multisite.Options.Forwardserves requests for instances withremoteset (Edge fills it). - Listeners and wrappers.
Host.TLStakes aTLSProvider,Host.Wrapperswrap the whole handler, andHost.Backgroundruns jobs until shutdown. - Per-instance settings. An extension reads its own
extensions.<name>withinst.ExtensionConfig(name)andmultisite.DecodeExtensionConfig.
Example
A toy host: instances from a YAML file, served by a runtime that answers with the instance it resolved. Run it from
this folder with go run ./example --instances instances.yaml, then curl -H 'Host: blog.localhost' localhost:8090/.
// A minimal host: instances from a YAML file, routed by hostname, served by a toy runtime that answers with the
// instance it resolved. Real hosts plug in a provider instead (ext/multisite-provider-pocker in-process, ext/multisite-provider-docker in containers).
//
// go run ./example --instances instances.yaml
// curl -H 'Host: blog.localhost' localhost:8090/
package main
import (
"context"
"flag"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/pockethost/pocketone/ext/multisite"
)
type echoRuntime struct{}
func (echoRuntime) Name() string { return "echo" }
func (echoRuntime) Serves(inst multisite.Instance) bool { return true }
func (echoRuntime) Ready() bool { return true }
func (echoRuntime) ServeInstance(w http.ResponseWriter, r *http.Request, inst multisite.Instance) {
fmt.Fprintf(w, "instance %s (%s%s)\n", inst.ID, r.Host, r.URL.Path)
}
func main() {
instances := flag.String("instances", "instances.yaml", "instances file: instances: [{id: blog, hosts: [blog.localhost]}]")
listen := flag.String("listen", "127.0.0.1:8090", "listen address")
flag.Parse()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
h := &multisite.Host{Source: &multisite.YAMLSource{Path: *instances}, Runtime: echoRuntime{}, Listeners: multisite.Listeners{Listen: *listen}}
if err := h.Run(ctx); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
How it works
| Piece | What it does |
|---|---|
Instance, InstanceSource, YAMLSource |
The instance model and where instances come from. |
Directory |
Instances by hostname and id, kept current from the source. |
Handler |
Health path, peer check, hostname, blocked, then remote (the Forward hook) or local (the runtime), with the gates. |
Runtime |
What a provider implements. Exactly one per process. |
Gate, RegisterSectionGate, ChainGates |
Request gates. |
Serve, TLSProvider |
Listeners: plain, TLS (a provider such as autotls, or cert files reloaded on change), redirect, graceful shutdown. |
Host, FromConfig |
All of the above assembled, by hand or from pocketone.yaml. |
License
MIT
Questions and comments
Comments are a GitHub Discussions thread; sign in with GitHub to post.