Skip to content

Docker Deployment

Synclet ships as a single container image. The fastest way to get running is Docker Compose, which bundles PostgreSQL alongside Synclet. You can also run the container standalone or skip Docker entirely with a bare binary.

Docker Compose starts PostgreSQL 16, runs database migrations, and starts Synclet in one command.

Terminal window
git clone https://github.com/synclet-io/synclet.git
cd synclet
docker compose up -d

The default docker-compose.yml includes four services:

  • postgres — PostgreSQL 16, data stored in a named volume, published on 127.0.0.1:5437.
  • migrate — Runs synclet migrate up once before Synclet starts and exits.
  • local-root — Makes /tmp/synclet_local on the host world-writable (mode 1777) once and exits, so connectors running as any user can write there.
  • synclet — API, scheduler, workers and the Docker executor in a single container (started with server --standalone), published on 127.0.0.1:8080.

The synclet service runs as user: "0:0": root reaches the Docker socket whatever group owns it on the host, and can write to /tmp/synclet and /tmp/synclet_local, which the Docker daemon creates root-owned. Access to the Docker socket is root-equivalent on the host either way.

Once the containers are healthy, open the dashboard at http://localhost:8080.

VariablePurposeDefault
DB_DSNPostgreSQL connection stringSet by Compose
AUTH_JWT_SECRETToken signing key (min. 32 characters)Public default in .env.compose; override in .env
SECRET_ENCRYPTION_KEYBase64-encoded 32-byte AES-256 key for credentialsPublic default in .env.compose; override in .env
PUBLIC_HTTP_SERVER_ADDRPublic HTTP listen address0.0.0.0:8080
INTERNAL_HTTP_SERVER_ADDRInternal HTTP listen address (executor API, /live, /ready, /metrics)0.0.0.0:8081
DOCKER_EXECUTOR_TEMP_DIR_ROOTPer-task scratch dir (config / catalog / state) under which connector bind mounts are created. Required when Synclet itself runs in a container with the host docker.sock mounted — see note below./tmp/synclet
DOCKER_EXECUTOR_LOCAL_ROOTHost directory mounted at /local in every connector container and never cleaned: file-writing destinations (DuckDB, CSV, local JSON, SQLite) leave their files here. Like the temp dir, it must be bind-mounted at the same path on the host./tmp/synclet_local

Generate production secrets before first start:

Terminal window
# AUTH_JWT_SECRET
openssl rand -base64 32
# SECRET_ENCRYPTION_KEY (must be exactly 32 bytes, base64-encoded)
openssl rand -base64 32

If you already have a PostgreSQL 16 instance, run the published image ghcr.io/synclet-io/synclet:<version> on its own (releases are listed on GitHub Releases).

Terminal window
docker run -d \
--name synclet \
--user 0:0 \
-p 8080:8080 \
-e DB_DSN="postgres://synclet:password@host.docker.internal:5432/synclet?sslmode=disable" \
-e AUTH_JWT_SECRET="$(openssl rand -base64 32)" \
-e SECRET_ENCRYPTION_KEY="$(openssl rand -base64 32)" \
-e DOCKER_EXECUTOR_TEMP_DIR_ROOT="/tmp/synclet" \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /tmp/synclet:/tmp/synclet \
ghcr.io/synclet-io/synclet:0.12.0 server --standalone

Why the docker.sock and the temp dir? Synclet needs access to the Docker socket (/var/run/docker.sock) to launch Airbyte connector containers during syncs. The image runs as a non-root user by default; --user 0:0 lets it reach the socket whatever group owns it on the host.

When Synclet itself runs inside a container and talks to the host daemon via that socket, every per-task scratch file (the rendered config.json, catalog.json, and state.json) is created by Synclet on the container’s filesystem, but the daemon that mounts it into the connector container is on the host. DOCKER_EXECUTOR_TEMP_DIR_ROOT is the path inside Synclet’s container where those files are written — it must be bind-mounted from an identically-named path on the host (e.g. both /tmp/synclet) so the host daemon can resolve the bind-mount source.

Running the binary natively on the host? Leave DOCKER_EXECUTOR_TEMP_DIR_ROOT unset — Synclet falls back to the OS temp dir, which the host daemon can already see.

Run synclet migrate up once against the same database before starting the server (or front it with a migrate sidecar — see the bundled compose file).

Verify the container is running:

Terminal window
docker logs synclet

You can run Synclet as a plain binary without Docker.

  1. Download the latest release from GitHub Releases.
  2. Set the required environment variables (see Environment Variables).
  3. Run migrations and start the server:
Terminal window
export DB_DSN="postgres://synclet:password@localhost:5432/synclet?sslmode=disable"
export ENVIRONMENT=production
# Required outside local development; generate each with: openssl rand -base64 32
export AUTH_JWT_SECRET="your-jwt-secret"
export SECRET_ENCRYPTION_KEY="your-encryption-key"
synclet migrate up
synclet server --standalone

The dashboard is available at http://localhost:8080.

Important: Connectors still require Docker on the host: every connector, Synclet’s own Go connectors included, runs as a Docker container.

The Docker executor sets the resources of every connector container it starts: the source and destination of a sync and the containers of check, spec and discover. A source’s or destination’s runtime_config sets them, and each key it leaves out takes the instance default.

runtime_config keyDocker settingWhen unset
memory_limit--memory2Gi
cpu_limit--cpus (NanoCPUs)1 core
memory_request--memory-reservation, capped at the memory limitno reservation
cpu_request--cpu-shares, 1024 per coreno shares set (Docker’s default weight)

Requests on Docker are weaker than on Kubernetes. Docker does not schedule by them or refuse a container that does not fit: the memory reservation is a soft limit the kernel reclaims down to when the host runs short of memory, and CPU shares are a relative weight that only matters while containers compete for CPU.

runtime_config also takes docker_network, the Docker network the connector containers join. It cannot mount host paths: a connector reaches host files only under /local, mounted from DOCKER_EXECUTOR_LOCAL_ROOT (see files written by destinations).

The instance defaults come from PIPELINE_RUNTIME_CPU_REQUEST, PIPELINE_RUNTIME_CPU_LIMIT, PIPELINE_RUNTIME_MEMORY_REQUEST and PIPELINE_RUNTIME_MEMORY_LIMIT (see Environment Variables).

Synclet serves /live and /ready on the internal port (8081 by default) for load balancers and orchestrators:

Terminal window
curl http://localhost:8081/ready
# Returns HTTP 200 when the database, background jobs and event router are up

The default Synclet image is based on distroless and does not include curl or wget. To add Docker health checks, use a multi-stage build that includes a health-check binary, or check from outside the container:

Terminal window
# Health check from the host or a sidecar
curl -f http://localhost:8081/ready
  • Dashboard — The built-in web UI at http://localhost:8080 shows pipeline status, job history, and logs.
  • Prometheus metrics — Synclet exposes a /metrics endpoint compatible with Prometheus on the internal port (8081). See Production Checklist for scraping configuration.

Back up your PostgreSQL database regularly:

Terminal window
pg_dump -Fc "$DB_DSN" > synclet_$(date +%Y%m%d).dump
# Restore
pg_restore -d "$DB_DSN" synclet_20260401.dump

Keep SECRET_ENCRYPTION_KEY with every backup: without it the credentials in the dump cannot be decrypted. See Backup and Restore for the full procedure.

Airbyte is a trademark of Airbyte, Inc. Synclet is not affiliated with, endorsed by or sponsored by Airbyte, Inc. Connector images keep their own licenses: see Connector licenses.