Build detection and builders
How Synq decides how to build your code (the Dockerfile-first rule, Compose, framework detection) and how to choose another builder per project, Dockerfile with a custom path and context, Nixpacks, Railpack or Cloud Native Buildpacks.
Every source that contains code (a GitHub repository, a Git URL, an uploaded folder or archive) goes through the same two questions:
- What is this? Synq scans the files and detects a Dockerfile, a Compose file, or a framework.
- How is the image built? The project's Builder setting: Auto (Synq's own detection, the default) or a tool you choose.
Everything after the build works the same whatever you pick: ports, environment variables, domains, zero-downtime deploys and rollbacks.
How Synq decides what to build
Synq looks at the app's folder (the repository root, or the Root directory you set for a monorepo app) and uses the first rule that matches:
| Order | If the folder has | Synq builds it as |
|---|---|---|
| 1 | A Dockerfile at the repository root (and no Compose file that adds settings, see below) | Dockerfile: your Dockerfile is built as is. |
| 2 | A recognized framework | That framework, with generated install, build and start commands. |
| 3 | docker-compose.yml, docker-compose.yaml, compose.yml or compose.yaml | Compose: one service per Compose service. |
| 4 | A Dockerfile (in a subfolder app) | Dockerfile. |
| 5 | An index.html and no package.json | A static site, served by the edge. |
| 6 | A package.json, server.js, app.js or index.js | A generic Node.js app. |
The Dockerfile-first rule
A Dockerfile at the repository root is your own build recipe, so it wins over framework detection.
A Next.js repository with a root Dockerfile is built from the Dockerfile, not as Next.js. The deploy
screen then shows Dockerfile Detected ("Your project will be built and deployed using its
Dockerfile") and asks only for the Exposed Port, which it reads from the Dockerfile's EXPOSE line.
Two exceptions:
- A Compose file that adds settings turns the rule off. If the root also has a Compose file that says
anything the Dockerfile path would lose, the Dockerfile does not win and detection continues down the
table (a framework, then the Compose file). "Anything" means: more than one service; a service that only
names an
image:; abuildwith another context, another Dockerfile,args,targetorsecrets; anyvolumes,env_file,depends_on,secrets,configs,commandorentrypoint; orenvironmentkeys that the Dockerfile does not set withENV. When Synq cannot read the Compose file's contents, it assumes the Compose file matters. A plain one-service wrapper (build: .and nothing else, the kind kept fordocker compose up --build) does not count: the Dockerfile is used. - A monorepo app in a subfolder does not get the Dockerfile-first rule. A sub-app's Dockerfile usually expects the whole repository as its build context, so Synq detects the framework first and uses the Dockerfile only when no framework matches. To build a sub-app from its Dockerfile anyway, choose the Dockerfile builder and set the Build context (see below).
To build a repository with a Dockerfile as a framework app instead, open Framework on the deploy screen
and press Change, or set it in synq.json. Use detected goes back to
what Synq found. To deploy a repository as a Compose stack when a framework was detected (or when the
Compose file is not at the root), open Compose file on the deploy screen ("Deploying with Docker
Compose? Point to the file."), enter the file or its folder, and press Scan.
Frameworks Synq detects
Frameworks are checked in this order, so a full-stack framework wins over a plain server library that is also in its dependencies:
- JavaScript and TypeScript: Next.js, Nuxt, SvelteKit, Astro, Remix, TanStack Start, Angular, Gatsby, Vite, Create React App, Vue, NestJS, AdonisJS, Elysia, Hono, Fastify, Koa, Express.
- Python: Django, Flask, FastAPI, then any Python app.
- Go: Gin, Fiber, Echo, then any Go module.
- Rust: Actix, Axum, Rocket, then any Cargo project.
- Ruby: Rails, Sinatra. PHP: Laravel, Symfony. JVM: Spring Boot, Quarkus, Kotlin. .NET: Blazor, .NET. Elixir: Phoenix.
The deploy screen shows what was detected and the commands it will run. Change any of them there, or pin
them in synq.json so every deploy uses the same settings.
How Synq picks the start command
For JavaScript and TypeScript apps, the start command is the app's production command. Many
package.json files use start for a development server: a new NestJS project has
"start": "nest start", which recompiles the app every time it boots. Synq picks, in order:
- A
start:prodorstart:productionscript (for NestJS,npm run start:prod). - The
startscript, when it is not a development server. - When
startis a development server: aprodorserve:prodscript, then the same file without the file watcher (nodemon server.jsbecomesnode server.js), then themainfield ofpackage.json, then the framework's own production command (node dist/main.jsfor NestJS,next startfor Next.js).
Synq treats these as development servers: nest start (with or without --watch), nodemon,
ts-node-dev, tsx watch, node --watch, bun --watch or --hot, next dev, nuxt dev, a plain vite,
react-scripts start, ng serve, vue-cli-service serve, gatsby develop, remix dev, astro dev,
webpack serve, expo start and parcel, also when they run through another script
("start": "npm run dev") or inside concurrently. Preview servers such as vite preview are not
development servers.
Scripts run with the project's package manager: npm run start:prod, yarn start:prod,
pnpm start:prod or bun run start:prod.
Projects created before Synq 1.2.3 may have npm run start saved as their start command. Nothing needs
changing: when a build step runs and the repository's start script is a development server, the build uses
the production command instead and says so in the build log, for example:
[synq-build] start command "npm run start" runs a dev server ("nest start"); using "npm run start:prod" insteadThis only replaces the plain npm run start, npm start, yarn start, pnpm start or bun run start, and
only with a production script, the file a watcher runs, or the framework's compiled entry. Any other start
command is used as written. To run something else, set the start command with Edit on the project's
Configuration tab, or in synq.json.
Choose a builder
The Builder setting decides which tool turns the source into a container image:
| Builder | What it does | Configure it with |
|---|---|---|
| Auto (default) | Synq detects the framework and writes the build. A Dockerfile in the repository is used when there is one (the rules above). | The deploy screen, or synq.json. |
| Dockerfile | Builds the repository's own Dockerfile. Its CMD starts the app. | Dockerfile path and Build context. |
| Nixpacks | Railway's Nixpacks detects the language and builds with Nix packages. | nixpacks.toml, or NIXPACKS_* environment variables. |
| Railpack | Railway's successor to Nixpacks, built on BuildKit. | railpack.json, or RAILPACK_* environment variables. |
| Buildpacks | Cloud Native Buildpacks from Heroku or Paketo. The Procfile, or the language default, starts the app. | Builder image, a Procfile, and buildpack environment variables. |
Nixpacks, Railpack and Buildpacks use the image's own start command, and the project's environment variables are passed to the build.
Where to set it
- Before the first deploy: on the deploy screen, open the Builder section (it is collapsed and on Auto unless you open it). It is offered for single-app projects on your own servers, not for Compose or multi-service projects.
- Later: open the project, then the Configuration tab, and expand the Builder card ("How this app's image is built on the build server"). Pick a builder, fill in its fields, and press Save builder. The change "Applies on the next deploy": press Deploy or Redeploy afterwards.
Projects that deploy a prebuilt image (Docker image projects and catalog apps that ship as a release) have nothing to build, so they have no Builder card.
Dockerfile
- Dockerfile path: relative to the build context. Leave it empty for
Dockerfile. Example:docker/Dockerfile.prod. - Build context: the directory sent to the build, relative to the repository root. Leave it empty for
the app's root directory. Example:
.for the whole repository, when a monorepo sub-app's Dockerfile copies shared packages.
Both must be paths inside the repository: no leading /, no ... Switching a project to Dockerfile
sets it up the way a Dockerfile import would: the framework becomes Docker, install and build commands are
cleared, the app runs as a web container, and the port comes from the Dockerfile's EXPOSE line (3000
when there is none).
Nixpacks and Railpack
Put nixpacks.toml or railpack.json in the app's folder, or set NIXPACKS_* or RAILPACK_*
variables on the project's Environment tab. For example, NIXPACKS_NODE_VERSION=22 or a
[phases.build] section in nixpacks.toml.
The card also takes extra system packages, so the common cases need no config file. Separate names with
spaces or commas; a name may use letters, digits and . _ + @ - (Mise names also : and /).
| Builder | Field | Becomes | Example |
|---|---|---|---|
| Nixpacks | Nix packages | NIXPACKS_PKGS | ffmpeg imagemagick |
| Nixpacks | APT packages | NIXPACKS_APT_PKGS | libpq-dev |
| Railpack | Railpack version | the Railpack release the build runs | 0.40.0 |
| Railpack | Mise packages | RAILPACK_PACKAGES | node@22 pipx:httpie |
| Railpack | Build APT packages | RAILPACK_BUILD_APT_PACKAGES | build-essential |
| Railpack | Deploy APT packages | RAILPACK_DEPLOY_APT_PACKAGES | ffmpeg |
A field wins over a project variable of the same name. Railpack version pins the whole tool (both
railpack prepare and its BuildKit frontend, ghcr.io/railwayapp/railpack-frontend:v<version>); leave it
empty to use the version Synq ships. A pin stays in place across Synq updates.
Buildpacks
Builder image is one of:
| Builder image | Good for |
|---|---|
heroku/builder:24 (default) | Most apps. |
heroku/builder:22 | Apps that need the older Ubuntu 22.04 base. |
paketobuildpacks/builder-jammy-base | Most apps, Paketo buildpacks. |
paketobuildpacks/builder-jammy-full | Apps that need extra system libraries. |
paketobuildpacks/builder-jammy-tiny | Small images without a shell. |
The list is fixed on purpose: a buildpacks builder gets access to the build server's Docker daemon, so
Synq only runs builders it knows. Add a Procfile (for example web: node server.js) to set the start
command.
Pinned tool versions
The builders run as pinned containers on the build server's Docker daemon, never as programs installed on
the host. The Builder card shows the version, for example "Runs Nixpacks 1.41.0 as a pinned container on
the build server." The current pins are Nixpacks 1.41.0, Railpack 0.40.0 and pack 0.40.9. They change
only with a Synq update, except that a project can pin its own Railpack version.
Static sites and other builders
Every builder except Auto produces a container. If your project is a static site, switching to another builder turns it into a web app on the Docker runtime ("Every builder except Auto runs the app as a container, so this static site becomes a web app on the Docker runtime").
Switching back to Auto
When a project leaves Auto, Synq remembers its framework settings (framework, commands, output directory, port, runtime). Choosing Auto again restores them, and the card says so ("Switching back to Auto restores the Next.js build settings."). A project that was created from its Dockerfile has nothing to restore, so Auto keeps building the Dockerfile. To build it as a framework app instead, use Edit in the Configuration tab and pick a framework.
The "This repo has a Dockerfile" banner
A project imported as a framework app (for example before Dockerfile-first detection existed, or because someone picked the framework by hand) may still have a Dockerfile in its repository. When that happens, the Builder card opens by itself and shows:
This repo has a Dockerfile. Build with it instead? Dockerfile is in the repository, but this project builds as nextjs.
Press Use Dockerfile to switch in one step. Synq changes the builder to Dockerfile, sets the port
from the Dockerfile's EXPOSE line, and shows "Now building with the Dockerfile". The next deploy builds
the Dockerfile. To undo it, choose Auto and save: the previous framework settings come back.
Synq looks for the Dockerfile in the app's root directory first, then at the repository root. The banner needs to read the repository, so it appears for GitHub projects. It does not appear for Git URL projects or uploads, where you can still pick Dockerfile by hand.
API
GET /api/projects/:id/builder: the saved builder, what the next deploy actually uses (effective), whether the repository has an unused Dockerfile (suggestDockerfile, with itsEXPOSEport) and the pinned tool versions.PATCH /api/projects/:idwithbuilder: for example{ "builder": { "type": "dockerfile", "dockerfilePath": "docker/Dockerfile", "dockerContext": "." } },{ "builder": { "type": "buildpacks", "buildpacksBuilder": "paketobuildpacks/builder-jammy-base" } },{ "builder": { "type": "nixpacks", "nixpacksPackages": "ffmpeg", "nixpacksAptPackages": ["libpq-dev"] } }(a package list is a string or an array; the API stores and returns an array), or{ "builder": { "type": "auto" } }. Applied on the next deploy.
Related
Deploy from GitHub
Import a repository and review the detected build.
synq.json
Pin the framework, commands and port in the repository.
Build on another server
Run the same builders on a separate build server and ship the image.
Deployments troubleshooting
Fix a build that fails or an app that does not start.
Deploy from a template or Git URL
Start a project without connecting your own GitHub account, from a one-click app, a framework starter, or any repository URL from GitLab, Bitbucket, Gitea, Forgejo or your own Git server.
Build on another server
Build a project's image on a separate build server so heavy builds never slow production: set up a build server, choose Build on, and what happens to the image, the cache, cancels and failures.