How do I configure a project with hodifly.json?
Hodifly can take its settings from a hodifly.json file at the root of your repository, instead of from the form in cPanel. The file is optional: without it, Hodifly detects what your project is and the form holds the settings. With it, the repository configures itself, the configuration is versioned with your code, and it rolls back with it. Deploying the same repository twice gives you two identical projects.
{
"mode": "php",
"runtime": "php:8.3",
"docroot": "public",
"build": "npm run build",
"persist": ["config/app.yaml", "storage/uploads/"],
"env": { "required": ["DB_HOST", "DB_USER"] },
"keepReleases": 5
}
Put it at the root of the repository, or at the root of the package folder for a project that lives in a subfolder of a monorepo. You do not have to write it by hand: in cPanel, open your project and click hodifly.json to generate one from the settings you already have.
Every setting
Setting | What it does |
|---|---|
| How the project is served: |
| The toolchain that builds it, and for PHP the version that serves it: |
| Replaces the default dependency install ( |
| The build command. An empty string means no build: the repository is published as it is. |
| Static sites only: the folder your build writes into ( |
| PHP only: the front-controller folder inside the release, |
| Node and Python apps: the file Passenger runs to start the app, relative to the project root: |
| The paths that must survive a deploy. See below. |
| The names of the environment variables your project cannot run without. |
| How many releases to keep for rollback, from 1 to 50. Five by default. |
|
|
| Send every visitor arriving over plain HTTP to the |
| Static sites only. Serve |
| Cron jobs, written like in a |
|
|
|
|
| Rebuild on a timer as well as on push, in minutes: |
| Static sites only: |
Node and Python: the entry file
Passenger does not run npm start: it runs one file, and startup is how you say which one. It takes a path, not just a name, which is what a compiled application needs:
{
"mode": "node",
"runtime": "node:22",
"build": "npm run build",
"startup": "dist/main.js"
}
Three things worth knowing:
- The app is still started from the project root, whatever the depth of that path. Relative paths in your code, and the folders you listed in
persist(uploads, assets), keep working. - The file has to be CommonJS. Passenger loads it with
require(), so an entry that is an ES module (a.mjsfile, or a project with"type": "module") needs a small bridge file at the root, andstartuppoints at the bridge:import('./dist/server/server.mjs').catch(e => { console.error(e); process.exit(1); });in a file namedserver.cjs. Next.js, Nuxt, Astro, SvelteKit and Remix need none of this, Hodifly writes it for them. - Your server must listen. An Angular SSR build only starts listening when its
server.mjsis the main module, which it is not under Passenger: remove that condition solisten()always runs, onprocess.env.PORT.
You can also set it in cPanel, in the project's Startup file field. The file wins over the field.
A Ruby application has no such setting: Rack always starts from config.ru.
What it deliberately cannot set
- The package folder and the branch. They are how Hodifly finds this file in the first place.
- The domain and the folder on your account. The same repository on two domains is two projects: the address belongs to the hosting account, not to the code.
- Anything secret: environment variable values, the deploy-hook secret, the build-hook URL, your GitLab token. A repository is the one place those must never end up.
env.requirednames them, cPanel holds them.
persist: what survives a deploy
Every deploy creates a new release, and old releases are deleted once keepReleases newer ones exist. So anything your app writes inside its own folder is on a countdown, and anything your repository does not ship is missing from every release. persist fixes both. Each listed path is moved once into a folder that sits outside every release, and linked back into each new one, so it survives deploys and rollbacks.
- A trailing
/means a directory, no slash means a file. It is the only way Hodifly can know what to create when the path exists nowhere yet. - The first deploy decides the starting content. If your repository ships the path, that copy is kept. If it does not, the path is created empty and the deploy log prints exactly where, so you can fill it over FTP. Nothing is ever copied from a
.examplefile: what a template should become is your decision. - After that, the persisted copy wins. A version that later appears in the repository is ignored.
- Laravel's
storage/, Symfony'svar/logandvar/sessions, and any SQLite database named in your configuration are already handled. Do not list them. - You cannot persist the folder your site is served from: it would freeze the site on one release and no later deploy could replace the code.
Rolling back restores the code, never the data: your uploads, your database and your configuration stay as they are today. See "Can I roll back after a bad deployment?".
Cron jobs
crons runs tasks on a schedule. It is written exactly like the crons of a vercel.json, so a project coming from Vercel needs no change:
{
"crons": [
{ "path": "/api/cron/digest", "schedule": "0 7 * * *" },
{ "path": "/wp-cron.php", "schedule": "*/15 * * * *" }
]
}
- A cron job calls an address of your site. At each scheduled time, Hodifly requests that path on your project's own domain, over HTTPS. Your code answers it like any other visitor, so it works the same for a static site with API functions, a PHP app or a Node, Python or Ruby app, and it always runs the version that is live.
scheduleis the five usual cron fields: minute, hour, day of the month, month, day of the week, in the server's time zone. Shortcuts like@dailyare not accepted: write0 0 * * *.pathstarts with/and can carry a query string, like/cron.php?job=cleanup.- Every call is signed. Anyone who knows the address can call it, so each cron call carries an
Authorization: Bearerheader holding the project'sCRON_SECRETenvironment variable. Your code compares it withprocess.env.CRON_SECRET(or your language's equivalent) and refuses anything else. - You do not have to create that secret. If the project has no
CRON_SECRET, the deploy that adds its first cron job creates a random one and says so in the deploy log. It appears in Hodifly > your project > Environment variables, where you can read it or replace it. A Node, Python or Ruby app receives it in that same deploy; a PHP app reads it from the next one. ACRON_SECRETyou set yourself is never replaced. - The jobs show up in cPanel > Cron Jobs, in a block marked as managed by Hodifly. That block is rewritten at each deploy, so edit the file rather than the block. Cron jobs you add yourself outside it are never touched.
- Remove the key (or the file) and the next deploy removes the jobs. A pull-request preview never touches them, and a rollback keeps the current ones.
- Up to 20 jobs, each given 5 minutes to answer. If the repository also has a
vercel.jsonwithcrons, the list fromhodifly.jsonreplaces it, and the deploy log says so.
A Laravel app does not need this for its scheduler: when your app schedules tasks, Hodifly already keeps a schedule:run cron job running every minute, alongside the ones you list here.
HTTPS: your site redirects on its own
Hodifly sends every visitor who arrives over plain HTTP to the https address of the same page, with a permanent redirect. Static sites, PHP applications and Node, Python or Ruby applications all get it, and you do not have to ask for it.
There is one condition, and it is the whole reason forceHttps exists: the redirect only appears once a real certificate actually covers your domain. The first deploy of a new site usually lands before the certificate has been issued, and sending visitors to an https their browser refuses would take the site down until it arrives. So the first deploy is served in clear and says so in its log, the certificate is issued, and the next deploy turns the redirect on by itself.
"forceHttps": falseturns the redirect off, for a site that has a reason to answer in clear."forceHttps": trueredirects without waiting for the certificate. Use it only when you know the certificate is already there: until it is, the site is unreachable.
The addresses your certificate authority checks under /.well-known/ are never redirected, so issuing and renewing a certificate keep working over HTTP.
Which settings apply when
Hodifly re-reads the file from your code on every deploy for everything that describes the build: build, install, output, docroot, startup, persist, env.required, keepReleases, noindex, forceHttps, spa and crons. Edit, push, done.
startup is in that list on purpose: the build is what decides which file starts your application, so moving your entry point should never mean going back to cPanel to make anything run. The value that was used is saved back onto the project, so the Startup file field shows what is actually running.
mode and runtime win on every deploy too. They decide how your application is set up before anything is built, so Hodifly reads them from your repository at the commit being deployed. When they differ from the project, the deploy switches the project first (with the build command, publish folder and startup file that suit the new mode), and the first lines of the deploy log say so. From then on the Settings page shows the value as set by hodifly.json: to change it, change the file and push.
Your hosting plan still decides which kinds of application can run. If the file asks for a Node.js, Python or Ruby app and your plan does not include them, the project keeps its current mode and the deploy log explains why. Rolling back to a release built in another mode is also refused: deploy that commit again instead.
The rest (autodeploy, previews, rebuildEvery, snippets) is applied when the project is created, or when you change its package folder. Changing one of those in the file later means changing it in the form too.
Where hodifly.json and a "vercel.json" say the same thing, hodifly.json wins, and the deploy log says so.
Check a file before you push
Paste it into the validator at hodifly.app/en/validate. It tells you whether the file is valid and, more usefully, exactly what it will do on the next deploy.
Good to know
- A file that is not valid JSON never breaks a deploy: it is ignored, with a warning in the deploy log, and the deploy continues with the settings from the form.
- Limits: 256 KB for the file, 500 settings, 4 KB per value.
env.requiredis a nice safety net. If a variable is missing, the deploy stops in a few seconds and names it, instead of your site answering with a blank page.
Updated on: 12/10/2026
Thank you!