Migrate from Netlify to Zenifra
This guide shows where to find each setting of a Netlify project and where to enter the same value in the Zenifra console. It covers static sites and SPAs, netlify.toml, the _redirects and _headers files, Netlify Functions, Forms, server-rendered frameworks (Next.js, Astro, Nuxt), and Netlify DNS. For the general plan, with data copy and DNS switch, see Migrate to Zenifra.
Keep the Netlify site running while you deploy and validate the application on Zenifra. Only remove the site after switching DNS.
Before you start: what your site uses
A site that only has a build, a published folder, redirects, and headers migrates directly. Netlify server features need an equivalent:
| On Netlify | On Zenifra |
|---|---|
| Static site or SPA | HTTP project with serve or NGINX |
_redirects, _headers, and [[redirects]] | serve.json or nginx.conf; see Redirects and headers |
| Netlify Functions | Routes of an HTTP server; see Functions |
| Scheduled Functions | Scheduled jobs |
| Background Functions | Queues with a worker, or scheduled jobs |
| Edge Functions | Middleware in the application server |
| Netlify Forms | Your own route in the application; see Forms |
| Next.js, Astro, or Nuxt with SSR | Node.js server; see Frameworks with a server |
| Netlify Blobs | Armazém or Key-Value |
| Deploy Previews | Preview Environments |
1. Copy the settings from Netlify
In the Netlify dashboard, open the project and go to Project configuration.
Build settings
In Project configuration › Developer settings › Continuous deployment › Build settings, write down:
| Netlify field | What it does |
|---|---|
| Base directory | Folder where Netlify installs dependencies and runs the build. Empty means the repository root |
| Package directory | Site folder in monorepos |
| Build command | Build command, such as npm run build |
| Publish directory | Published folder, such as dist, build, or public |
| Functions directory | Functions folder, netlify/functions by default |
In Branches and deploy contexts, in the same area, check the production branch. In Dependency management, check the Node.js version. The version may also be in an .nvmrc, a .node-version, or the NODE_VERSION variable: if it exists, the repository value wins.
Environment variables
Variables are in Project configuration › Environment variables. Each one can have values per context, such as production and deploy-preview. To export the production values with the Netlify CLI:
netlify env:list --plain --context production > netlify.envVariables marked as secret cannot be read back, not even with the CLI. Get those values from their source, such as the external service dashboard or the team's password manager.
Also check:
- team variables in Team settings › Environment variables, which reach the project without showing up in its list
- the
[build.environment]section ofnetlify.toml, which defines variables directly in the file
netlify.toml
If the repository has a netlify.toml, it takes precedence over the dashboard. Write down:
| Section | What to do on Zenifra |
|---|---|
[build] base, command, publish | Use in the project fields; see step 2 |
[build.environment] | Add as project variables |
[context.production] | Use the production values, which override [build] |
[[redirects]] and [[headers]] | Translate them; see Redirects and headers |
[functions] | Migrate the Functions; see Functions |
[[edge_functions]] and [[plugins]] | Review case by case |
The _redirects and _headers files live in the published folder, often copied from public/ during the build.
Domains and Netlify DNS
In Domain management › Production domains, write down the project's domains.
If the domain uses Netlify DNS, open DNS in the team menu, select the domain, and click Download records to download every record as CSV. You will need them, including the e-mail records (MX and TXT), when you change the nameservers.
2. Create the project on Zenifra
In the console, click + Create project, choose HTTP Application and the plan. Under Advanced Settings, choose Git repository as the Project Source, the Git provider GitHub (or Forgejo), the same repository, and the production branch.
Static site or SPA
| On Netlify | In the Zenifra console |
|---|---|
| Node.js version | Runtime Node.js and the same Version (20, 22, 24, or 26) |
| Base directory (or Package directory, in a monorepo) | Root directory. Use . for the root |
| Build command | Build Command |
| Publish directory | Folder used in the Start Command |
| — | Start Command: npx serve -s dist -l tcp://0.0.0.0:3000, replacing dist with the Publish directory |
| — | Port: 3000 |
| Environment variables | Environment Variables |
Add serve to the dependencies with npm install serve. -s answers unknown routes with index.html, like an SPA's /* /index.html 200 redirect. For multi-page HTML sites, such as Hugo, Eleventy, or static Astro, remove -s.
To spend less, serve the folder with NGINX on the Static plan; see Static sites and SPAs.
Generators outside Node.js
Sites built with Hugo, Jekyll, or another generator that does not run on Node.js are deployed as a Docker Image: generate the site in a Dockerfile stage and copy the folder into an NGINX image, as in the Static sites and SPAs example. The GitHub Action automates the build and deploy on every push.
pnpm, Yarn, and Bun
Zenifra installs Node.js dependencies with npm ci and needs a committed package-lock.json. If the site uses pnpm or Yarn, generate and commit a package-lock.json before migrating. Bun projects can use the Bun Runtime. See Runtimes.
3. Adjust what differs between the platforms
Redirects and headers
Zenifra does not read _redirects, _headers, or netlify.toml. With serve, create a serve.json inside the published folder. A simple way is to put the file in public/, which most frameworks copy to the build folder:
/blog/* /articles/:splat 301
/old /new 302/assets/*
Cache-Control: public, max-age=31536000, immutable{
"redirects": [
{ "source": "/blog/:slug", "destination": "/articles/:slug", "type": 301 },
{ "source": "/old", "destination": "/new", "type": 302 }
],
"headers": [
{
"source": "assets/**",
"headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }]
}
]
}In serve.json, type is the redirect status (301 by default), and rewrites does the same as a Netlify 200 redirect. A segment like :slug covers one path level: Netlify's :splat, which covers several levels, needs one rule per level or NGINX. For more complex rules, such as proxying to another domain (/api/* https://api.example.com/:splat 200), use NGINX with proxy_pass or handle the route in the application server.
Functions
Each file in netlify/functions is a function that starts on demand. On Zenifra, the application is a long-running server, so functions become routes of an HTTP server. Functions in Netlify's current format receive a Request and return a Response, which fits Hono:
import { Hono } from 'hono'
import { serve } from '@hono/node-server'
import { serveStatic } from '@hono/node-server/serve-static'
import contact from './netlify/functions/contact.js'
const app = new Hono()
// Same route the function had on Netlify
app.all('/.netlify/functions/contact', (c) => contact(c.req.raw))
// Site files
app.use('/*', serveStatic({ root: './dist' }))
serve({ fetch: app.fetch, port: Number(process.env.PORT) || 3000, hostname: '0.0.0.0' })Set Start Command node server.js, Port 3000, and the variable PORT=3000. If the function uses Netlify's second context argument, replace what it provided with variables or request data. Functions in the legacy handler(event, context) format must be adapted to receive the server request.
If you prefer, keep the old URL or switch to /api/... and update the calls in the frontend.
Scheduled and Background Functions
- Scheduled Functions become scheduled jobs. Create the job with Task source GitHub Repository, Runtime Node.js, and a Task command that runs the logic, such as
node scripts/task.mjs. The Cron expression can be the same: both platforms use UTC. Shortcuts like@hourlybecome0 * * * *, and@dailybecomes0 0 * * *. - Background Functions can become messages in a queue, processed by a worker project with Exposure Private, or a scheduled job if the task does not need to start right away.
Forms
Netlify detects forms with data-netlify="true" and stores submissions. On Zenifra, the form must post to a route of yours. Create a route in the application server that receives the POST, validates the fields, and sends the content by e-mail or saves it to a database. Then remove data-netlify and point the form's action to the new route.
Before turning Netlify off, save the submissions you already received, which are in the project's Forms panel.
Image CDN
URLs with /.netlify/images do not work outside Netlify. Use images optimized at build time, your framework's optimizer running on the server, such as next/image, or an external image service.
Netlify variables
Variables created by Netlify during the build, such as URL, DEPLOY_URL, CONTEXT, and NETLIFY, do not exist on Zenifra. If the code or build uses one of them, create a variable with the equivalent value, such as URL with your domain.
Frameworks with a server
On Netlify, Next.js, Astro, and Nuxt with server rendering use adapters that turn the application into functions. On Zenifra, they run as a Node.js server:
| Framework | What to change | Build Command | Start Command |
|---|---|---|---|
| Next.js | Nothing: the Netlify adapter is not used | npm run build | npx next start -H 0.0.0.0 -p 3000 |
| Astro (SSR) | Replace the @astrojs/netlify adapter with @astrojs/node in standalone mode | npm run build | node ./dist/server/entry.mjs |
| Nuxt 3 (SSR) | Remove the Netlify preset, if any | npm run build | node .output/server/index.mjs |
For Astro and Nuxt, set the variables HOST=0.0.0.0 and PORT=3000. For all of them, use Port 3000. See the Next.js guide for NEXT_PUBLIC_* variables, caching, and multiple instances.
4. Migrate data and files
| Source | Destination on Zenifra |
|---|---|
| Netlify Blobs | Armazém (files) or Key-Value (small data) |
| Netlify Forms submissions | Save them from the Netlify Forms panel before removing the project |
| External database (Supabase, Neon, etc.) | It can stay where it is or move to managed PostgreSQL with pg_dump and pg_restore |
5. Validate and switch DNS
- Open the project's
*.clients.zenifra.comURL and test pages, redirects, forms, and API routes. - Check Build logs and the application logs for errors.
- Add the domain on Zenifra and create the
CNAMEandTXTrecords shown in the console in your DNS. - If the domain uses Netlify DNS, recreate every record from the CSV at the new DNS provider, change the nameservers at the registrar, and wait for propagation, which can take up to a day. Only then remove the DNS zone on Netlify.
- Keep the Netlify site for a few days for rollback, and only then remove it.
Common issues
| Symptom | Likely cause | Fix |
|---|---|---|
| Reloaded route returns 404 | Missing SPA fallback | Use serve -s or try_files ... /index.html in NGINX |
| Old redirects stopped working | Zenifra does not read _redirects or netlify.toml | Translate them to serve.json or nginx.conf |
/.netlify/functions/... returns 404 | Function without a server | Register the route in the server; see Functions |
| Form does not submit | data-netlify has no effect outside Netlify | Create your own route; see Forms |
npm ci fails | Site uses pnpm or Yarn, without package-lock.json | Generate and commit a package-lock.json |
Blank page and 404 for .js files | Wrong folder in the start command | Use the same folder as the Publish directory |
| Domain e-mail stopped | MX records were not recreated | Recreate the records from the Netlify DNS CSV |
Need help?
Write to [email protected] with your netlify.toml, or reach us on WhatsApp at +55 (11) 5199-3013.
Last updated on
Migrate from Heroku to Zenifra
Step-by-step guide to bring a Heroku app to Zenifra: how to export Config Vars, translate the Procfile, release phase, buildpacks, and Heroku Scheduler, migrate Heroku Postgres, and where to enter each value in the console.
Migrate from Render to Zenifra
Step-by-step guide to bring a Render Web Service to Zenifra: where to copy the Build Command, Start Command, Root Directory, and variables in Render and where to enter each value in the Zenifra console.