Docker
Run every instance on a host in its own container, with the official PocketBase or PocketOne binary of the version the instance asks for. One host can serve PocketBase 0.23 and 0.36 and PocketOne side by side.
Docker is one of the two runtime providers of the multi-instance multisite. The other is Pocker, which runs every instance inside one process. A host runs exactly one of them.
Install · Configure · Use · Integrations · Example
Install
Docker is built into pocketone. It needs a Docker daemon on the same machine; nothing else to install.
It's off until pocketone.yaml names it as the provider (below). Then pocketone serve runs the host instead of a
single app.
In your own Go build, register it as a provider of the host before building the host from the config:
multisite.RegisterProvider(&docker.Provider{Resolve: cfg.ResolvePath})
h, err := multisite.FromConfig(multisite.Options{Resolve: cfg.ResolvePath})
The module is github.com/pockethost/pocketone/ext/multisite-provider-docker. Add docker.NewCommand(...) to your root command for the
docker seed and docker update commands.
Configure
Set provider: docker and a docker section in pocketone.yaml. Listeners (server), instances (instances) and
the firewall are the host's sections.
provider: docker
docker:
image: alpine:3.20 # base image the binaries run in
binaries: /var/lib/pocketbase # release binary cache
root: /var/lib/instances # one folder per instance: <root>/<id>
versions: ["pb:0.23", "pb:0.36", "p1:0.0"]
default_version: "pb:0.36.*"
idle_ttl: 10m
max_concurrent_starts: 4
instances:
file: ./instances.yaml
image, binaries and root are required.
Versions. An instance picks its binary with the prefix of its version: pb: is upstream PocketBase, p1: is
PocketOne, no prefix means pb:. versions lists the minors the host serves; the newest cached patch that matches
wins. A version that isn't listed gets a clear 503. Upstream PocketBase 0.22 and older are refused unless you lower
min_upstream and keep those releases in the cache yourself.
Per instance (instances.yaml or a mothership): version, secrets (passed as environment), encryption_key,
dev, and dir to put its folder somewhere other than <root>/<id>. An instance with runtime set to anything but
docker isn't served here.
Limits and hardening are optional: nano_cpus, pids_limit, nofile, cap_drop and no_new_privileges.
pocketone.yaml reference
docker:
Runtime provider docker: each instance in its own container with the PocketBase (pb:) or PocketOne (p1:) binary of its version (image, binary cache and manifest, instance root, default version, idle stop, limits).
| Key | Type | Default | Description |
|---|---|---|---|
socket |
string | The Docker socket. Default $DOCKER_HOST when it is unix://, else /var/run/docker.sock. | |
image |
string | Base image the binaries run in, such as alpine:3.20. Required; pulled when missing. | |
binaries |
string | Folder of the release binary cache and its releases.json. Required. | |
versions |
list of string | The minors this host serves, such as pb:0.36 or p1:0.0 (latest patch of each, from docker update). | |
root |
string | Folder with one folder per instance ( |
|
home |
string | The instance folder inside the container. Default /home/pockethost. | |
default_version |
string | Version for instances that name none, such as p1:0.0.*. Empty: such instances fail. | |
min_upstream |
string | Oldest upstream PocketBase release served. Default 0.23.0; lower it only for releases you cache yourself. | |
idle_ttl |
duration | Stop a container after this long without requests. Default 10m. | |
max_concurrent_starts |
int | Containers starting at once. Default 4. | |
start_timeout |
duration | How long a container gets to answer its health check. Default 60s. | |
nano_cpus |
int | CPU limit per container in billionths of a CPU (1000000000 = one CPU). 0: no limit. | |
pids_limit |
int | Process limit per container. 0: Docker's default. | |
nofile |
list of int | Open-file limit per container: [soft, hard]. Empty: Docker's default. | |
cap_drop |
list of string | Linux capabilities to drop, such as [ALL]. | |
no_new_privileges |
bool | Set no-new-privileges on every container. | |
network |
string | Docker network for the containers. Default the bridge. | |
adopt_image |
string | Also adopt running unlabeled containers of this image named after an instance id (taking over from another manager). |
Use
Fetch the binaries
pocketone docker update # find the latest patch of each docker.versions minor, record and download it
pocketone docker seed # download whatever is recorded but not in the cache yet
update checks upstream PocketBase releases (ganigeorgiev.com, then the archived GitHub) and PocketOne releases on
GitHub, verifies each download against the release's checksums.txt, and records the list in the cache's
releases.json. Run it when you want new patches. Nothing is downloaded at request time: when the host starts, any
listed release missing from the cache is downloaded in the background.
The cache layout is <binaries>/<version>/<platform>/pocketbase for PocketBase (PocketHost's existing layout) and
<binaries>/p1/<version>/<platform>/pocketone for PocketOne.
Run the host
pocketone serve
- A container starts on an instance's first request and is ready when
/api/healthanswers. At mostmax_concurrent_startsstart at once. - It stops after
idle_ttlwithout requests. - It restarts on its next request when what it runs changed (secrets, version, mounts). A change to routing only doesn't restart it.
- It stops when the instance is deleted or moves to another edge.
- Containers keep running when the host restarts; the next process adopts them by their labels.
Hooks and migrations
A change in an instance's pb_hooks restarts its container. PocketBase's own hooks watcher stays on too, so a hook
file that fails to load is logged and skipped instead of stopping the instance. A request that arrives while the
instance restarts waits for it instead of failing; one that lost its connection mid-restart is sent once more if
it's safe to repeat (GET, HEAD or OPTIONS without a body). Migrations run when a container boots.
Maintenance
Instance folders are ordinary folders under root. To work on one while nothing has it open, a host's Go code calls
WithInstanceStopped (below); requests for that instance wait up to 30 s, then get a 503 with Retry-After.
Switching provider
Docker and Pocker both use <root>/<id>, so switching is a config change. Stop the containers first when leaving
docker, since they keep running across a host restart on purpose. Data only moves forward: an instance started on a
newer PocketBase than wrote its data is upgraded.
Integrations
Works with
| Extension | How they fit |
|---|---|
| Multisite | Docker is a runtime provider of the host: the host routes by hostname, runs the listeners and gates, and hands each request to docker. |
| Pocker | The other provider. A host runs one or the other, over the same instance folders. |
| Edge | For a fleet: requests for instances on another edge are forwarded there. |
| Mothership | Instances (and their versions) come from a mothership instead of a file. |
| Firewall | Rate limits and allow lists in front of every instance. |
Building on Docker
docker.Providerfields extend the provider from Go:Containeradds mounts, environment and labels to each container;Versionrewrites an instance's version before the binary is picked;OnLogreceives container output (default: the debug log);Versionsreplaces theversionslist (for example a fleet's list from a mothership);MinUpstreamreplacesmin_upstream.Spec(whatContainerfills in):Binds,Env,Labels, plusCmdto adjust the command (for example drop a flag an old release lacks) andHealthPath/HealthAnyStatusfor the readiness check.VersionsSource: any type withReleases(ctx)decides which releases the host serves, each pinned to its download and sha256.YAMLVersions(theversionskey) is the built-in one.- Runtime API:
docker.New(docker.Config{...})builds the runtime without the config file.WithInstanceStopped(ctx, id, fn)stops an instance, holds its requests and runsfnon its folder (for deploys and restores);StartInstancestarts one now;InstanceDirreturns its folder. - Release helpers:
ParseVersion,Pick/PickMin,LatestPatches,UpdateandSeedare what the commands use.
Example
A minimal docker host: instances from a YAML file, each in its own container, routed by hostname on one listener. With
a Docker daemon running, start it from this folder with
go run ./example --instances instances.yaml --data ./instances --binaries ./bin --versions pb:0.23,p1:0.0.
// A minimal docker host: instances from a YAML file, each in its own container with the PocketBase or PocketOne
// version it asks for, routed by hostname on one listener.
//
// go run ./example --instances instances.yaml --data ./instances --binaries ./bin --versions pb:0.23,p1:0.0
//
// instances.yaml:
//
// instances:
// - { id: blog, hosts: [blog.localhost], version: "pb:0.23.*" }
// - { id: shop, hosts: [shop.localhost], version: "p1:0.0.*" }
package main
import (
"context"
"flag"
"log/slog"
"os"
"os/signal"
"strings"
"syscall"
"github.com/pockethost/pocketone/ext/multisite"
"github.com/pockethost/pocketone/ext/multisite-provider-docker"
)
func main() {
instances := flag.String("instances", "instances.yaml", "instances file")
data := flag.String("data", "./instances", "folder with one folder per instance")
binaries := flag.String("binaries", "./bin", "PocketBase and PocketOne binary cache")
versions := flag.String("versions", "pb:0.23,p1:0.0", "minors to serve (latest patch each; fetched at start)")
image := flag.String("image", "alpine:3.20", "base image")
listen := flag.String("listen", "127.0.0.1:8090", "listen address")
flag.Parse()
b := &docker.Binaries{Root: *binaries}
tracked := strings.Split(*versions, ",")
if err := docker.RunUpdate(context.Background(), os.Stdout, docker.DefaultDiscovery(), b, tracked); err != nil {
slog.Error("versions", "error", err)
os.Exit(1)
}
rt, err := docker.New(docker.Config{Image: *image, DataRoot: *data, Binaries: b, Versions: docker.YAMLVersions{Tracked: tracked, Root: b.Root}})
if err != nil {
slog.Error("docker", "error", err)
os.Exit(1)
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
h := &multisite.Host{Source: &multisite.YAMLSource{Path: *instances}, Runtime: rt, Listeners: multisite.Listeners{Listen: *listen}}
if err := h.Run(ctx); err != nil {
slog.Error("host", "error", err)
os.Exit(1)
}
}
How it works
- Each instance folder is mounted at
/home/pockethostand the binary read-only at/home/pockethost/pocketbase. The container runs./pocketbase serve --http 0.0.0.0:8090, plus--devfor dev instances and--encryptionEnv=PB_ENCRYPTION_KEYwhen the instance has an encryption key. Its secrets are its environment. - The port is published on 127.0.0.1 and the host proxies to it. This is the layout PocketHost's Node edge uses, so its instance folders and binary cache move as they are.
- Every container carries labels:
p1.runtime,p1.instance,p1.version(the resolved release, such aspb:0.36.9) andp1.spec(a hash of its configuration, used to tell when it must restart).adopt_imagealso adopts unlabeled containers of an image, for taking over from another manager. - Docker is reached over its socket API directly; there's no Docker SDK dependency.
Status
Tested against a real Docker daemon, with upstream PocketBase 0.23 and PocketOne 0.0.1 side by side, alone and as a host's runtime. Not yet in production.
go test ./... runs the Docker tests when a daemon is reachable (they're skipped with -short). The first run
downloads an upstream release and a PocketOne release into the user cache.
License
MIT
Questions and comments
Comments are a GitHub Discussions thread; sign in with GitHub to post.