#!/bin/bash set -euo pipefail # ChirpStack runtime setup: # 1. wait for the Cloudron postgresql + redis addons # 2. persist the API JWT secret (rotating it would invalidate tokens) # 3. regenerate /app/data/config/10-cloudron.toml on EVERY start so addon # credentials and OIDC secrets are always current (Cloudron rotates # addon passwords on restore / migration) # 4. seed the operator-owned config fragments ONCE (network + region); # these are meant to be edited with the Cloudron file manager and # survive restarts # 5. exec chirpstack (diesel migrations run automatically at startup) # # Config layout (chirpstack concatenates every *.toml in --config ; # tables must not repeat across files): # 10-cloudron.toml generated: [logging] [postgresql] [redis] [api] # [user_authentication] - DO NOT hand-edit # 50-network.toml seeded once: [network] (net_id, enabled_regions) # region_*.toml seeded once: [[regions]] blocks (gateway MQTT # backend, channel plan, region network overrides) CONFIG_DIR="/app/data/config" GENERATED_CONF="${CONFIG_DIR}/10-cloudron.toml" NETWORK_CONF="${CONFIG_DIR}/50-network.toml" SECRET_FILE="/app/data/.api_jwt_secret" mkdir -p "${CONFIG_DIR}" # --- 1. wait for the addons ------------------------------------------------- wait_tcp() { local host="$1" port="$2" name="$3" echo "Waiting for ${name} at ${host}:${port} ..." until (exec 3<>"/dev/tcp/${host}/${port}") 2>/dev/null; do echo "${name} is unavailable - sleeping" sleep 2 done echo "${name} is up" } wait_tcp "${CLOUDRON_POSTGRESQL_HOST:-127.0.0.1}" "${CLOUDRON_POSTGRESQL_PORT:-5432}" "PostgreSQL" wait_tcp "${CLOUDRON_REDIS_HOST:-127.0.0.1}" "${CLOUDRON_REDIS_PORT:-6379}" "Redis" # --- 2. persistent API JWT secret -------------------------------------------- if [[ ! -s "${SECRET_FILE}" ]]; then ( umask 077; head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' > "${SECRET_FILE}" ) echo "Generated new API JWT secret" fi API_SECRET="$(cat "${SECRET_FILE}")" # --- 3. generated platform config (rewritten on every start) ----------------- toml_escape() { local s="$1" s="${s//\\/\\\\}" s="${s//\"/\\\"}" printf '%s' "${s}" } DB_HOST="${CLOUDRON_POSTGRESQL_HOST:-127.0.0.1}" DB_PORT="${CLOUDRON_POSTGRESQL_PORT:-5432}" DB_NAME="${CLOUDRON_POSTGRESQL_DATABASE:-chirpstack}" DB_USER="$(toml_escape "${CLOUDRON_POSTGRESQL_USERNAME:-chirpstack}")" DB_PASSWORD="$(toml_escape "${CLOUDRON_POSTGRESQL_PASSWORD:-chirpstack}")" PG_DSN="postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=disable" # Cloudron redis requires auth; prefer the platform-provided URL. if [[ -n "${CLOUDRON_REDIS_URL:-}" ]]; then REDIS_SERVERS="$(toml_escape "${CLOUDRON_REDIS_URL}")" else REDIS_PASSWORD="$(toml_escape "${CLOUDRON_REDIS_PASSWORD:-}")" REDIS_SERVERS="redis://:${REDIS_PASSWORD}@${CLOUDRON_REDIS_HOST:-127.0.0.1}:${CLOUDRON_REDIS_PORT:-6379}" fi AUTH_MODE="${CHIRPSTACK_AUTH_MODE:-openid_connect}" OIDC_REGISTRATION="${CHIRPSTACK_OIDC_REGISTRATION:-true}" LOG_LEVEL="${CHIRPSTACK_LOG_LEVEL:-info}" OIDC_ISSUER="$(toml_escape "${CLOUDRON_OIDC_ISSUER:-}")" OIDC_CLIENT_ID="$(toml_escape "${CLOUDRON_OIDC_CLIENT_ID:-}")" OIDC_CLIENT_SECRET="$(toml_escape "${CLOUDRON_OIDC_CLIENT_SECRET:-}")" APP_ORIGIN="$(toml_escape "${CLOUDRON_APP_ORIGIN:-}")" cat > "${GENERATED_CONF}" < "${NETWORK_CONF}" <<'EOF' # Operator configuration - seeded on first start, safe to edit with the # Cloudron file manager (changes apply on restart). # # Do NOT add [postgresql], [redis], [api], [user_authentication] or # [logging] here: those tables are owned by the generated 10-cloudron.toml # and chirpstack fails to parse duplicate tables. [network] # NetID (3 bytes, hex) - MUST be changed to a unique value for this # network. 000000-0000FF is reserved for private / experimental networks # (see LoRa Alliance NetID assignments). net_id = "000001" # Enabled regions; each entry must match the id of a [[regions]] block in # one of the region_*.toml files in this directory. More region files can # be copied from the upstream repo (chirpstack/configuration/). enabled_regions = ["us915_0"] EOF echo "Seeded network config at ${NETWORK_CONF}" fi if [[ ! -f "${CONFIG_DIR}/region_us915_0.toml" ]]; then cat > "${CONFIG_DIR}/region_us915_0.toml" <<'EOF' # US915 region (channels 0-7 + 64) - the standard US915 sub-band plan. # Verbatim from upstream chirpstack/configuration/region_us915_0.toml; # edit the gateway MQTT backend below to point at your broker. [[regions]] id = "us915_0" description = "US915 (channels 0-7 + 64)" common_name = "US915" user_info = "" [regions.gateway] force_gws_private = false [regions.gateway.backend] enabled = "mqtt" [regions.gateway.backend.mqtt] topic_prefix = "us915_0" share_name = "chirpstack" server = "tcp://localhost:1883" username = "" password = "" qos = 0 clean_session = false client_id = "" keep_alive_interval = "30s" ca_cert = "" tls_cert = "" tls_key = "" [[regions.gateway.channels]] frequency = 902300000 bandwidth = 125000 modulation = "LORA" spreading_factors = [7, 8, 9, 10] [[regions.gateway.channels]] frequency = 902500000 bandwidth = 125000 modulation = "LORA" spreading_factors = [7, 8, 9, 10] [[regions.gateway.channels]] frequency = 902700000 bandwidth = 125000 modulation = "LORA" spreading_factors = [7, 8, 9, 10] [[regions.gateway.channels]] frequency = 902900000 bandwidth = 125000 modulation = "LORA" spreading_factors = [7, 8, 9, 10] [[regions.gateway.channels]] frequency = 903100000 bandwidth = 125000 modulation = "LORA" spreading_factors = [7, 8, 9, 10] [[regions.gateway.channels]] frequency = 903300000 bandwidth = 125000 modulation = "LORA" spreading_factors = [7, 8, 9, 10] [[regions.gateway.channels]] frequency = 903500000 bandwidth = 125000 modulation = "LORA" spreading_factors = [7, 8, 9, 10] [[regions.gateway.channels]] frequency = 903700000 bandwidth = 125000 modulation = "LORA" spreading_factors = [7, 8, 9, 10] [[regions.gateway.channels]] frequency = 903000000 bandwidth = 500000 modulation = "LORA" spreading_factors = [8] [regions.network] installation_margin = 10 rx_window = 0 rx1_delay = 1 rx1_dr_offset = 0 rx2_dr = 8 rx2_frequency = 923300000 rx2_prefer_on_rx1_dr_lt = 0 rx2_prefer_on_link_budget = false downlink_tx_power = -1 adr_disabled = false min_dr = 0 max_dr = 3 enabled_uplink_channels = [0, 1, 2, 3, 4, 5, 6, 7, 64] [regions.network.rejoin_request] enabled = false max_count_n = 0 max_time_n = 0 [regions.network.class_b] ping_slot_dr = 8 ping_slot_frequency = 0 EOF echo "Seeded region config at ${CONFIG_DIR}/region_us915_0.toml" fi # --- 5. run ------------------------------------------------------------------- # chirpstack applies the embedded diesel migrations on startup, then serves # the web UI + gRPC/REST API on 0.0.0.0:8080. echo "Starting ChirpStack ..." exec /usr/bin/chirpstack --config "${CONFIG_DIR}"