Deploy
Send your project's code to a running app from your own machine: pb_hooks, pb_migrations, pb_pages,
pb_public, packages and the production node_modules tree. Only changed files are uploaded and the app swaps them
in; restart applies new migrations and packages. pocketone dev keeps your folder and the app in step both ways and
streams the app's logs; pull and logs work the other way.
pocketone link https://myapp.example.com # add the remote and sign in (Remotes)
pocketone deploy # upload what changed
pocketone logs
Install · Configure · Use · Integrations · Example
Install
Deploy is built into pocketone and on in both directions: every app it serves accepts deploys from its
superusers, and the deploy, dev, pull, logs and restart commands are there to deploy to other apps. There's
nothing to install.
In your own Go build, register it before the app starts. Remotes gives the commands their remotes and logins, and Tokens lets you deploy with API tokens:
app := pocketbase.New()
tokens.MustRegister(app, tokens.Config{}) // optional: API tokens with the deploy scope
remotes.MustRegister(app, remotes.Config{}) // remotes, login, and /_pocker/whoami discovery
deploy.MustRegister(app, deploy.Config{}) // the deploy API + deploy/dev/pull/logs/restart commands
app.Start()
The module is github.com/pockethost/pocketone/ext/deploy. deploy.Config{DisableServer: true} keeps only the
commands, for a binary that deploys elsewhere but must not accept deploys.
Configure
There is no pocketone.yaml section, flag or settings screen. Defaults on the receiving app:
- Who may deploy: superusers. You sign in with
pocketone login(see Remotes) or use an API token. A token needs thedeployscope (or no scopes);deploy:readallows reading files and logs only. - Where files land: next to
pb_data, PocketBase's usual layout (pb_hooks/,pb_migrations/,pb_public/,node_modules/beside it). - Upload store: uploaded files and past deploys are kept in
.pb_deploy/next topb_data. It's outsidepb_data, so backups skip it, and it's never deployed.
On your machine, the commands use the project's remotes and saved logins from Remotes. In CI,
set POCKETONE_TOKEN (or POCKETONE_TOKEN_<REMOTE>) instead of signing in.
Use
All commands take an optional remote name or URL; without one they use the project's default remote. Run them from your project folder (or any folder below it).
deploy
pocketone deploy # to the default remote
pocketone deploy prod -m "fix signup" # to the remote "prod", with a note
pocketone deploy --restart # and restart if migrations or packages changed (CI)
pocketone deploy --dry-run # list what would be sent, send nothing
pocketone deploy --out /tmp/set # write the files to a folder instead
You'll see how many files are deployed and how many are uploaded, then a line like
Deployed <id>: 3 changed, 0 deleted, 41 unchanged. With nothing changed it says Up to date: no changes.
| Flag | What it does |
|---|---|
--restart |
Restart the app after the files are in, when migrations or packages changed. |
--dry-run |
List what would be deployed; deploy nothing. |
--out <dir> |
Write the deploy set to a local folder instead of deploying. |
-i, --include |
Add include patterns to the default set. |
-e, --exclude |
Exclude patterns (pb_data is always excluded). |
--node-modules |
Ship the production node_modules tree (the default when there's a package.json). |
--no-node-modules |
Don't ship node_modules; leave the remote's alone. |
--commit <sha> |
Commit to record. Default: $GITHUB_SHA, $CI_COMMIT_SHA, or git HEAD. |
-m, --message |
A note to record with the deploy. |
-v, --verbose |
List every file. |
What's sent: files matching pb_* and pb_*/**/* except pb_data, plus package.json, bun.lock(b) and
patches/**. Dotfiles are included (pb_public/.well-known); .git* files and symlinks are skipped.
Packages: with a package.json, the production node_modules tree is collected the way Node resolves it (hoisted
npm and bun, pnpm's store, workspace links; never devDependencies) and shipped as plain files, so plain JS hooks can
require npm packages. The remote then owns its root node_modules: packages you no longer ship are removed there.
Restarts: files are swapped in while the app runs, each rename atomic, and deploy never restarts the app on its
own. pb_pages and pb_public are read from disk on each request, and hooks reload by themselves (PocketBase's
--hooksWatch, Pocker's watcher). Migrations and packages apply only when the app starts, so a deploy that changes
them says Migrations or packages changed and leaves the restart to you: pass --restart (the app restarts once,
after the whole batch is in, and you hear if it fails to start), run pocketone restart, or type r in dev. So
does the first deploy that creates pb_hooks, since a watcher only sees folders that existed when the app started.
restart
pocketone restart prod
Restarts the app so it applies new migrations and packages, and tells you if it fails to start. An app that isn't running (a sleeping Pocker instance) is left alone; it applies them when it starts.
dev
pocketone dev
pocketone dev prod --prefer local # first sync: upload yours where both sides differ
Keeps the project folder and the remote in step, both ways, and streams the remote's logs. Stop it with Ctrl+C.
| What | Direction |
|---|---|
pb_hooks, pb_migrations, pb_pages, pb_public and other pb_* folders |
both ways |
package.json, bun.lock(b), patches/ |
both ways |
node_modules |
uploaded only, when package.json or the lockfile changes |
pb_data |
never (it's the app's live data) |
- Your edits go up half a second after you save.
- Changes on the remote, such as the migrations the dashboard writes when you edit a collection, come down within
a few seconds (
--poll, default 5s).devasks the remote for one hash and only looks further when it changed, so a largepb_publiccosts nothing while it's quiet. - Restarts:
devnever restarts the app by itself. Hooks reload by themselves, pages and public files are live on the next request. After you upload a migration or new packages,devsays so: typerand Enter to restart the app and apply them. Migrations that come down are already applied on the remote. - Deletes go both ways too.
- Changed on both sides: your copy wins and is uploaded; the remote's is saved in
.pocketone/conflicts/anddevsays so. - First run: files that exist on only one side are copied to the other. If a file differs on both sides,
devlists them and asks whether to keep yours or take the remote's;--prefer local|remoteanswers in advance.
dev keeps what it last synced in .pocketone/sync.json (add .pocketone/ to .gitignore). List files to leave
out, such as ones your hooks generate, in .pocketoneignore, one gitignore-style pattern per line.
| Flag | What it does |
|---|---|
--prefer local|remote |
First sync: who wins where both sides differ (default: ask). |
--poll <duration> |
How often to check the remote (default 5s). |
--push-only |
Only upload, the way dev used to work. Also used by itself for remotes without two-way sync. |
--no-logs |
Don't stream the remote's logs. |
It takes the same file flags as deploy.
pull
pocketone pull prod # pb_hooks, pb_migrations, pb_pages, pb_public, package.json
pocketone pull prod pb_hooks pb_public/index.html
pocketone pull prod --dry-run
Downloads files from a remote into the project. pb_data is only reachable on an instance that's powered off, so a
single app (always running while it answers) refuses it.
logs
pocketone logs prod
pocketone logs prod -n 20
Replays the last 100 lines (-n, --lines, 0 to 1000), then follows. Requests show as GET /url → 200 (1.2 ms) and
errors are marked ERR. PocketBase writes its logs in batches, so lines show up a few seconds late. The stream
reconnects by itself if it drops.
Deploy history
Every deploy is recorded in the _deploys collection of the receiving app: status, file counts, bytes, commit,
message, source, token name, superuser and any error. Superusers can browse it in the dashboard.
Integrations
Works with
| Extension | How they fit |
|---|---|
| Remotes | Gives the commands their remotes, saved logins and CI tokens. The commands find the deploy API through the remote's /_pocker/whoami. |
| OAuth Server | pocketone login signs in through the browser when the remote runs it. |
| Tokens | Deploy registers the deploy and deploy:read scopes, so you can make API tokens for CI that can only deploy or only read. |
| Pocker | The same API runs for many instances with your own backend: build it with deploy.New (below). PocketHost does this. |
| PocketHost | A remote without /_pocker/whoami is taken to be a PocketHost instance: the commands use /_ph/deploy/v1 and a PocketHost token (POCKETHOST_TOKEN, or the pockethost remote read from .pockethostconfig). |
Building on Deploy
- Go client.
deploy.NewClient(ctx, rc)takes a Remotes client and finds its deploy API.Deploy,Pull,Collect,WatchandNewSyncer(two-way sync:Sync,RemoteChanged) are the commands' building blocks. - Your own host.
deploy.New(deploy.Options{Prefix, Backend, Instances, PBRoot, DataRoot, PlatformPaths, Logs})builds the API; mountapi.Wrap(next).Backenddecides which instance a request is for (Lookup(r)), who it acts as (Authenticate(r, inst)→Auth{UserID, Scopes, Via, TokenLabel}, an*AuthError, orErrNoAccessfor a 404) and records history.Instancesis pocker's Manager (stop, swap, start). The defaultFileLogstails each instance'slogs/exec.log, which pocker writes. PocketHost composes it with its own token backend and keeps/_ph/deploy/v1. - Restarts. Your
Instancesshould reload an instance by itself when itspb_hookschange (pocker's Manager does); deploy only restarts through it when a client asks (Client.Restart, orDeployOptions.Restart). - Discovery. Deploy advertises itself as the
deployfeature of/_pocker/whoami, so clients find its path.
Example
example/main.go is an app that both accepts deploys and makes them: the deploy API, API tokens, remotes and the deploy commands. Its comment walks through a first deploy against itself.
// Command example is a PocketOne app that accepts deploys and can deploy: the deploy API (/_pocker/deploy/v1), API
// tokens, remotes, and the deploy/dev/pull/logs commands.
//
// go run ./example serve --http=127.0.0.1:8090 # the remote
// cd myproject && mkdir pb_hooks && echo 'routerAdd("GET", "/hi", (e) => e.string(200, "hi"))' > pb_hooks/main.pb.js
// go run ./example link http://127.0.0.1:8090 --email [email protected] # password: password
// go run ./example deploy
// curl http://127.0.0.1:8090/hi
//
// Logins (ext/devusers, local testing only): dashboard [email protected] / password, app user [email protected] / password.
package main
import (
"log"
"ganigeorgiev.com/pocketbase/pocketbase"
"ganigeorgiev.com/pocketbase/pocketbase/plugins/jsvm"
"github.com/pockethost/pocketone/ext/deploy"
"github.com/pockethost/pocketone/ext/devusers"
"github.com/pockethost/pocketone/ext/remotes"
"github.com/pockethost/pocketone/ext/tokens"
)
func main() {
app := pocketbase.New()
devusers.Register(app) // local testing: superuser [email protected] and user [email protected], password "password"
jsvm.MustRegister(app, jsvm.Config{}) // deployed pb_hooks and pb_migrations
tokens.MustRegister(app, tokens.Config{})
remotes.MustRegister(app, remotes.Config{})
deploy.MustRegister(app, deploy.Config{}) // deploys land next to pb_data
if err := app.Start(); err != nil {
log.Fatal(err)
}
}
Deploy API
/_pocker/deploy/v1 (superusers, see Configure):
| Route | What it does |
|---|---|
GET /whoami |
Who the request acts as, on which instance. |
POST /check |
Takes a manifest, returns the hashes the server is missing. |
POST /blobs |
Gzipped PHB1 bundles: [sha256][uint32 length][bytes]…, every hash verified, 64 MB per request. |
POST /commit |
Stage, swap in, record. "restartNeeded": true in the answer when migrations or packages wait for a restart; "restart": true in the request restarts after the swap. With "partial": true only the listed files (and "delete") change. |
POST /restart |
Restart the app (applies migrations and packages); restarted and any startError. |
GET/DELETE /files |
List or delete files. |
GET/PUT /files/raw |
Download or upload one file. |
GET /tree |
One folder of the code set (?path=, default the root) with a hash over everything below it; hashOnly=1 for just the hash. |
GET /logs |
The app's logs as server-sent events. |
The protocol is the pockethost npm CLI's, so the same client deploys to PocketOne apps and PocketHost.
How it works
- Go module
github.com/pockethost/pocketone/ext/deploy.example/main.gois an app that accepts deploys and can deploy (logins from devusers: [email protected] / password). - A deploy is a hash diff: the client sends a manifest, uploads only the missing files in bundles, then commits.
- On a single app the files are swapped in while it runs; a restart (when a client asks) is
app.Restart(), which re-executes the process (Config.Restartoverrides it; not on Windows). Config.DirandConfig.StoreDirmove the deploy folder and the upload store;Config.Prefixmoves the API.- Logs on a single app come from its own
_logstable; entries at error level and above are marked as stderr. - The
node_modulescollector isnodemodules, a port of the npm CLI's.
License
MIT
Questions and comments
Comments are a GitHub Discussions thread; sign in with GitHub to post.