#!/usr/bin/env bash
# Synkronus one-line installer
#
# curl -fsSL https://raw.githubusercontent.com/OpenDataEnsemble/synkronus-quickstart/main/get.sh | sudo bash
#
# Sets up a complete Synkronus server (Synkronus + PostgreSQL + Caddy for HTTPS)
# on a fresh Ubuntu 24.04 / Debian 12 server, using Podman (recommended) or Docker.
#
# Everything is written to /opt/synkronus. Afterwards, manage the server with
# the `synkronus` command (status, backup, restore, update, ...).
#
# Options (all optional; each also has an environment variable form):
# --domain NAME Your own domain, e.g. data.example.org (SYNK_DOMAIN)
# --ip ADDRESS Public IP to use if detection is wrong (SYNK_PUBLIC_IP)
# --tunnel-token TOKEN Use a Cloudflare Tunnel instead of open ports (SYNK_TUNNEL_TOKEN)
# --local Local testing only: plain http://localhost (SYNK_LOCAL=1)
# --admin-user NAME Admin username (default: admin) (SYNK_ADMIN_USER)
# --admin-password PASS Admin password (default: generated) (SYNK_ADMIN_PASSWORD)
# --runtime podman|docker Container engine (default: podman) (SYNK_RUNTIME)
# --version TAG Synkronus image tag (default: latest) (SYNK_VERSION)
# --yes Never ask questions (for cloud-init / scripts) (SYNK_YES=1)
# --help Show this help
#
# Re-running this script on a server that already has Synkronus changes nothing:
# it shows the status instead. Use `synkronus update` to update.
set -Eeuo pipefail
INSTALL_DIR="${SYNK_DIR:-/opt/synkronus}"
DOMAIN="${SYNK_DOMAIN:-}"
PUBLIC_IP="${SYNK_PUBLIC_IP:-}"
TUNNEL_TOKEN="${SYNK_TUNNEL_TOKEN:-}"
LOCAL_ONLY="${SYNK_LOCAL:-0}"
ADMIN_USER="${SYNK_ADMIN_USER:-admin}"
ADMIN_PASSWORD="${SYNK_ADMIN_PASSWORD:-}"
RUNTIME="${SYNK_RUNTIME:-}"
IMAGE_TAG="${SYNK_VERSION:-latest}"
ASSUME_YES="${SYNK_YES:-0}"
NO_PULL="${SYNK_NO_PULL:-0}" # testing only: use images already present
SUBNET="${SYNK_SUBNET:-10.254.247.0/24}"
# ---------------------------------------------------------------------------
# Output helpers
# ---------------------------------------------------------------------------
if [ -t 1 ]; then
BOLD=$'\033[1m'; GREEN=$'\033[32m'; YELLOW=$'\033[33m'; RED=$'\033[31m'; RESET=$'\033[0m'
else
BOLD=""; GREEN=""; YELLOW=""; RED=""; RESET=""
fi
step() { printf '\n%s==>%s %s%s%s\n' "$GREEN" "$RESET" "$BOLD" "$*" "$RESET"; }
# Long messages are wrapped so they stay readable in narrow browser consoles.
wrap() { fold -s -w "${1:-60}"; }
info() { printf '%s\n' "$*" | wrap 58 | sed 's/^/ /'; }
warn() { printf '%s[!]%s ' "$YELLOW" "$RESET"; printf '%s\n' "$*" | wrap 58 | sed '2,$s/^/ /'; }
die() { printf '\n%s[x] ' "$RED" >&2; printf '%s\n' "$*" | wrap 58 | sed '2,$s/^/ /' >&2; printf '%s' "$RESET" >&2; exit 1; }
trap 'die "Something went wrong on line $LINENO. Nothing was deleted. Run the same command again, or ask for help and include the lines above."' ERR
usage() {
if [ -f "$0" ]; then sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//'
else echo "Options are listed at https://github.com/OpenDataEnsemble/synkronus-quickstart#one-line-installer"; fi
}
while [ $# -gt 0 ]; do
case "$1" in
--domain) DOMAIN="${2:-}"; shift 2 ;;
--ip) PUBLIC_IP="${2:-}"; shift 2 ;;
--tunnel-token) TUNNEL_TOKEN="${2:-}"; shift 2 ;;
--local) LOCAL_ONLY=1; shift ;;
--admin-user) ADMIN_USER="${2:-}"; shift 2 ;;
--admin-password) ADMIN_PASSWORD="${2:-}"; shift 2 ;;
--runtime) RUNTIME="${2:-}"; shift 2 ;;
--version) IMAGE_TAG="${2:-}"; shift 2 ;;
--yes|-y) ASSUME_YES=1; shift ;;
-h|--help) usage; exit 0 ;;
*) die "Unknown option: $1 (use --help to see the options)" ;;
esac
done
# Questions are read from the keyboard even when this script arrives through `curl | bash`.
can_ask() { [ "$ASSUME_YES" != "1" ] && { : /dev/null; }
ask() { # ask "Question" default -> echoes answer
local answer=""
if can_ask; then read -r -p "$1" answer /dev/null 2>&1; then synkronus status || true; fi
printf '\nUseful commands:\n synkronus credentials show the address and admin login\n synkronus update update to the newest version (takes a backup first)\n synkronus help everything else\n\n'
exit 0
fi
# ---------------------------------------------------------------------------
# 1. Check the machine
# ---------------------------------------------------------------------------
step "Checking this server"
[ "$(id -u)" -eq 0 ] || die "Please run this as root. Put 'sudo' in front, like: curl -fsSL | sudo bash"
[ -r /etc/os-release ] || die "Could not detect the operating system. Please use Ubuntu 24.04 or Debian 12."
OS_ID="$( . /etc/os-release; echo "${ID:-}")"
OS_NAME="$( . /etc/os-release; echo "${PRETTY_NAME:-unknown}")"
case "$OS_ID" in
ubuntu|debian) ;;
*) die "This installer supports Ubuntu and Debian (found: $OS_NAME). Please create an Ubuntu 24.04 server." ;;
esac
info "Operating system: $OS_NAME"
ARCH="$(uname -m)"
case "$ARCH" in x86_64|aarch64|arm64) ;; *) die "Unsupported processor type: $ARCH (needs x86_64 or arm64)." ;; esac
MEM_MB=$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo)
info "Memory: ${MEM_MB} MB"
[ "$MEM_MB" -ge 900 ] || die "This server has ${MEM_MB} MB of memory. Synkronus needs at least 1 GB. Choose a bigger server size."
DISK_MB=$(df -Pm / | awk 'NR==2 {print $4}')
info "Free disk: ${DISK_MB} MB"
[ "$DISK_MB" -ge 5000 ] || die "Only ${DISK_MB} MB of free disk space. Please free up space or choose a server with at least 10 GB."
export DEBIAN_FRONTEND=noninteractive
apt_install() { apt-get install -y -q "$@" >/dev/null; }
if ! command -v curl >/dev/null || ! command -v openssl >/dev/null; then
apt-get update -q >/dev/null
apt_install curl openssl ca-certificates
fi
# Small servers: add swap so the database is not killed when memory runs low.
if [ "$MEM_MB" -lt 2000 ] && [ "$(awk '/SwapTotal/ {print $2}' /proc/meminfo)" -eq 0 ] && [ ! -e /swapfile ]; then
info "Adding 2 GB of swap space (helps small servers stay stable)"
if fallocate -l 2G /swapfile 2>/dev/null && chmod 600 /swapfile && mkswap /swapfile >/dev/null && swapon /swapfile 2>/dev/null; then
grep -q '^/swapfile ' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
else
rm -f /swapfile; warn "Could not add swap space. Continuing without it."
fi
fi
# ---------------------------------------------------------------------------
# 2. Choose how people will reach the server
# ---------------------------------------------------------------------------
step "Choosing your server's web address"
detect_ip() {
local ip=""
for url in https://api.ipify.org https://ipv4.icanhazip.com https://ifconfig.me/ip; do
ip="$(curl -4 -fsS --max-time 6 "$url" 2>/dev/null | tr -d '[:space:]')" || true
if [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then printf '%s' "$ip"; return 0; fi
done
return 1
}
if [ -n "$TUNNEL_TOKEN" ]; then
MODE="tunnel"
info "Using a Cloudflare Tunnel. No ports need to be opened on this server."
elif [ "$LOCAL_ONLY" = "1" ]; then
MODE="local"
info "Local testing mode: http://localhost (no HTTPS)."
else
if [ -z "$DOMAIN" ] && can_ask; then
printf '\n Do you have your own web address (domain) for this server, like data.example.org?\n'
printf ' If not, just press Enter and you will get a free address that works right away.\n\n'
DOMAIN="$(ask " Your domain (or press Enter to skip): " "")"
DOMAIN="$(printf '%s' "$DOMAIN" | tr -d '[:space:]' | sed -e 's#^https\?://##' -e 's#/.*$##' | tr 'A-Z' 'a-z')"
fi
if [ -z "$PUBLIC_IP" ]; then
PUBLIC_IP="$(detect_ip)" || die "Could not find this server's public IP address. Run again with: --ip YOUR.SERVER.IP"
fi
info "This server's public IP: $PUBLIC_IP"
if [ -n "$DOMAIN" ]; then
MODE="domain"; SITE_HOST="$DOMAIN"
else
MODE="sslip"; SITE_HOST="${PUBLIC_IP}.sslip.io"
fi
fi
case "$MODE" in
domain|sslip) URL="https://${SITE_HOST}" ;;
local) URL="http://localhost" ;;
tunnel) URL="${DOMAIN:+https://$DOMAIN}"; URL="${URL:-(the public hostname you set in Cloudflare)}" ;;
esac
info "Your server will be at: ${BOLD}${URL}${RESET}"
# Domain must point here, otherwise HTTPS cannot be set up.
if [ "$MODE" = "domain" ]; then
RESOLVED="$(getent ahostsv4 "$DOMAIN" 2>/dev/null | awk 'NR==1 {print $1}')" || true
if [ "$RESOLVED" != "$PUBLIC_IP" ]; then
warn "Your domain ${DOMAIN} does not point to this server yet (it points to: ${RESOLVED:-nothing})."
info "Fix: at your domain provider, create an 'A record' for ${DOMAIN} with the value ${PUBLIC_IP}."
info "If you use Cloudflare, set it to 'DNS only' (grey cloud) for now."
info "HTTPS will start working automatically a few minutes after the record is correct."
else
info "Domain check: ${DOMAIN} points to this server. Good."
fi
fi
# Ports 80 and 443 must be free for Caddy (not needed with a tunnel).
if [ "$MODE" != "tunnel" ] && command -v ss >/dev/null; then
for p in 80 443; do
[ "$MODE" = "local" ] && [ "$p" = "443" ] && continue
if ss -Hltn "sport = :$p" 2>/dev/null | grep -q .; then
die "Port $p is already used by another program on this server (often a web server like nginx or apache). Stop it first, e.g.: systemctl disable --now nginx apache2"
fi
done
fi
# ---------------------------------------------------------------------------
# 3. Install the container engine
# ---------------------------------------------------------------------------
step "Installing the container engine"
if [ -z "$RUNTIME" ]; then
if command -v podman >/dev/null; then RUNTIME=podman
elif command -v docker >/dev/null && docker info >/dev/null 2>&1; then RUNTIME=docker
else RUNTIME=podman
fi
fi
podman_major() { podman --version 2>/dev/null | awk '{print $3}' | cut -d. -f1; }
if [ "$RUNTIME" = "podman" ]; then
if ! command -v podman >/dev/null || ! command -v podman-compose >/dev/null; then
info "Installing Podman (this takes a minute or two)"
apt-get update -q >/dev/null
apt_install podman podman-compose || true
apt_install aardvark-dns netavark 2>/dev/null || true
fi
if ! command -v podman-compose >/dev/null || [ "$(podman_major || echo 0)" -lt 4 ]; then
warn "This system's Podman is too old or incomplete (Ubuntu 22.04 and older). Using Docker instead."
RUNTIME=docker
fi
fi
if [ "$RUNTIME" = "docker" ]; then
if ! command -v docker >/dev/null; then
info "Installing Docker (this takes a minute or two)"
curl -fsSL https://get.docker.com | sh >/dev/null 2>&1 || die "Docker could not be installed. Check the server's internet connection and try again."
fi
systemctl enable --now docker >/dev/null 2>&1 || true
docker compose version >/dev/null 2>&1 || apt_install docker-compose-plugin || die "Docker Compose is missing. Install it with: apt-get install docker-compose-plugin"
fi
info "Using: $RUNTIME"
# ---------------------------------------------------------------------------
# 4. Write the configuration
# ---------------------------------------------------------------------------
step "Writing the configuration to $INSTALL_DIR"
umask 077
mkdir -p "$INSTALL_DIR/backups"
# Data left behind by an earlier installation whose settings were deleted cannot be
# opened with new passwords, so stop instead of failing later.
if [ ! -f "$INSTALL_DIR/.env" ] && "$RUNTIME" volume inspect synkronus_pgdata >/dev/null 2>&1; then
die "This server has Synkronus data from an earlier installation, but its settings file ($INSTALL_DIR/.env) is gone, so the data cannot be opened. If you don't need that data, remove it with: $RUNTIME volume rm synkronus_pgdata synkronus_appdata and run the installer again."
fi
# An earlier attempt that did not finish: keep its secrets, because the database
# may already have been created with them.
PREV_DB_PASSWORD=""; PREV_JWT=""
if [ -f "$INSTALL_DIR/.env" ]; then
info "Reusing the settings from the earlier installation"
PREV_DB_PASSWORD="$( . "$INSTALL_DIR/.env"; echo "${POSTGRES_PASSWORD:-}")"
PREV_JWT="$( . "$INSTALL_DIR/.env"; echo "${JWT_SECRET:-}")"
[ -n "$ADMIN_PASSWORD" ] || ADMIN_PASSWORD="$( . "$INSTALL_DIR/.env"; echo "${ADMIN_PASSWORD:-}")"
fi
[ -n "$ADMIN_PASSWORD" ] || ADMIN_PASSWORD="$(gen_secret 20)"
DB_PASSWORD="${PREV_DB_PASSWORD:-$(gen_secret 32)}"
JWT="${PREV_JWT:-$(gen_secret 48)}"
cat > "$INSTALL_DIR/.env" < /usr/local/bin/synkronus <<'HELPER'
#!/usr/bin/env bash
# synkronus: manage the Synkronus server installed by get.sh
set -Eeuo pipefail
DIR="${SYNK_DIR:-/opt/synkronus}"
[ -f "$DIR/.env" ] || { echo "Synkronus is not installed here ($DIR/.env not found)."; exit 1; }
[ "$(id -u)" -eq 0 ] || exec sudo -E "$0" "$@"
set -a; . "$DIR/.env"; set +a
RT="$SYNK_RUNTIME"
if [ "$RT" = "podman" ]; then COMPOSE=(podman-compose -p synkronus -f "$DIR/compose.yml"); else COMPOSE=(docker compose -p synkronus -f "$DIR/compose.yml"); fi
compose() { (cd "$DIR" && "${COMPOSE[@]}" "$@"); }
yq() { local v="${1//\'/\'\'}"; printf "'%s'" "$v"; } # YAML single-quoted string
render() { # writes compose.yml and Caddyfile from .env
local ports="" caddy_ip
case "$SYNK_MODE" in
domain|sslip) ports=$' ports:\n - "80:80"\n - "443:443"\n - "443:443/udp"' ;;
local) ports=$' ports:\n - "80:80"' ;;
tunnel) ports="" ;;
esac
case "$SYNK_MODE" in
domain|sslip) cat > "$DIR/Caddyfile" < "$DIR/Caddyfile" < "$DIR/Caddyfile" < "$DIR/compose.yml" </dev/null 2>&1; }
wait_healthy() { # wait_healthy SECONDS
local i=0
while [ "$i" -lt "${1:-120}" ]; do healthy && return 0; sleep 3; i=$((i+3)); done
return 1
}
up() { compose up -d >/dev/null 2>&1 || compose up -d; }
down() { compose down >/dev/null 2>&1 || true; }
backup() {
local stamp tmp out
stamp="$(date +%Y%m%d-%H%M%S)"; tmp="$(mktemp -d)"; out="$DIR/backups/synkronus-$stamp.tar.gz"
echo "Backing up the database..."
"$RT" exec synkronus-db pg_dump -U synkronus -d synkronus --no-owner > "$tmp/database.sql"
echo "Backing up app bundles and attachments..."
"$RT" exec synkronus tar czf - -C /app/data . > "$tmp/appdata.tar.gz"
printf 'created=%s\nimage=%s\nurl=%s\n' "$stamp" "$SYNK_IMAGE" "$SYNK_URL" > "$tmp/info.txt"
tar czf "$out" -C "$tmp" database.sql appdata.tar.gz info.txt
rm -rf "$tmp"; chmod 600 "$out"
find "$DIR/backups" -name 'synkronus-*.tar.gz' -mtime +"${BACKUP_KEEP_DAYS:-7}" -delete 2>/dev/null || true
echo "Backup saved: $out ($(du -h "$out" | cut -f1))"
}
restore() {
local file="${1:-}" tmp
[ -f "$file" ] || { echo "Usage: synkronus restore /opt/synkronus/backups/synkronus-YYYYMMDD-HHMMSS.tar.gz"; ls -1t "$DIR/backups" 2>/dev/null | head -5; exit 1; }
echo "This replaces ALL current data on this server with the backup:"; echo " $file"
if [ "${SYNK_YES:-0}" != "1" ]; then read -r -p "Type YES to continue: " ok /dev/null
"$RT" exec synkronus-db psql -q -U synkronus -d postgres -c 'DROP DATABASE IF EXISTS synkronus WITH (FORCE);' -c 'CREATE DATABASE synkronus OWNER synkronus;'
"$RT" exec -i synkronus-db psql -q -U synkronus -d synkronus -v ON_ERROR_STOP=1 < "$tmp/database.sql" >/dev/null
"$RT" start synkronus >/dev/null
rm -rf "$tmp"
wait_healthy 90 && echo "Restore finished. Synkronus is running." || { echo "Restore finished but Synkronus is not healthy yet. Check: synkronus logs"; exit 1; }
}
case "${1:-help}" in
status)
echo "Address: $SYNK_URL"
echo "Version: $SYNK_IMAGE"
if healthy; then echo "Synkronus: running"; else echo "Synkronus: NOT responding (try: synkronus restart, then synkronus logs)"; fi
"$RT" ps --format '{{.Names}}\t{{.Status}}' 2>/dev/null | grep '^synkronus' | sed 's/^/ /' || true
last="$(ls -1t "$DIR"/backups/synkronus-*.tar.gz 2>/dev/null | head -1 || true)"
echo "Last backup: ${last:-none yet}"
echo "Free disk: $(df -Ph / | awk 'NR==2 {print $4}')"
;;
credentials)
cat "$DIR/CREDENTIALS.txt" 2>/dev/null || { echo "Address: $SYNK_URL"; echo "Admin username: $ADMIN_USERNAME"; echo "Admin password: $ADMIN_PASSWORD"; }
;;
start) render; up; wait_healthy 120 && echo "Synkronus is running at $SYNK_URL" ;;
stop) compose stop >/dev/null 2>&1 || compose stop; echo "Stopped." ;;
restart) render; down; up; wait_healthy 120 && echo "Synkronus is running at $SYNK_URL" || { echo "Synkronus did not start. See: synkronus logs"; exit 1; } ;;
logs) shift; "$RT" logs --tail "${1:-100}" synkronus 2>&1; echo "--- web server (caddy) ---"; "$RT" logs --tail 20 synkronus-caddy 2>&1 ;;
backup) backup ;;
restore) shift; restore "${1:-}" ;;
update)
echo "Taking a backup first..."; backup
old="$("$RT" image inspect --format '{{.Id}}' "$SYNK_IMAGE" 2>/dev/null || true)"
echo "Downloading the newest version..."; compose pull
render; down; up
if wait_healthy 150; then echo "Updated. Synkronus is running at $SYNK_URL"; "$RT" image prune -f >/dev/null 2>&1 || true
else
echo "The new version did not start. Going back to the previous version..."
[ -n "$old" ] && "$RT" tag "$old" "$SYNK_IMAGE"; down; up
wait_healthy 120 && echo "Back on the previous version. Please report the problem." || echo "Still not running. See: synkronus logs"
exit 1
fi
;;
reset-admin-password)
user="${2:-$ADMIN_USERNAME}"; pass="${3:-$(openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | cut -c1-20)}"
echo "Setting a new password for '$user'..."
SYNKRONUS_RECOVERY_CREATE_USER="$user" SYNKRONUS_RECOVERY_CREATE_PASS="$pass" render
down; up; wait_healthy 120 || { echo "Synkronus did not start. See: synkronus logs"; exit 1; }
unset SYNKRONUS_RECOVERY_CREATE_USER SYNKRONUS_RECOVERY_CREATE_PASS; render; down; up; wait_healthy 120 || true
if [ "$user" = "$ADMIN_USERNAME" ]; then
sed -i "s/^ADMIN_PASSWORD=.*/ADMIN_PASSWORD=$pass/" "$DIR/.env"
sed -i "s/^Admin password: .*/Admin password: $pass/" "$DIR/CREDENTIALS.txt" 2>/dev/null || true
fi
echo "Done. New login -> username: $user password: $pass"
;;
uninstall)
echo "This stops Synkronus and removes it from this server."
if [ "${2:-}" = "--delete-data" ]; then
read -r -p "ALL data will be deleted forever. Type DELETE to continue: " ok /dev/null 2>&1 || true; rm -rf "$DIR"
else
compose down >/dev/null 2>&1 || true; rm -f "$DIR/.installed"
echo "Your data and backups are kept in $DIR and in the container volumes."
echo "Running the installer again brings Synkronus back with the same data."
fi
systemctl disable --now synkronus.service >/dev/null 2>&1 || true
rm -f /etc/systemd/system/synkronus.service /etc/cron.d/synkronus-backup /usr/local/bin/synkronus
echo "Removed."
;;
render) render ;;
boot) render; up ;;
help|-h|--help|*)
cat <
status Is the server running? Address, version, last backup
credentials Show the address and admin login again
backup Back up the database and all files now
restore FILE Put back a backup (replaces current data)
update Back up, then update to the newest version
reset-admin-password Set a new admin password and show it
logs Show recent server messages
start | stop | restart
uninstall Remove Synkronus (keeps data unless --delete-data)
Settings: $DIR/.env Backups: $DIR/backups (daily at 02:00, kept ${BACKUP_KEEP_DAYS:-7} days)
EOF
;;
esac
HELPER
chmod 755 /usr/local/bin/synkronus
umask 022
# Start on boot (Podman does not restart containers after a reboot by itself).
if [ -d /run/systemd/system ]; then
cat > /etc/systemd/system/synkronus.service </dev/null 2>&1 || true
fi
# Nightly backup at 02:00.
cat > /etc/cron.d/synkronus-backup <<'EOF'
# Nightly Synkronus backup (written by get.sh)
0 2 * * * root /usr/local/bin/synkronus backup >> /var/log/synkronus-backup.log 2>&1
EOF
chmod 644 /etc/cron.d/synkronus-backup
command -v cron >/dev/null || apt_install cron >/dev/null 2>&1 || warn "Could not install cron; nightly backups need it."
# Open the firewall if one is active (never turned on by us, so SSH cannot be locked out).
if [ "$MODE" != "tunnel" ] && command -v ufw >/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then
ufw allow 80/tcp >/dev/null; ufw allow 443/tcp >/dev/null; ufw allow 443/udp >/dev/null
info "Firewall: opened ports 80 and 443"
fi
# ---------------------------------------------------------------------------
# 5. Start
# ---------------------------------------------------------------------------
step "Starting Synkronus (the first start downloads about 500 MB, please wait)"
synkronus render
if [ "$NO_PULL" != "1" ]; then
(cd "$INSTALL_DIR" && if [ "$RUNTIME" = podman ]; then podman-compose -p synkronus -f compose.yml pull; else docker compose -p synkronus -f compose.yml pull; fi) >/dev/null 2>&1 \
|| die "Could not download the Synkronus software. Check the server's internet connection and run the same command again."
fi
synkronus boot >/dev/null 2>&1 || synkronus boot
info "Waiting for Synkronus to be ready..."
ok=0
for _ in $(seq 1 60); do
if "$RUNTIME" exec synkronus wget -qO- -T 3 http://127.0.0.1:8080/health >/dev/null 2>&1; then ok=1; break; fi
sleep 3
done
[ "$ok" = 1 ] || die "Synkronus did not start within 3 minutes. See what happened with: synkronus logs"
info "Synkronus is running."
HTTPS_NOTE=""
if [ "$MODE" = "domain" ] || [ "$MODE" = "sslip" ]; then
info "Getting the HTTPS certificate (up to 2 minutes)..."
ok=0
for i in $(seq 1 40); do
if curl -fsS --max-time 5 --resolve "${SITE_HOST}:443:127.0.0.1" "https://${SITE_HOST}/health" >/dev/null 2>&1; then ok=1; break; fi
[ "$i" = 20 ] && "$RUNTIME" restart synkronus-caddy >/dev/null 2>&1 || true
sleep 3
done
if [ "$ok" = 1 ]; then info "HTTPS is working."
else
HTTPS_NOTE="HTTPS is not ready yet. Make sure ports 80 and 443 are open in your provider's firewall$( [ "$MODE" = domain ] && echo " and that ${DOMAIN} points to ${PUBLIC_IP}" ). It will start working by itself once they are."
warn "$HTTPS_NOTE"
fi
fi
# ---------------------------------------------------------------------------
# 6. Save and show the result
# ---------------------------------------------------------------------------
umask 077
{
echo "Synkronus server"
echo "Address: $URL"
echo "Admin username: $ADMIN_USER"
echo "Admin password: $ADMIN_PASSWORD"
echo
echo "Log in to the Portal at the address above. Then create a separate"
echo "Read/Write user for phones (Users > Create user); keep the admin login for yourself."
} > "$INSTALL_DIR/CREDENTIALS.txt"
date -u +%Y-%m-%dT%H:%M:%SZ > "$INSTALL_DIR/.installed"
umask 022
line="============================================================"
printf '\n%s%s%s\n' "$GREEN" "$line" "$RESET"
printf '%s Your Synkronus server is ready%s\n\n' "$BOLD" "$RESET"
printf ' Address: %s%s%s\n' "$BOLD" "$URL" "$RESET"
printf ' Admin username: %s%s%s\n' "$BOLD" "$ADMIN_USER" "$RESET"
printf ' Admin password: %s%s%s\n\n' "$BOLD" "$ADMIN_PASSWORD" "$RESET"
printf ' Copy these somewhere safe. They are also saved in\n %s/CREDENTIALS.txt\n (show them again with: synkronus credentials)\n' "$INSTALL_DIR"
if [ "$MODE" = "tunnel" ]; then
printf '\n Last step, in Cloudflare: in your tunnel add a\n Public Hostname for %s\n with Service type HTTP and URL caddy:80\n' "${DOMAIN:-your address}"
fi
[ -n "$HTTPS_NOTE" ] && printf '\n %sNote:%s %s\n' "$YELLOW" "$RESET" "$(printf '%s' "$HTTPS_NOTE" | fold -s -w 52 | sed '2,$s/^/ /')"
printf '\n Next:\n 1. Open the address in your browser and log in.\n 2. Under Users, create a Read/Write user\n for your phones.\n 3. Upload your app bundle, then connect\n Formulus with that user.\n\n Manage the server with: synkronus help\n'
printf '%s%s%s\n\n' "$GREEN" "$line" "$RESET"