Signed in to your workspace with shpyrd login (see Getting started), the CLI speaks the workspace's API: on shpyrd cloud there is nothing else to set up. On a cluster you run yourself, a kubeconfig named on the command line (--context) makes it go through the cluster instead.
Create a project
shpyrd projects create "My Service" # slug my-service: namespace app-my-service + App resourceshpyrd projects create "My Service" --save # also writes shpyrd.yaml (project: my-service, plus what the directory's build profile implies)Names are lowercase letters, digits and dashes (max 40 characters) and become the hostname: https://my-service.<domain>.
A new project asks its visitors to sign in: only people with a role on it can open the app, and the app receives who they are. For a site anyone may open, create it with --public or switch later with shpyrd access set public. See Sign-in for your app.
Deploy
From inside your repository:
shpyrd deploy # app from shpyrd.yaml, or --project my-serviceWhat happens:
- Archive. The committed tree of the current directory (
git archive HEAD) is packed; run it from a subdirectory to deploy just that service of a monorepo. Uncommitted changes are not included unless you pass--working-tree(also chosen automatically when nothing in the directory is committed yet). Outside a Git repository the directory is tarred. - Upload. The archive is sent to your workspace over its API (self-hosted, with
--context: through the Kubernetes API server). - Build. In the cluster, with the Paketo buildpacks (kpack) or, when the directory has a
Dockerfile, with BuildKit; the CLI streams every step. - Release. The controller rolls the new image out process by process and prints the release number and URL.
==> Archiving HEAD:examples/hello (654f4925638e)==> Uploading source (2.6 KiB)==> Building===> prepare===> detect4 of 9 buildpacks participating===> build web (default): /layers/paketo-buildpacks_go-build/targets/bin/web worker: /layers/paketo-buildpacks_go-build/targets/bin/worker===> export==> Releasing Deploying: Releasing v3: web 1/3 updated · worker 0/1 updated Running: web 3/3 · worker 1/1Released v3: Deploy 654f4925638ehttps://hello-world.acme.shpyrd.appOther sources:
shpyrd deploy --git https://github.com/org/repo --ref main --path services/api # new commits rebuild automaticallyshpyrd deploy --image ghcr.io/org/repo:1.4.2 # run a prebuilt image, no buildshpyrd deploy --no-wait # do not follow the buildDeploying from Git is also available in the dashboard (Deploy button, or when creating the project).
Buildpacks: languages, stacks and system packages
The Paketo buildpacks detect the language from the repository and do the right thing for the common case. A few patterns need hints they cannot guess; the CLI recognises those patterns in the directory and fills the hints in before the build (build profiles), saying what it inferred and why:
==> Detected static site (public/index.html and no language files) build.buildpacks=[web-servers], BP_WEB_SERVER=nginx, BP_WEB_SERVER_ROOT=public (shpyrd.yaml values win; --save writes these there)| The CLI sees | It infers |
|---|---|
public/index.html and no language files | the web-servers buildpack on nginx serving public/ |
vite in package.json, a build script, no start script | the web-servers buildpack building with Node and serving dist/ with single-page routing |
next in package.json | BP_NODE_RUN_SCRIPTS=build, NODE_ENV=production for the build |
config.ru | RACK_ENV=production (Sinatra 4 only permits real hostnames in production) |
config/application.rb | RAILS_ENV=production, RAILS_LOG_TO_STDOUT=1, the /up health check when the app routes it |
public/index.php | the PHP buildpack with nginx in front of PHP-FPM serving public/ |
an Aptfile with libvips, ImageMagick, ffmpeg, GDAL, OpenCV, Tesseract, Chromium... | build.stack: full (their dependency trees need the full run image) |
Anything written in shpyrd.yaml wins over an inferred value; a Dockerfile build takes none of the buildpack hints. shpyrd deploy --save (or shpyrd projects create --save) writes the inferred values into shpyrd.yaml — a new file, or the missing keys added to the existing one with its comments kept — so what runs is what is committed. Detection reads the local directory: --git and --image deploys get none of it.
The sections below are what the profiles write, for when the pattern is not one of these.
Static sites
A directory with only static files (HTML, CSS, JS) needs BP_WEB_SERVER or the web-servers buildpack cannot detect it:
# shpyrd.yamlbuild: env: BP_WEB_SERVER: nginx # or httpd BP_WEB_SERVER_ROOT: public # directory that holds index.htmlSingle-page apps (React, Vite)
A Vite project with no start script in package.json is served as a static site after npm run build. The Node buildpack would win detection (there is a package.json) and produce an image with no process to start. Use build.buildpacks to compose explicitly:
build: buildpacks: [web-servers] # Paketo web-servers: builds with Node, serves with nginx env: BP_NODE_RUN_SCRIPTS: build BP_WEB_SERVER: nginx BP_WEB_SERVER_ROOT: dist BP_WEB_SERVER_ENABLE_PUSH_STATE: "true" # HTML5 routingBuildpacks and stacks
build.buildpacks pins the buildpack group the project uses (names from shpyrd sizes list); build.stack chooses the base image. The full stack (jammy-full) carries more system libraries than the base (jammy, default) and is useful when a language extension needs a C library that is present on Ubuntu but not in Paketo's minimal base image:
build: stack: full # base (default) or fullSystem packages (Aptfile)
An Aptfile in the repository root lists Ubuntu packages to install into the image, one per line:
# Aptfilelibvips42The CLI translates it to the format the heroku/deb-packages buildpack reads and composes it in front of the language's buildpack. No project.toml or explicit build.buildpacks needed.
# shpyrd.yaml not required for an Aptfile — the CLI detects itshpyrd deployRelease phase
When the image has a release process type — the Procfile line release: bundle exec rails db:prepare — the platform runs it before every new release rolls out. The rollout waits; a failure leaves the previous release serving and marks the project Failed with the reason.
# Procfilerelease: bundle exec rails db:prepareweb: bundle exec puma -C config/puma.rb==> Releasing Deploying: release phase: running /cnb/process/release Running: web 1/1Released v2: Deploy abc123def456The command's output streams into the deploy (release | ...) and stays in shpyrd logs --process release. shpyrd projects info lists the process types, including release, and a pending or failed release phase; the project page shows the phase while it runs and, when it fails, the reason, the output and a Run it again button (shpyrd redeploy does the same: with a failed release command, a redeploy runs the command again rather than restarting instances that never rolled out).
For Dockerfile images without a Procfile, declare the command in shpyrd.yaml:
processes: release: command: ["python", "manage.py", "migrate"]Dockerfile builds
When the deployed directory contains a Dockerfile, shpyrd deploy builds it instead of using buildpacks:
==> Archiving working tree (c26f84642501)==> Building with Dockerfile (Dockerfile)==> Uploading source (1.7 KiB)==> Building===> fetch===> build#8 importing cache manifest from 10.96.0.50:5000/apps/hello-docker:cache#9 CACHED...pushed 10.96.0.50:5000/apps/hello-docker@sha256:2a6749dc...==> ReleasingReleased v2: Deploy c26f84642501The build runs as a rootless BuildKit Job in the project namespace: a first step fetches your archive (or clones the Git revision), the second builds the context and pushes the image. Multi-stage builds, build arguments, .dockerignore and a layer cache between builds all work as with docker build.
Pin or tune it in shpyrd.yaml:
build: strategy: dockerfile # buildpacks | dockerfile; without it, auto-detected from the Dockerfile dockerfile: deploy/Dockerfile target: runtime # multi-stage target env: NODE_ENV: production # build argumentsprocesses: web: {} # runs the image CMD worker: command: ["node", "worker.js"] # Dockerfile images have one entrypoint: other process types name their commandFrom Git, detection is not possible; say so explicitly: shpyrd deploy --git https://github.com/o/r --dockerfile (optionally --dockerfile deploy/Dockerfile). The dashboard's Deploy dialog has the same choice. Git sources with a Dockerfile are rebuilt when the revision or the build settings change, not on every new commit as buildpack builds are; pass a commit or redeploy to rebuild a branch.
Failures show BuildKit's error in the CLI, the Activity panel and shpyrd projects info; the previous release keeps serving. examples/hello-docker in the repository is a complete example.
Processes and sizes
Declare process types in shpyrd.yaml next to your code; shpyrd deploy applies it:
project: hello-worldprocesses: web: port: 8080 worker: cpu: "500m" memory: 256Mibuild: env: BP_GO_TARGETS: ./cmd/web:./cmd/worker # Go: one process per commandScale at any time; counts survive deploys:
shpyrd scale web=3 worker=2Config vars
shpyrd secrets set DATABASE_URL=postgres://... LOG_LEVEL=debugshpyrd secrets unset LOG_LEVELshpyrd secrets list # names and when each was last set; values are never shownEvery change is a release (Set DATABASE_URL config var) and restarts the processes with the new environment. The dashboard's Config tab does the same, including pasting .env files.
Plain, non-secret variables can also live in shpyrd.yaml under env: and travel with the code — useful for things like RACK_ENV, RAILS_ENV or NODE_ENV that belong in the repository rather than in the cluster's secret store:
env: RACK_ENV: production RAILS_LOG_TO_STDOUT: "1"env: is authoritative when present: an empty map (env: {}) removes every previously declared plain variable. Secret values — API keys, database passwords — always go through shpyrd secrets set, never here.
Global config vars
Settings every project should have (an OPENAI_API_KEY, a region) are set once by a platform admin and injected into every process of every project:
shpyrd globals set OPENAI_API_KEY=sk-... REGION=eushpyrd globals unset REGIONshpyrd globals list # names and when each was set; values are never shownGlobals come first in the environment: a project's own config var of the same name wins, and variables from attached resources win over both. A change is a Global config change release in every project that receives them (the Cluster page's card asks first and says how many). shpyrd secrets list and the Config tab show them as provided by cluster and mark project vars that override one. A project opts out in shpyrd.yaml:
globals: false # none of themglobals: { exclude: [OPENAI_API_KEY] } # all but theseHealth checks
shpyrd configures probes automatically. No configuration is needed for the common case:
| Process type | Default probe |
|---|---|
web (or any process with a port) | HTTP GET / on PORT |
Any process with port: set | TCP on that port |
| Workers and other processes without a port | None — relies on restart-on-crash |
The deploy waits for each new instance to pass its readiness probe before the old one is removed, so traffic is always served. If a new instance never becomes healthy the rollout stalls, the Activity panel shows the reason (exit code, probe error) and a rollback button; the old instances keep serving.
Override the default or disable checking in shpyrd.yaml:
processes: web: healthCheck: path: /healthz # HTTP GET on PORT; replaces the default / interval: 5s gracePeriod: 30s # startup time before failures count shutdownDelay: 5s # drain time before SIGTERM worker: healthCheck: command: [python, -c, "import app; app.is_healthy()"] # custom command api: healthCheck: tcp: true # explicit TCP when port: is set batch: healthCheck: disabled: true # no probeshpyrd projects info shows the health config per process. shpyrd.yaml changes take effect on the next deploy.
Shell and one-off commands
shpyrd shell # bash (or sh) in web.1shpyrd shell --instance worker.2 # a specific instanceshpyrd shell -- cat /etc/os-release # run one command and return its exit codeshpyrd shell attaches to a running instance: what you see is the live process's filesystem and environment. Buildpack images get the same environment as the process (through the CNB launcher), so node, bundle or python are on the PATH.
shpyrd run rails db:migrate # a new instance of the current release, removed when the command exitsshpyrd run --size shared-l python manage.py import big.csvshpyrd run --detach ./nightly.sh # start and return; follow with shpyrd logs -p runshpyrd run starts a temporary instance (like heroku run) with the release's image and config vars, streams its output and exits with the command's exit code; piped input works (cat dump.sql | shpyrd run psql). Instances left by --detach or a killed terminal are cleaned up after they finish.
Volumes
Processes that need a disk mount a project volume:
shpyrd volumes create data --size 5Gi # a persistent disk of the project# shpyrd.yamlprocesses: web: volumes: - name: data path: /dataVolumes are persistent: they outlive deploys, scaling and crashes and are deleted only by shpyrd volumes delete (or the project's destruction). A volume is single-instance by default (block storage attaches to one node): the process mounting it runs one instance and rolls out with a stop-then-start (a few seconds of downtime per deploy, no data risk), and shpyrd scale web=3 is refused with that explanation. --shared volumes can be mounted by many instances and processes but need a provisioner that offers ReadWriteMany; they are unsafe for SQLite. See Resources for the details.
Logs
shpyrd logs --project shop -fThe Logs tab streams every instance live. The Logs page covers the log agent, the on-node limits and drains to your log provider.
Releases and rollback
shpyrd releasesRELEASE CREATED DIGEST DESCRIPTIONv7 (current) 2026-09-21 22:44:30 2edf5661353b Rollback to v5v6 2026-09-21 22:44:18 2edf5661353b Set GREETING config varv5 2026-09-21 22:44:06 2edf5661353b Set GREETING config varv4 2026-09-21 21:53:52 2edf5661353b Deploy 654f4925638eshpyrd rollback # to the release before the current oneshpyrd rollback 4 # to a specific release: its build and its config varsA rollback is refused while another release is still rolling out (--force overrides). See Concepts for what a release contains.
Redeploy
shpyrd redeploy # new instances of the current releaseshpyrd redeploy --rebuild # build the same source againRedeploy tries the current release again without creating a release. With a healthy or unhealthy release it starts new instances of it (a rolling restart: the fix for an instance stuck on a dependency that came back). When the last build failed - a registry hiccup, a flaky download - it builds the same source again instead, and the release that results is a normal deploy. The project page has the same button; in the card of a failed release it reads Retry build.
Open and inspect
shpyrd open # opens https://my-service.<domain>shpyrd projects info my-service # status, releases and every resource of the project (app, volumes...)shpyrd projects listkubectl -n app-my-service get all,ingress,volumes.shpyrd.io,image.kpack.io,jobs # self-hosted: on a cluster you runDestroy
shpyrd projects destroy my-service # deletes namespace app-my-service and everything in itThe command warns about the data on the project's volumes before asking for confirmation. Images stay in the registry.