Skip to content

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:

  1. Clone

    The commit is fetched from GitHub, GitLab, Bitbucket or Codeberg.

  2. Build

    HostStack builds an image — from your Dockerfile if the service has a Dockerfile path, otherwise from a Dockerfile it generates for the detected runtime. The build log shows each step.

  3. 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.

  4. Pre-deploy command

    If the service has one — usually migrations — it runs inside the new container now.

  5. 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 cache

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.

RuntimeDetected fromVersion read from
Node.jspackage.json.nvmrc, .node-version, volta.node, engines.node
Bunpackage.json with bun.lock, bun.lockb or bunfig.tomlpackageManager ("bun@…"), .bun-version, engines.bun
Pythonrequirements.txt or pyproject.toml.python-version, runtime.txt, Pipfile, requires-python, Poetry’s python dependency
Gogo.modgo.mod’s go and toolchain lines, fetched as needed on the newest Go
RustCargo.tomlrust-toolchain.toml or rust-toolchain
RubyGemfileGemfile’s ruby line, .ruby-version, Gemfile.lock
Elixirmix.exs.tool-versions or mise.toml only (elixir, erlang)
.NET*.csproj, *.fsproj, *.vbproj or *.sln<TargetFramework>, global.json
PHPcomposer.jsonconfig.platform.php, require.php
Javapom.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-slim

Build 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 "…".

RuntimeInstallBuildStart
Node.jsnpm installnpm run buildnpm start
Bunbun installbun run buildbun run start
Pythonpip install -r requirements.txt—python main.py
Gogo mod downloadgo build -o /app/server ../server
Rustcargo fetchcargo build --release./target/release/<package>
Rubybundle install—bundle exec ruby app.rb
Elixirmix deps.getmix compilemix run --no-halt
.NETdotnet restoredotnet publish -c Release -o /app/publishdotnet <Assembly>.dll
PHPcomposer install --no-dev—php -S 0.0.0.0:$PORT -t public
JavaMaven or Gradle dependenciesMaven or Gradle packagejava -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 Dockerfile

HostStack then builds exactly that file and ignores the runtime and the install and build commands. The build log says Using custom Dockerfile: 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"

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:

  1. The new container starts with the same volume mounted as the old one.
  2. Its process can't open the data directory — the old container still holds the lock — so it exits.
  3. Without that process, the new container never becomes healthy.
  4. Because it never becomes healthy, the old container is never stopped.
  5. 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_...

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

StatusMeaning
pendingQueued.
buildingCloning and building the image.
build_failedThe build failed or timed out. The previous version is still serving.
deployingThe new container is starting and being health-checked.
liveServing traffic.
failedThe new container never became healthy, or the pre-deploy command failed.
cancelledStopped by you before it finished.
deactivatedWas live, and was replaced by a newer deploy.
supersededA 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-cache helps 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 PORT on 0.0.0.0 in 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 /tmp or 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?

Essential cookies only — for login sessions. No tracking. Details