Articles on: 🌍 Web Hosting & PaaS
This article is also available in:

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

mode

How the project is served: static, node, python, php or ruby. Overrides detection and the project settings.

runtime

The toolchain that builds it, and for PHP the version that serves it: node:22, php:8.3, python:3.12, ruby:3.3, or none for no toolchain at all. Overrides the project settings, like mode.

install

Replaces the default dependency install (npm ci, composer install).

build

The build command. An empty string means no build: the repository is published as it is.

output

Static sites only: the folder your build writes into (dist, _site).

docroot

PHP only: the front-controller folder inside the release, public for Laravel and Symfony. "." serves the repository root, for an app that has no such folder.

startup

Node and Python apps: the file Passenger runs to start the app, relative to the project root: server.js, dist/main.js for a NestJS build, passenger_wsgi.py. See below.

persist

The paths that must survive a deploy. See below.

env.required

The names of the environment variables your project cannot run without.

keepReleases

How many releases to keep for rollback, from 1 to 50. Five by default.

noindex

true asks search engines to skip the site, on every response. For a staging site.

forceHttps

Send every visitor arriving over plain HTTP to the https address. Hodifly already does this on its own once your domain has a certificate, so this setting is normally here to say false. See below.

spa

Static sites only. Serve index.html for any address that is not a real file, so the routes of a React, Vue or Angular app survive a reload or a direct link. Already automatic when your build looks like a single-page app: true forces it, false turns it off.

crons

Cron jobs, written like in a vercel.json: a list of {"path": "/api/cron", "schedule": "*/15 * * * *"}. Each one calls that address of your site on a schedule. See below.

autodeploy

false stops pushes from deploying; you deploy by hand from cPanel.

previews

true builds a preview URL for each pull request.

rebuildEvery

Rebuild on a timer as well as on push, in minutes: 0, 60, 1440 or 10080.

snippets

Static sites only: {"head": "...", "body": "..."} HTML injected into every built page.


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 .mjs file, or a project with "type": "module") needs a small bridge file at the root, and startup points at the bridge: import('./dist/server/server.mjs').catch(e => { console.error(e); process.exit(1); }); in a file named server.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.mjs is the main module, which it is not under Passenger: remove that condition so listen() always runs, on process.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.required names 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 .example file: 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's var/log and var/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.
  • schedule is the five usual cron fields: minute, hour, day of the month, month, day of the week, in the server's time zone. Shortcuts like @daily are not accepted: write 0 0 * * *.
  • path starts 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: Bearer header holding the project's CRON_SECRET environment variable. Your code compares it with process.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. A CRON_SECRET you 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.json with crons, the list from hodifly.json replaces 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": false turns the redirect off, for a site that has a reason to answer in clear.
  • "forceHttps": true redirects 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.required is 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

Was this article helpful?

Share your feedback

Cancel

Thank you!