CI/CD
Docker
Bake the Handoff CLI into your container image and make handoff run the CMD.
Any platform that runs your Dockerfile (self-hosted Docker, Fly.io, Render, Railway, Heroku) uses the same pattern: install the CLI at build time, make handoff run the command, pass the token as a runtime env var.
Dockerfile
FROM oven/bun:1-alpine
# Install Handoff CLI in its own layer so it stays cached across builds.
RUN apk add --no-cache curl \
&& curl -fsSL https://raw.githubusercontent.com/jtljrdn/handoff-env/main/install.sh \
| HANDOFF_INSTALL_DIR=/usr/local/bin sh
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
CMD ["handoff", "run", \
"--project", "myapp", \
"--env", "production", \
"--", \
"bun", "run", "dist/server.js"]The binary adds ~60 MB to the image. It's a self-contained Bun compile, so the runtime image doesn't need Node installed separately for the CLI itself.
Running the container
docker run -e HANDOFF_TOKEN=hnd_xxxxxxxx -p 3000:3000 myappOr with docker-compose.yml:
services:
app:
image: myapp:latest
environment:
HANDOFF_TOKEN: ${HANDOFF_TOKEN}
ports:
- '3000:3000'PaaS variants
Fly.io, Render, Railway, and Heroku all deploy from a Dockerfile (Heroku can also use a buildpack; see the callout below). Use the same image pattern above and set HANDOFF_TOKEN as a platform secret:
# Fly.io
fly secrets set HANDOFF_TOKEN=hnd_xxxxxxxx
# Render
# Dashboard → Environment → add HANDOFF_TOKEN
# Railway
# Dashboard → Variables → add HANDOFF_TOKEN
# Heroku
heroku config:set HANDOFF_TOKEN=hnd_xxxxxxxxRotating Handoff variables
Change the value in the dashboard, then restart the container:
# Local
docker compose restart app
# Fly
fly apps restart myapp
# Render / Railway: redeploy from the dashboard
# Heroku
heroku ps:restart --app myapphandoff run pulls fresh values on every startup; there's no hot-reload from inside the running process.
Multi-stage builds
If image size matters, copy the binary from a builder stage so curl and the installer don't end up in the final layer:
FROM alpine:3 AS handoff
RUN apk add --no-cache curl \
&& curl -fsSL https://raw.githubusercontent.com/jtljrdn/handoff-env/main/install.sh \
| HANDOFF_INSTALL_DIR=/usr/local/bin sh
FROM oven/bun:1-alpine
COPY --from=handoff /usr/local/bin/handoff /usr/local/bin/handoff
# ...rest of your imageTroubleshooting
- Container crashes immediately with exit 2:
HANDOFF_TOKENisn't set. Checkdocker inspect <container> --format '{{.Config.Env}}'. - Container crashes with exit 3: the token's org is on the Free plan. CLI access needs Team.
- Slow startup:
handoff runmakes one HTTPS call to pull variables. If that's noticeable, check for a slow DNS or TLS handshake from the container's network, not the CLI.