Migrate to Zenifra

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 NetlifyOn Zenifra
Static site or SPAHTTP project with serve or NGINX
_redirects, _headers, and [[redirects]]serve.json or nginx.conf; see Redirects and headers
Netlify FunctionsRoutes of an HTTP server; see Functions
Scheduled FunctionsScheduled jobs
Background FunctionsQueues with a worker, or scheduled jobs
Edge FunctionsMiddleware in the application server
Netlify FormsYour own route in the application; see Forms
Next.js, Astro, or Nuxt with SSRNode.js server; see Frameworks with a server
Netlify BlobsArmazém or Key-Value
Deploy PreviewsPreview 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 fieldWhat it does
Base directoryFolder where Netlify installs dependencies and runs the build. Empty means the repository root
Package directorySite folder in monorepos
Build commandBuild command, such as npm run build
Publish directoryPublished folder, such as dist, build, or public
Functions directoryFunctions 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.env

Variables 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 of netlify.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:

SectionWhat to do on Zenifra
[build] base, command, publishUse 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 NetlifyIn the Zenifra console
Node.js versionRuntime 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 commandBuild Command
Publish directoryFolder 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 variablesEnvironment 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:

_redirects (Netlify)
/blog/*      /articles/:splat   301
/old         /new               302
_headers (Netlify)
/assets/*
  Cache-Control: public, max-age=31536000, immutable
public/serve.json (Zenifra)
{
  "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:

server.js
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 @hourly become 0 * * * *, and @daily becomes 0 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:

FrameworkWhat to changeBuild CommandStart Command
Next.jsNothing: the Netlify adapter is not usednpm run buildnpx next start -H 0.0.0.0 -p 3000
Astro (SSR)Replace the @astrojs/netlify adapter with @astrojs/node in standalone modenpm run buildnode ./dist/server/entry.mjs
Nuxt 3 (SSR)Remove the Netlify preset, if anynpm run buildnode .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

SourceDestination on Zenifra
Netlify BlobsArmazém (files) or Key-Value (small data)
Netlify Forms submissionsSave 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

  1. Open the project's *.clients.zenifra.com URL and test pages, redirects, forms, and API routes.
  2. Check Build logs and the application logs for errors.
  3. Add the domain on Zenifra and create the CNAME and TXT records shown in the console in your DNS.
  4. 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.
  5. Keep the Netlify site for a few days for rollback, and only then remove it.

Common issues

SymptomLikely causeFix
Reloaded route returns 404Missing SPA fallbackUse serve -s or try_files ... /index.html in NGINX
Old redirects stopped workingZenifra does not read _redirects or netlify.tomlTranslate them to serve.json or nginx.conf
/.netlify/functions/... returns 404Function without a serverRegister the route in the server; see Functions
Form does not submitdata-netlify has no effect outside NetlifyCreate your own route; see Forms
npm ci failsSite uses pnpm or Yarn, without package-lock.jsonGenerate and commit a package-lock.json
Blank page and 404 for .js filesWrong folder in the start commandUse the same folder as the Publish directory
Domain e-mail stoppedMX records were not recreatedRecreate 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

On this page