Skip to main content

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 A record 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

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:

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.

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
tip

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:

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.

Notes:

  • For DigitalOcean Spaces set STORAGE_DRIVER=digitalocean, S3_REGION=fra1 and S3_ENDPOINT=fra1.digitaloceanspaces.com / S3_ENDPOINT_PRIVATE=fra1.digitaloceanspaces.com (your region). For GCP see GCS_CREDENTIALS_FILE in the Environment Variables Overview.
  • Every other variable (Slack, reports limits, presigned uploads, private downloads) is described in the Environment Variables Overview.

4. Install​

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 .env on the host and passes it to the API as environment variables, so the file can stay chmod 600; values under environment: take precedence.
  • The API listens on 127.0.0.1:9000 only; 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 healthcheck probes /health), so docker compose ps shows whether the API is healthy.
  • Logs are rotated (5 × 10 MB per container) instead of growing until the disk is full.
note

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.

5. Start​

docker compose up -d
docker compose ps
docker compose logs backend

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 exec -T backend /usr/bin/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 up -d backend

Use up -d, not restart: restart keeps the old environment.

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.

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.

  1. 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.
  2. 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.
  3. Apps. Enable cdn on each public app with Create Application or Update application. Private apps cannot use it; the API rejects the combination with 400.
  4. Clients. Use an official SDK with the edge URL set to the bucket or the CDN in front of it: EdgeURL in the Go SDK, edgeURL in the JavaScript SDK. The SDK reads the manifest first and falls back to /checkVersion on a miss.
  5. Telemetry. With ENABLE_TELEMETRY=true, pass a device ID to the SDK. Edge hits never reach /checkVersion, so the SDK reports them through /telemetry/beacon instead.

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:

./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/*

Dashboard​

The API serves the dashboard at https://faynosync.example.com/dashboard/; there is nothing to deploy separately. Optional settings:

  • DASHBOARD_ENABLED=false turns it off.
  • DASHBOARD_TUF_METADATA_URL pre-fills the TUF metadata URL in the key generation form.
  • ALLOWED_CORS is 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.

  1. Set FAYNOSYNC_VERSION in .env to the new release.
  2. Pull and recreate the API container:
    docker compose pull backend
    docker compose up -d
  3. Apply the migrations shipped with the new version:
    docker compose exec -T backend /usr/bin/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​

WhatWhyHow
MongoDBapps, versions, users, tokens, settingsmongodump (below)
.envsecrets and configurationcopy to a secure location
ONLINE_KEY_DIRTUF online signing keyssecure offline copy; keep it separate from the database dumps
Bucketsbuild artifacts, feeds, TUF metadataenable versioning on the buckets
Rediscache, telemetry counters, TUF task and bootstrap stateRedis 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 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

Security Checklist​

  1. Generate every secret (step 2); never reuse values from the repository.
  2. Remove API_KEY once admin accounts exist (step 7).
  3. Serve the API only over HTTPS (step 8).
  4. Expose only ports 80 and 443; MongoDB, Redis and the API port stay on the host (with the binary, block port 9000 in the firewall).
  5. Keep TRUSTED_PROXIES set to the addresses your proxy connects from.
  6. Upgrade regularly and read the changelog.

Troubleshooting​

  • backend stays in Created (Compose) — MongoDB or Redis is not healthy yet. Check docker compose ps and docker compose logs db cache.
  • dependency db failed to start (Compose) — check docker compose logs db; if MongoDB 8.0 cannot start on your host, use mongo:7.0 as described in step 4.
  • The service exits right after start (binary) — MongoDB or Redis is unreachable or .env is not readable by the faynosync user; check sudo journalctl -u faynosync.
  • JWT_SECRET is empty or shorter than 32 bytes or holds 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; check docker compose logs backend or sudo journalctl -u faynosync.

Environment Variables Reference​

For every available environment variable, see the Environment Variables Overview.