> ## Knowledge Base Index
> Fetch the complete knowledge base index at: https://help.hodi.host/sitemap.xml
> Use this file to discover available pages before exploring further.
> Pure-Markdown content can be obtained by appending a '.md' suffix to the content URLs listed in the sitemap (without the trailing slash).

# 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?](https://help.hodi.host/en/article/can-i-roll-back-after-a-bad-deployment-with-hodifly-1w3uadl/)".

## 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](https://help.hodi.host/en/article/can-i-use-a-verceljson-file-rhjtfx/)" 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](https://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.