Builds and Helipack
How Helipod turns a Git repository into a container image, and how to tune it with helipack.json.
When a service's source is a Git repository (or an inline Dockerfile), every deploy starts with a build. Image sources skip this step.
git clone (branch, root directory)
│
├── Dockerfile in the folder? ──► docker build with your Dockerfile
│
└── no Dockerfile ──► Helipack: detect → generate a Dockerfile → build
│
▼
image pushed to your private registry space, then rolled outYour own Dockerfile
If the folder being built (the repository root, or the service's rootDirectory) contains a Dockerfile, Helipod builds it as it is. Your variables are available during the build as a BuildKit secret named build_env (a .env file), so they never end up in an image layer:
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN --mount=type=secret,id=build_env,target=/app/.env npm ci && npm run build
CMD ["node", "dist/server.js"]Helipack: builds without a Dockerfile
Helipack is Helipod's zero-config builder. It reads your project, writes a multi-stage Dockerfile tuned for the framework it finds, and builds it with layer caching.
Detect
Helipack reads package.json, bun.lock / lock files, composer.json, requirements.txt / pyproject.toml, framework config files and scripts, and works out the runtime, framework, package manager, build command, start command and port.
Apply your settings
Settings from helipack.json, railway.json / railway.toml and the service settings are merged on top of what was detected (see Configuration).
Generate
It writes a multi-stage Dockerfile: dependencies are installed in a cached stage, the app is built in a second stage, and only what is needed to run goes into the final image, which runs as a non-root user.
Build and report
The image is built with cache mounts for the package manager and framework caches, so later builds reuse work. The detected port, start command and health check path are passed to the deploy.
The build log shows each decision, for example:
ℹ️ No Dockerfile found. Using Helipack builder
🔍 Detecting runtime... bun / nextjs (bun) → port 3000
🔍 Found Next.js config: next.config.mjs
✨ Injected standalone mode into next.config.mjsSupported stacks
| Runtime | Frameworks | Package managers |
|---|---|---|
| Node.js | Next.js, Nuxt, Remix, Astro, NestJS, Elysia, Hono, Express, Fastify, Vite | npm, yarn, pnpm |
| Bun | Same as Node.js | bun |
| PHP | Laravel, Symfony, WordPress, CodeIgniter, Slim, Lumen, Yii, CakePHP | composer |
| Python | Django, FastAPI, Flask, Starlette | pip, poetry, uv |
Anything else: add a Dockerfile and Helipod builds it directly.
Configuration
Helipack works with no configuration. To override what it detects, add a helipack.json next to your package.json (or at the root of the service's rootDirectory):
{
"run": {
"port": 3000,
"command": "node server.js"
},
"build": {
"command": "npm run build:prod",
"dockerfile": "Dockerfile.production",
"strict": false
},
"health": {
"path": "/health",
"duration": 15
},
"packages": {
"apk": ["openssl", "ffmpeg"]
},
"env": {
"NODE_ENV": "production"
}
}Prop
Type
railway.json and railway.toml are read too, so a project that already deploys on Railway needs no changes: build.buildCommand, build.dockerfilePath, build.builder: "DOCKERFILE", build.watchPatterns, deploy.startCommand and deploy.healthcheckPath are understood.
Precedence (highest first): helipack.json, then railway.json / railway.toml, then the service settings (start command, health check, root directory), then detection. Configuration in the repository wins, so the same commit always builds the same way.
Commands must be one line (chain with &&) and paths must stay inside the repository; invalid values are skipped with a warning in the build log.
Variables and secrets during the build
- Service variables are available while building, as a
.envfile mounted for the build only. Frameworks that read variables at build time (NEXT_PUBLIC_*,VITE_*, ...) see them. They are never written into an image layer. - Secret files added to the service in the dashboard are mounted for the build too, and removed afterwards.
.envfiles, private keys and.gitin your repository are always excluded from the build context.
Next.js
The final image runs Next.js in standalone mode (a small server with only the files it needs, about 150 MB instead of well over 1 GB). Helipack adds output: "standalone" to your config for you:
- it finds the object your config exports, also when it is wrapped by plugins, for example
const config = {...}; export default withMDX(config); - if your config has a shape it can't edit safely (a function, a computed export), it keeps your file as
next.config.helipack-original.*and adds a small wrapper that loads it and adds the option.
To skip the edit, set it yourself:
const config = {
output: "standalone",
};
export default config;--turbopack is removed from the build script, because the standalone output needs the regular build.
Monorepos
Set the service's root directory to the app's folder (apps/web, services/api) when you create it (source.rootDirectory) or later with PATCH /services/{id}. Only that folder is built. When a build at the repository root fails and the repository looks like a monorepo, the build log lists the app folders it found.
When a build fails
Read the deploy log: GET /services/{id}/deployments/{deploymentId}/logs. Common fixes:
| Log says | Do this |
|---|---|
| No framework detected | Add a Dockerfile, or set build.command and run.command in helipack.json |
| Wrong port, health check times out | Set run.port in helipack.json, or make the app listen on $PORT |
A system library is missing (libvips, openssl, ...) | Add it under packages in helipack.json |
| TypeScript errors stop the build | They only do when build.strict is true; fix them or turn it off |
| Monorepo built from the wrong folder | Set the root directory |
A failed build is refunded and the previous deployment keeps serving.