Production Deployment
This guide deploys faynoSync on one Linux server: the API runs next to MongoDB and Redis, Nginx terminates TLS in front of it, and build artifacts live in your cloud object storage. One machine, one .env.
There are two ways to run it, and only a few steps differ between them — those steps have a tab for each:
- Docker Compose (recommended): the API, MongoDB and Redis run as containers from one compose file.
- Binary: the release binary runs as a systemd service, and you bring your own MongoDB and Redis (installed on the server or managed). The binary includes the dashboard and the database migrations.
If you only want to try faynoSync on your machine, use Local Setup instead.
What You Need
- A Linux server
- A domain name with a DNS
Arecord pointing to the server, e.g.faynosync.example.com - Nginx and Certbot on the server (installed in step 8)
- Object storage: AWS S3, DigitalOcean Spaces or Google Cloud Storage, with two buckets:
- a public bucket for public applications (objects must be publicly readable)
- a private bucket for private applications
- recommended: a bucket for edge manifests (
S3_BUCKET_NAME_CDN) — reuse the public bucket or create a dedicated one, see Serve update checks from the edge
- Docker Compose
- Binary
1. Create the Project Directory
mkdir faynosync && cd faynosync
All files below are created in this directory.
2. Generate Secrets
Create .env with freshly generated secrets and restrict its permissions:
- Docker Compose
- Binary
cat > .env <<EOF
API_KEY=$(openssl rand -base64 16)
JWT_SECRET=$(openssl rand -base64 32)
MONGO_ROOT_PASSWORD=$(openssl rand -hex 24)
REDIS_PASSWORD=$(openssl rand -hex 24)
EOF
chmod 600 .env
The compose file creates MongoDB and Redis with these passwords and passes the connection settings to the API.
cat > .env <<EOF
API_KEY=$(openssl rand -base64 16)
JWT_SECRET=$(openssl rand -base64 32)
MONGODB_URL=mongodb://faynosync:$(openssl rand -hex 24)@127.0.0.1/faynosync?authSource=admin
REDIS_PASSWORD=$(openssl rand -hex 24)
EOF
chmod 600 .env
Do not forget to create the MongoDB user faynosync (role readWrite on the faynosync database) and set the Redis requirepass with the generated passwords — without the user the API exits with Authentication failed. With managed MongoDB or Redis, replace MONGODB_URL and REDIS_PASSWORD with the values from your provider.
The API refuses to start when JWT_SECRET is empty or shorter than 32 bytes. Do not copy API_KEY or JWT_SECRET from the repository's .env.example: their insecure-dev-* values are public, and in release mode the API refuses to start with them.
3. Configure the Instance
Append the rest of the configuration to .env and replace the example values:
cat >> .env <<'EOF'
# Release to run (see the note below)
FAYNOSYNC_VERSION=v2.7.0
# Public URL of the API
API_URL=https://faynosync.example.com
# Storage (AWS S3 example)
STORAGE_DRIVER=aws
S3_ACCESS_KEY=your_access_key
S3_SECRET_KEY=your_secret_key
S3_REGION=us-east-1
S3_BUCKET_NAME=your-public-bucket
S3_BUCKET_NAME_PRIVATE=your-private-bucket
S3_BUCKET_NAME_CDN=your-public-bucket
S3_ENDPOINT=https://your-public-bucket.s3.amazonaws.com
S3_ENDPOINT_PRIVATE=https://your-private-bucket.s3.amazonaws.com
# Features
PERFORMANCE_MODE=true
ENABLE_TELEMETRY=true
REPORTS_ENABLED=true
TUF_ENABLED=true
EOF
Set FAYNOSYNC_VERSION to the latest release: v2.7.0. Pinning a version instead of latest means an upgrade happens only when you change this line.
Then add the settings that depend on how you run faynoSync:
- Docker Compose
- Binary
cat >> .env <<'EOF'
# Nginx on the host reaches the container through the Docker network gateway
TRUSTED_PROXIES=172.16.0.0/12
ONLINE_KEY_DIR=/private_keys
EOF
MONGODB_URL, REDIS_HOST, REDIS_PORT and REDIS_PASSWORD for the API are set by the compose file — do not add them.
cat >> .env <<'EOF'
# Nginx on the same host
TRUSTED_PROXIES=127.0.0.1
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
ONLINE_KEY_DIR=/opt/faynosync/private_keys
EOF
Notes:
- For DigitalOcean Spaces set
STORAGE_DRIVER=digitalocean,S3_REGION=fra1andS3_ENDPOINT=fra1.digitaloceanspaces.com/S3_ENDPOINT_PRIVATE=fra1.digitaloceanspaces.com(your region). For GCP seeGCS_CREDENTIALS_FILEin the Environment Variables Overview. - Every other variable (Slack, reports limits, presigned uploads, private downloads) is described in the Environment Variables Overview.
4. Install
- Docker Compose
- Binary
Download the compose file:
curl -fsSLO https://raw.githubusercontent.com/ku9nov/faynoSync/main/deploy/docker-compose.yaml
It runs the API, MongoDB and Redis. You can read it on GitHub before running it.
What this gives you:
- MongoDB and Redis are reachable only inside the compose network — no ports are published for them.
- Compose reads
.envon the host and passes it to the API as environment variables, so the file can staychmod 600; values underenvironment:take precedence. - The API listens on
127.0.0.1:9000only; the outside world reaches it through Nginx. - The API starts after MongoDB and Redis report healthy, and restarts if it crashes.
- The image has a built-in healthcheck (
faynoSync healthcheckprobes/health), sodocker compose psshows whether the API is healthy. - Logs are rotated (5 × 10 MB per container) instead of growing until the disk is full.
If MongoDB 8.0 does not start on your host (docker compose logs db), change the db image to mongo:7.0: faynoSync is fully supported on it. Do this only before the first start — an existing 8.0 data volume cannot be opened by 7.0.
Download the archive for your architecture (amd64 or arm64) from the release, verify it, and install it to /opt/faynosync:
VERSION=v2.7.0
ARCH=amd64
BASE=https://github.com/ku9nov/faynoSync/releases/download/$VERSION
curl -fsSLO "$BASE/faynoSync_${VERSION}_linux_${ARCH}.tar.gz"
curl -fsSLO "$BASE/SHA256SUMS"
sha256sum --check --ignore-missing SHA256SUMS
tar xzf "faynoSync_${VERSION}_linux_${ARCH}.tar.gz"
sudo useradd --system --home-dir /opt/faynosync --shell /usr/sbin/nologin faynosync
sudo install -d -o faynosync -g faynosync -m 750 /opt/faynosync
sudo install -o root -g root -m 755 "faynoSync_${VERSION}_linux_${ARCH}/faynoSync" /opt/faynosync/faynoSync
sudo install -o faynosync -g faynosync -m 600 .env /opt/faynosync/.env
Create /etc/systemd/system/faynosync.service:
[Unit]
Description=faynoSync update server
After=network-online.target
Wants=network-online.target
[Service]
User=faynosync
Group=faynosync
WorkingDirectory=/opt/faynosync
Environment=GIN_MODE=release
ExecStart=/opt/faynosync/faynoSync
Restart=on-failure
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
What this gives you:
- The API reads
/opt/faynosync/.envfrom its working directory; only thefaynosyncuser can read it, and the binary itself is owned by root. GIN_MODE=releaseturns on the release-mode checks: the API refuses to start with theinsecure-dev-*secrets from the repository.- The API restarts if it crashes and can write only to its private
/tmp.
The binary listens on port 9000 on all interfaces. Block it with your firewall so the outside world reaches the API only through Nginx, e.g. with ufw: sudo ufw allow 22,80,443/tcp && sudo ufw enable.
5. Start
- Docker Compose
- Binary
docker compose up -d
docker compose ps
docker compose logs backend
sudo systemctl daemon-reload
sudo systemctl enable --now faynosync
sudo journalctl -u faynosync -f
The log should show:
level=info msg="Connected to MongoDB!"
level=info msg="Redis connection is required. Connecting to Redis."
level=info msg="Connected to Redis successfully"
level=info msg="Telemetry allow-list reloaded after initializing app handler"
With Docker Compose the log also starts with .env file not found, using system variables. This is expected: Compose passes .env to the container as environment variables, not as a file, and the API reads them from the environment.
6. Apply Database Migrations
The API does not run migrations on startup. Apply them once it is running:
- Docker Compose
- Binary
docker compose exec -T backend /usr/bin/faynoSync migrate up
sudo -u faynosync sh -c 'cd /opt/faynosync && exec ./faynoSync migrate up'
The command prints Migrations completed, or No pending migrations to apply when the schema is already current. Migrations create the indexes faynoSync relies on, including the unique index that rejects a second document for the same app version, so do not skip this step. Run it again after every upgrade (see Upgrade).
If the command reports app versions stored in more than one document, no migration was applied: merge or remove the listed documents, then run it again.
7. Create the Admin Account
Register the admin with the API_KEY from your .env (see Sign Up):
curl -X POST -H "Content-Type: application/json" \
-d '{"username": "admin", "password": "<strong-password>", "api_key": "<API_KEY>"}' \
http://127.0.0.1:9000/signup
/signup stays open while API_KEY is set. Once your admin accounts exist, remove the API_KEY line from .env and restart the API to disable it:
- Docker Compose
- Binary
docker compose up -d backend
Use up -d, not restart: restart keeps the old environment.
sudo systemctl restart faynosync
The log then shows API_KEY is not set, /signup is disabled.
8. Nginx and TLS
The commands below are for Debian/Ubuntu. On other distributions, install Nginx, Certbot and the Certbot Nginx plugin with your package manager, put the config in /etc/nginx/conf.d/faynosync.conf and skip the ln -s step.
Install Nginx and Certbot:
sudo apt update
sudo apt install nginx certbot python3-certbot-nginx
Create /etc/nginx/sites-available/faynosync.conf:
# Cache zone for /checkVersion (files in sites-enabled are included inside the http block)
proxy_cache_path /var/cache/nginx-faynosync levels=1:2 keys_zone=checkversion_cache:10m inactive=60s max_size=100m;
server {
server_name faynosync.example.com;
client_max_body_size 1000M;
location / {
proxy_pass http://127.0.0.1:9000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60;
proxy_send_timeout 60;
proxy_read_timeout 60;
send_timeout 60;
}
# Optional: short-lived cache for update checks
location = /checkVersion {
proxy_pass http://127.0.0.1:9000;
proxy_cache checkversion_cache;
proxy_cache_valid 200 60s;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
proxy_cache_lock_timeout 65s;
proxy_cache_background_update on;
proxy_cache_key "$scheme$request_method$host$request_uri";
add_header X-Cache-Status $upstream_cache_status;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
access_log /var/log/nginx/faynosync.access.log;
error_log /var/log/nginx/faynosync.error.log;
listen 80;
}
Enable the site, then let Certbot obtain a certificate and switch the server block to HTTPS:
sudo ln -s /etc/nginx/sites-available/faynosync.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d faynosync.example.com
Certbot renews the certificate automatically. Check the result:
curl https://faynosync.example.com/health
For more caching and rate-limiting options see Performance Mode.
Recommended: Serve Update Checks from the Edge
For any internet-facing instance, serve steady-state update checks from object storage instead of the API. With edge mode, the API answers only the first check for each combination of app, channel, platform, arch and client version after a publish; every later check is a static object read. This is the cheapest way to handle client load, and it needs no API clustering.
- Bucket. Set
S3_BUCKET_NAME_CDN(see What You Need). Manifests are written as public objects, so the bucket must allow public reads. Only the API writes to it. - CDN (optional). Put a CDN in front of that bucket. Manifests carry
Cache-Control: public, max-age=60, must-revalidate, so clients see a publish within about a minute. - Apps. Enable
cdnon each public app with Create Application or Update application. Private apps cannot use it; the API rejects the combination with400. - Clients. Use an official SDK with the edge URL set to the bucket or the CDN in front of it:
EdgeURLin the Go SDK,edgeURLin the JavaScript SDK. The SDK reads the manifest first and falls back to/checkVersionon a miss. - Telemetry. With
ENABLE_TELEMETRY=true, pass a device ID to the SDK. Edge hits never reach/checkVersion, so the SDK reports them through/telemetry/beaconinstead.
Clients that call /checkVersion directly keep working unchanged; they just don't benefit from the edge. For the design, cache keys and invalidation rules, see Edge and S3 Response Cache.
TUF Signing Keys
Not part of the first install: do this when you bootstrap TUF for an app. TUF_ENABLED=true only turns on the TUF API, and the API does not generate keys — you create them outside the server (e.g. with the script from the dashboard), then copy the online signing keys (targets, snapshot, timestamp and delegated roles) into ONLINE_KEY_DIR. Each file is named after its key ID. Root keys stay offline and never go on the server (see TUF Management). If you don't use TUF, set TUF_ENABLED=false.
The key files must be readable by the user the API runs as:
- Docker Compose
- Binary
./private_keys is mounted read-only, and the API runs as UID 65532 inside the container:
sudo chown -R 65532:65532 ./private_keys
sudo chmod 700 ./private_keys && sudo chmod 600 ./private_keys/*
sudo install -d -o faynosync -g faynosync -m 700 /opt/faynosync/private_keys
sudo install -o faynosync -g faynosync -m 600 /path/to/online-keys/* /opt/faynosync/private_keys/
Dashboard
The API serves the dashboard at https://faynosync.example.com/dashboard/; there is nothing to deploy separately. Optional settings:
DASHBOARD_ENABLED=falseturns it off.DASHBOARD_TUF_METADATA_URLpre-fills the TUF metadata URL in the key generation form.ALLOWED_CORSis only needed if you also host a dashboard build on another origin.
Upgrade
Read the release's CHANGELOG entry before upgrading — breaking changes are listed first.
- Docker Compose
- Binary
- Set
FAYNOSYNC_VERSIONin.envto the new release. - Pull and recreate the API container:
docker compose pull backend
docker compose up -d - Apply the migrations shipped with the new version:
docker compose exec -T backend /usr/bin/faynoSync migrate up
- Download and verify the new archive as in step 4, then replace the binary and restart:
sudo install -o root -g root -m 755 "faynoSync_${VERSION}_linux_${ARCH}/faynoSync" /opt/faynosync/faynoSync
sudo systemctl restart faynosync - Apply the migrations shipped with the new version:
sudo -u faynosync sh -c 'cd /opt/faynosync && exec ./faynoSync migrate up'
Restarting takes the API offline for a few seconds; clients retry their next update check, and edge-served checks are not affected.
Backups
| What | Why | How |
|---|---|---|
| MongoDB | apps, versions, users, tokens, settings | mongodump (below) |
.env | secrets and configuration | copy to a secure location |
ONLINE_KEY_DIR | TUF online signing keys | secure offline copy; keep it separate from the database dumps |
| Buckets | build artifacts, feeds, TUF metadata | enable versioning on the buckets |
| Redis | cache, telemetry counters, TUF task and bootstrap state | Redis persistence (the compose file enables AOF); TUF bootstrap state can also be rebuilt with Bootstrap Recovery |
Dump MongoDB to a compressed archive and restore it:
- Docker Compose
- Binary
docker compose exec -T db sh -c \
'mongodump --username root --password "$MONGO_INITDB_ROOT_PASSWORD" --authenticationDatabase admin --archive --gzip' \
> faynosync-$(date +%F).archive.gz
docker compose exec -T db sh -c \
'mongorestore --username root --password "$MONGO_INITDB_ROOT_PASSWORD" --authenticationDatabase admin --archive --gzip --drop' \
< faynosync-2026-01-01.archive.gz
With the MongoDB Database Tools installed and MONGODB_URL from your .env:
mongodump --uri "$MONGODB_URL" --archive --gzip > faynosync-$(date +%F).archive.gz
mongorestore --uri "$MONGODB_URL" --archive --gzip --drop < faynosync-2026-01-01.archive.gz
Security Checklist
- Generate every secret (step 2); never reuse values from the repository.
- Remove
API_KEYonce admin accounts exist (step 7). - Serve the API only over HTTPS (step 8).
- Expose only ports 80 and 443; MongoDB, Redis and the API port stay on the host (with the binary, block port
9000in the firewall). - Keep
TRUSTED_PROXIESset to the addresses your proxy connects from. - Upgrade regularly and read the changelog.
Troubleshooting
backendstays inCreated(Compose) — MongoDB or Redis is not healthy yet. Checkdocker compose psanddocker compose logs db cache.dependency db failed to start(Compose) — checkdocker compose logs db; if MongoDB 8.0 cannot start on your host, usemongo:7.0as described in step 4.- The service exits right after start (binary) — MongoDB or Redis is unreachable or
.envis not readable by thefaynosyncuser; checksudo journalctl -u faynosync. JWT_SECRET is empty or shorter than 32 bytesorholds the public development value— regenerate the secret as in step 2.- Storage access denied — check the storage credentials and bucket permissions; the public bucket must allow public reads.
- Nginx 502 — the API is not running or not listening on port
9000; checkdocker compose logs backendorsudo journalctl -u faynosync.
Environment Variables Reference
For every available environment variable, see the Environment Variables Overview.