# Build on another server
URL: https://synquic.tech/docs/guides/build-servers.md

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.

import { Step, Steps } from 'fumadocs-ui/components/steps';

By default a project is built on the server it runs on. A big `next build`, a Rust compile or a Buildpacks
run then competes with your live apps for CPU, memory and disk. **Build on** moves the build to another of
your servers: the image is built there and then sent to the deploy server, which only has to load it and
start the container.

Nothing else about the build changes. The same builder runs (Auto with Synq's generated Dockerfiles, your
Dockerfile, Nixpacks, Railpack or Buildpacks), with the same settings, the same build log and the same
Cancel button.

<Callout title="What you need first">
- Two of your own servers with Docker, connected to Synq under **Servers**: the one the app runs on and the
  one that builds.
- The same CPU architecture on both (both amd64, or both arm64). An image built on an arm64 server does not
  start on an amd64 server.
- Permission to change the build server's settings and the project's settings.
</Callout>

## Set up a build server

<Steps>

<Step>

### Turn on "Can build for other servers"

Open **Servers**, pick the server that should build, and open its **Docker** tab. Turn on **Can build for
other servers** in the **Build for other servers** card. It is off for every server until you turn it on, so
no server builds code for another one by surprise.

</Step>

<Step>

### Choose it under Build on

Open the project, go to **Configuration**, and open the **Build on** card. Pick the build server instead of
**Deploy server**, then **Save**. The deploy server itself is not listed: building there is the default.

</Step>

<Step>

### Deploy

The setting applies from the next deploy. The build log says where the build runs, for example
`→ Build server: builder-1 (amd64). The image is built there and shipped to the deploy server over SSH (zstd).`

</Step>

</Steps>

A compose or monorepo project uses the same card: every service image is built on the build server and sent
to the deploy server as soon as it is ready, while the build server moves on to the next service.

## What happens during a deploy

1. **Checks before anything is cloned.** Synq connects to the build server and checks that it still builds for
   other servers, that Docker answers, that its CPU architecture matches the deploy server's, and that it has
   at least 2 GB of disk left for Docker.
2. **The build** runs on the build server exactly like a build on the deploy server. Clone on the server,
   per-server GitHub access and forwarded git credentials apply to the build server, because that is where
   the source is fetched. Project variables reach the build as a BuildKit secret, never as an image layer or a
   command-line argument.
3. **The hand-off.** Synq checks the image's CPU architecture and that the deploy server has room for it,
   then streams `docker save` from the build server into `docker load` on the deploy server through the SSH
   connections the panel already holds. The stream is compressed with zstd when both servers have `zstd`,
   otherwise gzip. Nothing is written to a temporary file, no registry is involved, and no port is opened on
   either server.
4. **Verification.** On the deploy server Synq inspects the loaded image and compares it with the one that was
   built: the same image ID, or, when the two Docker daemons use different image stores, the same layers,
   platform and container settings. An image that does not match is deleted and the deploy fails.
5. **Clean up.** The image is removed from the build server. On the deploy server it carries the usual tag and
   labels, so rollbacks, the rollback window and image cleanup treat it like any other build.

The bytes travel from the build server to the machine running the panel and on to the deploy server. When the
panel runs on your laptop (the desktop app), keep the image size in mind.

## Build cache

The build cache stays on the build server: BuildKit's layer cache and the package-manager caches that Synq's
generated Dockerfiles mount are reused by the next build of the project there.
**Clear build cache** on a project that builds elsewhere clears the build server's cache. The daily build-cache
sweep keeps every server's cache under 10 GB, build servers included.

A build server without the docker buildx plugin uses Docker's legacy builder, which keeps no cache between
remote builds (the image is removed after the hand-off) and cannot pass variables as secrets. Install
`docker-buildx-plugin` on build servers.

## When the build server can't build

| Problem | What happens |
| --- | --- |
| The build server is offline, or Docker on it does not answer | The deploy fails before cloning: "Can't reach build server builder-1: ..." |
| "Can build for other servers" was turned off, or the server was removed | The deploy fails and names the setting. Removing a server sends its projects back to building on their deploy server. |
| The build server's CPU architecture differs from the deploy server's | The deploy fails: "The build server builder-1 builds arm64 images, but the deploy server runs on amd64". Synq does not cross-compile. |
| Less than 2 GB of free Docker disk on the build server | The deploy fails before the build. A build that runs out of disk midway says which server to clean up. |
| The deploy server has no room for the image | The transfer does not start, and the message gives both sizes. |
| The transfer breaks or stalls for 10 minutes | The deploy fails; the partial image is removed from the deploy server. |

To build on the deploy server instead of failing in the first four cases, turn on **Build on the deploy
server if this one is unavailable** in the **Build on** card. The deploy log then says why it fell back. It is
off by default: when you move builds off production, production should not start building on its own.

## Cancelling

**Cancel** stops the build on the build server (its `docker build` and any build containers there) and stops
an image transfer that is under way. An image that was already loaded on the deploy server for the cancelled
deploy is removed again.

## What always builds on the deploy server

- **Static sites.** Their files are extracted from the build container into the folder the edge serves, on the
  deploy server. In a compose project, static services build on the deploy server while the other services
  build on the build server.
- **Projects that deploy a prebuilt image** and **rollbacks that restore a kept image** build nothing.
- **Apps that run directly on the host** (without Docker), **Synq Cloud** projects and **cluster** projects,
  which have builders of their own.
- **App templates with an inline Dockerfile** (one-click apps that ship their own build files).

## From the API

Turn a server into a build server, then point a project at it:

```bash
curl -X PATCH -H "Authorization: Bearer $SYNQ_TOKEN" -H "Content-Type: application/json" \
  -d '{"buildServer":true}' \
  "$SYNQ_URL/api/system/servers/$BUILD_SERVER_ID"

curl -X PATCH -H "Authorization: Bearer $SYNQ_TOKEN" -H "Content-Type: application/json" \
  -d '{"buildServerId":"'"$BUILD_SERVER_ID"'","buildServerFallback":false}' \
  "$SYNQ_URL/api/projects/$PROJECT_ID"
```

Send `"buildServerId":null` to build on the deploy server again. The server must belong to the project's
organization and have `buildServer` on, or the request fails with `400`.

## Related guides

<Cards>
  <Card title="Build detection and builders" href="/docs/guides/build-detection" description="How the image is built: Dockerfile, generated Dockerfiles, Nixpacks, Railpack, Buildpacks." />
  <Card title="Custom servers" href="/docs/guides/custom-servers" description="Connect the servers that build and run your apps." />
  <Card title="Docker cleanup" href="/docs/guides/docker-cleanup" description="Free disk on a server, build servers included." />
</Cards>
