Deploying
How HostStack turns a commit into a running service: which runtime and version it builds with, which commands it runs, how traffic moves to the new version without downtime, and how to go back.
How a deploy runs
A deploy starts when you push to the service's branch (with Auto-deploy on push on, the default), when you click Deploy on the service page, or from the CLI, SDK or API. Every deploy runs the same stages:
Clone
The commit is fetched from GitHub, GitLab, Bitbucket or Codeberg.
Build
HostStack builds an image — from your
Dockerfileif the service has a Dockerfile path, otherwise from a Dockerfile it generates for the detected runtime. The build log shows each step.Start and health check
The new container starts next to the one serving traffic, and HostStack waits for it to answer on
PORT. See Health checks.Pre-deploy command
If the service has one — usually migrations — it runs inside the new container now.
Switch
Traffic moves to the new container and the old one is stopped. The deploy is Live.
If any stage fails, the deploy stops there and the previous version keeps serving.
hoststack deploy trigger svc_...
hoststack deploy trigger svc_... --commit 1a2b3c4
hoststack deploy trigger svc_... --clear-cache # rebuild without the build cacheconst { deploy } = await hs.deploys.trigger(teamId, 'svc_...');Runtimes
HostStack builds ten runtimes without a Dockerfile. It detects the runtime from the files in your repository when you create the service, and you can change it under Settings → General → Runtime. If none is set, the build detects it again, checking the files in the order below — so a repository with both go.mod and package.json builds as Go.
| Runtime | Detected from | Version read from |
|---|---|---|
| Node.js | package.json | .nvmrc, .node-version, volta.node, engines.node |
| Bun | package.json with bun.lock, bun.lockb or bunfig.toml | packageManager ("bun@…"), .bun-version, engines.bun |
| Python | requirements.txt or pyproject.toml | .python-version, runtime.txt, Pipfile, requires-python, Poetry’s python dependency |
| Go | go.mod | go.mod’s go and toolchain lines, fetched as needed on the newest Go |
| Rust | Cargo.toml | rust-toolchain.toml or rust-toolchain |
| Ruby | Gemfile | Gemfile’s ruby line, .ruby-version, Gemfile.lock |
| Elixir | mix.exs | .tool-versions or mise.toml only (elixir, erlang) |
| .NET | *.csproj, *.fsproj, *.vbproj or *.sln | <TargetFramework>, global.json |
| PHP | composer.json | config.platform.php, require.php |
| Java | pom.xml, build.gradle or build.gradle.kts | .java-version, .sdkmanrc, system.properties, maven.compiler.release, java.version, Gradle toolchain |
Language versions
The build uses the version your project already declares, in the file its ecosystem normally uses — the first match in the list above wins. .tool-versions and mise.toml are also read for Node.js, Bun, Python, Ruby, PHP, Elixir and Java. With nothing declared, HostStack builds on the newest release — the newest LTS for Node.js and Java — and that default moves forward with new releases.
The build log says which version it chose and where it read it:
Runtime version: Node.js 22 (from .nvmrc) → node:22-slimBuild and start commands
Each runtime has default commands, so a conventional project deploys without any. Set your own in Settings → General — Install Command, Build Command and Start Command — or with hoststack services update svc_... --build-command "…".
| Runtime | Install | Build | Start |
|---|---|---|---|
| Node.js | npm install | npm run build | npm start |
| Bun | bun install | bun run build | bun run start |
| Python | pip install -r requirements.txt | — | python main.py |
| Go | go mod download | go build -o /app/server . | ./server |
| Rust | cargo fetch | cargo build --release | ./target/release/<package> |
| Ruby | bundle install | — | bundle exec ruby app.rb |
| Elixir | mix deps.get | mix compile | mix run --no-halt |
| .NET | dotnet restore | dotnet publish -c Release -o /app/publish | dotnet <Assembly>.dll |
| PHP | composer install --no-dev | — | php -S 0.0.0.0:$PORT -t public |
| Java | Maven or Gradle dependencies | Maven or Gradle package | java -jar /app/app.jar |
A Python project with only a pyproject.toml installs with pip install .. If the default build script doesn't exist in package.json, the build step is skipped rather than failed. Install, build and start commands that call pnpm get pnpm installed first — the version in packageManager when package.json pins one — since the Node.js image does not ship it. Next.js, Laravel, Rails and Django projects get framework-specific defaults for any command you leave empty — the deploy guide for each framework lists them, with a working hoststack.yaml.
Whatever the start command, the app must listen on 0.0.0.0 at the port in PORT. A hoststack.yaml in the repository can set these commands too, and wins over the dashboard — see Infrastructure as code.
Use your own Dockerfile
For a language HostStack doesn't build, or for system packages the generated image lacks, build from a Dockerfile. A new service created from a GitHub, GitLab, Bitbucket or Codeberg repository with a Dockerfile at its root uses it on its own when the CLI, SDK, API or MCP create names no runtime and no commands. The dashboard does the same for a GitHub repository and shows it as Build with the repository’s Dockerfile. An existing service never switches by itself, and a service from a public Git URL isn't checked: point those at the file. The path is relative to the service's root directory:
hoststack services update svc_... --dockerfile-path Dockerfileawait hs.services.update(teamId, 'svc_...', { dockerfilePath: 'Dockerfile' });Build my "web" service from the Dockerfile at the root of its repo.HostStack then builds exactly that file and ignores the runtime and the install and build commands. The build log says Using custom Dockerfile: Dockerfile.
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
# HostStack sets PORT; listen on it, on 0.0.0.0.
CMD ["node", "server.js"]Your image runs in the same sandbox as a generated one: a read-only root filesystem and a reduced set of capabilities. An image that writes next to its code or fixes up permissions at start-up needs changes — see Container runtime.
Environment variables in builds
Each variable is injected into the build, the running container, or both (the default) — the dashboard calls it Inject into. A variable marked secret is kept out of the build unless you choose Build only. Variables are encrypted at rest with AES-256-GCM. See Environment variables.
Run migrations before traffic switches
Set a pre-deploy command and HostStack runs it with /bin/sh -c inside the new container, after it passes its health check and before it takes traffic. It has the service's environment variables, so DATABASE_URL is there. If it exits non-zero or runs longer than 10 minutes, the deploy fails and the previous version keeps serving.
An image without a shell — Go's distroless runtime image, or your own FROM scratch — runs the command directly instead, so there it must be a program and its arguments, such as ./server migrate. Shell syntax (&&, $VAR, quotes, pipes) fails the deploy with a message that says so.
hoststack services update svc_... --pre-deploy-command "npm run migrate"await hs.services.updateConfig(teamId, 'svc_...', {
preDeployCommand: 'npm run migrate',
});Run "npm run migrate" as the pre-deploy command on my "web" service.The command's output appears in the deploy log under the predeploy phase. The dashboard doesn't have a field for it yet.
Zero-downtime deploys
By default a deploy is rolling: the new container starts next to the old one, and traffic moves to it only once it is healthy. A version that never becomes healthy never receives a request, and the service stays up throughout.
Once traffic is on the new container, the old one is taken off the network and sent SIGTERM, with ten seconds to exit before it is killed. Requests still open on it at that moment can be cut off, so keep request handlers short and let long work run in a worker or cron job.
When rolling doesn't work: recreate
Rolling can't deploy a container that holds an exclusive lock on a mounted volume — a database running inside your container, an embedded queue, anything with a lockfile on shared storage. It fails as a deadlock, not an error:
- The new container starts with the same volume mounted as the old one.
- Its process can't open the data directory — the old container still holds the lock — so it exits.
- Without that process, the new container never becomes healthy.
- Because it never becomes healthy, the old container is never stopped.
- Because the old one is never stopped, the lock is never released.
That runs for the whole health-check grace period and then fails. Set Settings → Runtime → Deploy Strategy to Recreate — stop old container first (--deploy-strategy recreate in the CLI, deployStrategy: 'recreate' in the SDK), and HostStack stops the old container before starting the new one.
Build cache and timeout
HostStack keeps each service's Docker layer cache between builds. Generated Dockerfiles copy your manifest and lockfile and install dependencies before copying the rest of the code, so dependencies are reinstalled only when package.json, the lockfile or their equivalents change. Commit your lockfile to get this. To start from scratch, deploy with hoststack deploy trigger svc_... --clear-cache.
With a yarn.lock, an install command of plain yarn install runs as yarn install --frozen-lockfile — or yarn install --immutable on Yarn 2 and later, recognised by .yarnrc.yml or a packageManager pin — so a lockfile that is out of date with package.json fails the build instead of being re-resolved. For Yarn 2 and later, .yarnrc.yml, .yarn/releases and .yarn/plugins are copied in before the install too. An install command with flags of its own runs as written.
A build may run for 30 minutes before it is stopped. To fail faster, lower it under Settings → Size & Scaling → Build Settings → Build Timeout (seconds), anywhere from 60 to 1800.
Roll back
A rollback starts a new deploy from an earlier deploy's image. Nothing is rebuilt, so it is quick, and the previous version answers health checks before it takes traffic like any other deploy.
hoststack deploy list svc_... # find the deploy to go back to
hoststack deploy rollback svc_... dpl_...await hs.deploys.rollback(teamId, 'svc_...', 'dpl_...');You can roll back to a deploy only while HostStack still has its image. In the dashboard, Redeploy on the service page re-runs the current live deploy's image without cloning or building.
Deploy statuses
| Status | Meaning |
|---|---|
| pending | Queued. |
| building | Cloning and building the image. |
| build_failed | The build failed or timed out. The previous version is still serving. |
| deploying | The new container is starting and being health-checked. |
| live | Serving traffic. |
| failed | The new container never became healthy, or the pre-deploy command failed. |
| cancelled | Stopped by you before it finished. |
| deactivated | Was live, and was replaced by a newer deploy. |
| superseded | A newer push to the same branch replaced it before it went live. |
Troubleshooting
- Could not detect runtime
None of the files in Runtimes is at the root of the build. Set the runtime in Settings → General, set the service's root directory if the app lives in a subfolder, or use a Dockerfile.
- Build timed out after N minutes
Check that the lockfile is committed so dependencies come from the cache, and that large generated or vendored files aren't in the repository. Deploying with
--clear-cachehelps when the cache itself is the problem.- Build out of memory
The build process needed more memory than the build is allowed. Reduce the build's parallelism (bundler workers,
--max-old-space-size), or move the service to a larger size.- Health check timed out
The app didn't answer on
PORTon0.0.0.0in time. The exact message says whether nothing answered or which status came back — see Health checks.- Read-only file system
The app writes somewhere other than
/tmpor a volume. See Container runtime.- Out of memory at runtime
The running app exceeded its size's memory; there is no swap. Move to a larger size, or find what grows — the service's metrics show memory over time.
Next steps
Was this page helpful?