INOTrackfast, reliable, unapologetic

Installing inotrack on your own server

The admin panel is currently available in Russian only; menu items and button labels in this guide are translated.

Requirements

  • Ubuntu or Debian (LTS). The installer checks the OS and stops on other distributions.
  • Free ports 80 and 443 (Caddy gets the TLS certificate from Let's Encrypt itself).
  • Docker and docker compose — if they are missing, the installer installs them.
  • Domain(s) whose A record already points to this server's IP: one for the admin panel and one or more for receiving clicks (tracker domains). The installer checks the A record before starting (you can skip it with a flag if the server is behind NAT/CDN). No domains of your own? The installer issues free inotrack domains (see below).
  • A license key, purchased at https://inotrack.run/en/buy/ (see "How to get a license key" below). No key? The installer sets up a 7-day trial.
  • Root SSH access — the script installs system packages, writes to /opt/inotrack and edits systemd/cron.
  • ClickHouse: the standard install brings up its own (the image is pinned to 24.8 — deploy/docker-compose.yml/deploy/k8s/clickhouse.yaml). If you connect your OWN already-deployed ClickHouse instead (INOTRACK_CH_ADDRS), the version can be older; below 24.8 the product still starts (24.8 is not a hard requirement), but the daily report summary works only on 23.12 and newer (it needs REFRESHABLE MATERIALIZED VIEW) — what exactly is lost on an older version and why that is safe is in the operations guide (in Russian), section "ClickHouse: version and the daily report summary".

Server requirements (load of 1-10M clicks/day)

Minimum Recommended (1-10M clicks/day) Growth/scaling
CPU 2 vCPU 4 vCPU more offers/flows with anti-fraud (4.8, 4.9.x) and cost-sync (8.1) — size by CPU, not by clicks
RAM 4 GB 8 GB Postgres/ClickHouse/Redis/tracker-core share one box — it grows with disk and the number of concurrent reports (7.x)
Disk 15 GB (the installer's hard limit) 160 GB NVMe see the growth estimate below; NVMe is not optional: ClickHouse on a network/HDD disk cannot keep up merging parts at peaks of ~2k rps
Network — public IP, 80/443 open Caddy gets TLS from Let's Encrypt itself

The average load is ~115 rps (1-10M/day), the peak is up to ~2000 rps (ad campaigns, traffic spikes). On a minimum-spec server the installer (check_resources()) only warns and does not stop the install, except for disk: with less than 15 GB of free space it will not proceed, even in interactive mode, without explicit confirmation.

Disk growth estimate

ClickHouse is the main consumer of space (raw clicks + conversions + postbacks, default TTL 13/36/3/13 months, deploy/clickhouse/ttl-policy.sql). The rule of thumb is ~100-300 bytes per click after ClickHouse compression (it depends on how many of the sub1-sub10/UTM fields are actually filled and how uniform they are — LowCardinality columns like country/device/os compress far better than arbitrary strings):

Clicks/day ~GB/day (ClickHouse) ~GB/month Over 13 months (click TTL)
1,000,000 0.1-0.3 3-9 40-115
5,000,000 0.5-1.5 15-45 195-580
10,000,000 1-3 30-90 390-1170

On the recommended box (160 GB NVMe) at the upper traffic bound (10M/day), the disk holds several months of history, not all 13 — either shorten the click TTL (deploy/clickhouse/ttl-policy.sql), or plan a bigger disk up front, or move ClickHouse to a separate volume. tracker-ctl doctor and the InotrackDiskSpaceLow alert (deploy/prometheus/alerts.yml) warn you ahead of time, not after the fact.

Redis growth estimate

Unlike ClickHouse, Redis here is not an archive (no TTL policy, entirely in memory) — all structures on the click path and in postbacks live for a limited window/TTL, but the volume grows WITH TRAFFIC VOLUME, not only with history. Each per-click/per-conversion key takes ≈ 150-170 B (the key name + Redis overhead — hash table entry, TTL metadata, etc.):

Structure Owner TTL/window ~GB per 1M/day ~GB per 10M/day
uniq:<...> click uniqueness internal/flow/uniqueness.go the flow's window (day/campaign/forever, default day = 24h) ~0.1 ~1.1
caps:cnt:*/caps:block:* caps internal/caps until the end of the window period (day/week/month/until) negligible (per offer×window, not per click) negligible
pb:dedup:* postback dedup internal/postback (DedupTTL) fixed 30 days, does not scale with the flow's window ~5 (per 1M CONVERSIONS/day) proportional to conversion growth
click:<clickid> click cache for postback matching internal/click/clickcache.go INOTRACK_CLICK_CACHE_TTL (default 30 min, ~750 B per key) ~0.02 ~0.16

The heaviest and most often overlooked item is pb:dedup:*: the 30-day TTL is not tied to the flow's uniqueness window and does not reset within a day, so it is what sets the lower bound of Redis memory on established traffic, not the uniqueness keys.

⚠️ The campaign (90 days by default, INOTRACK_UNIQUE_CAMPAIGN_DAYS) and forever windows for click uniqueness are not recommended at 1M+ clicks/day — the set of live keys is not bounded by a day, and at high traffic they quickly add up to tens of GB. Use day unless you have a direct business reason to keep uniqueness longer.

install.sh computes REDIS_MAXMEMORY from the host's memory itself (25%, at least 1gb — see redis_maxmemory_default()) and keeps it on reinstall if it is already set in .env. Redis runs with the noeviction policy (docker-compose.yml) — when the limit is reached it does not silently evict keys, but answers OOM on writes (caps/dedup must not be lost silently); the InotrackRedisMemoryHigh alert (deploy/prometheus/alerts.yml) fires ahead of time, at 80%.

DNS: DNS-only mode, no proxy (Cloudflare "orange cloud" and the like)

The click-intake domains (TRACKER_DOMAINS) must resolve directly to this server's IP (DNS-only) — not through Cloudflare proxying or a similar CDN/WAF in front of the tracker. There are two independent reasons, and both fail silently:

  1. Caddy gets the TLS certificate through Let's Encrypt's HTTP-01/TLS-ALPN challenge — the challenge has to reach Caddy itself; a proxy in front of it either breaks certificate issuance or requires separate edge-TLS setup on the proxy side (outside this installer's scope). install.sh does not solve this yet — if you still need Cloudflare's proxy mode for tracker domains, you will have to set up certificate issuance separately (DNS-01, your own certificate), as is done for the geo hostname of edge nodes (docs/edge-geo.md, EDGE_GEO_TLS).
  2. The visitor's real IP behind a proxy (A7): install.sh fetches the current Cloudflare ranges itself and writes them to INOTRACK_CLOUDFLARE_IPS/CADDY_CLOUDFLARE_IPS (see deploy/.env.example, the "trusted proxies" section, and deploy/Caddyfile) on EVERY install/reinstall, for the central role — always, without flags. Caddy takes the client from CF-Connecting-IP only from Cloudflare's own networks (otherwise it uses the TCP connection address, as before) and passes it to the tracker in X-Real-IP. install.sh overwrites both lists, so do not add anything to them by hand — the edit is lost on the next run. If your proxy is NOT Cloudflare (another CDN/WAF), set its networks in INOTRACK_EXTRA_PROXY_IPS (comma-separated CIDRs — in .env, in the environment or in --answers-file) and rerun install.sh: it keeps this variable between runs and appends it to both lists. Such a proxy must pass the visitor in CF-Connecting-IP or append it to X-Forwarded-For — Caddy parses X-Forwarded-For right to left up to the first untrusted address, so a forged entry on the left of the chain is ignored. If Cloudflare changes its ranges, rerun install.sh over the same install — it will fetch the current list from https://www.cloudflare.com/ips/.

The admin panel domain (PANEL_DOMAIN) can be proxied the same way — but by default the recommendation is the same: DNS-only until you need otherwise. The login attempt limit (internal/auth/ratelimit.go, keyed by email and IP), INOTRACK_ADMIN_IP_ALLOWLIST and the IP in the session list are all computed from the visitor's address. On all panel routes Caddy overwrites X-Real-IP with its own {client_ip} — a client-supplied header never reaches the core; requests that go through the panel itself (sign-in, pages, server actions) the panel forwards to the core with the same X-Real-IP. The core accepts X-Real-IP only from trusted proxies (INOTRACK_TRUSTED_PROXIES, by default the private networks where Caddy and the panel container live); a direct request with a forged header gets nothing. Behind Cloudflare, {client_ip} is the address from CF-Connecting-IP, so the limit and the allowlist see the visitor, not Cloudflare (as long as the Cloudflare ranges in CADDY_CLOUDFLARE_IPS are current).

One-command install

curl -fsSL https://get.inotrack.run/install.sh -o install.sh && bash install.sh

-o install.sh && bash install.sh instead of | bash is deliberate: the curl | bash pipe swallows curl's exit code (set -o pipefail is off in your shell by default), and if get.inotrack.run is unavailable (network, a typo in the URL, a temporary failure) it silently runs bash on EMPTY input: no visible error message, and simply nothing happens. With &&, everything stops at the first curl failure, with curl's own error on screen.

Or from an already downloaded repository:

sudo ./deploy/install.sh

What the installer asks (interactive mode)

  • the license key (press Enter without a key for a trial, see below);
  • whether to use free inotrack domains — if no domains were passed via flags;
  • the admin panel domain;
  • tracker domain(s), comma-separated (one is fine, add the rest later with tracker-ctl domain add);
  • the email for Let's Encrypt;
  • the instance role — central (everything on one server, the default) or edge;
  • the report timezone (default Europe/Moscow);
  • the base currency (default USD);
  • the first administrator's email.

At the end of the install a one-time summary with the first administrator's login and password is printed — save it right away: the password is not shown again (only the hash is stored in .env).

Unattended install (mass deployments)

sudo ./deploy/install.sh --unattended \
  --panel-domain panel.example.com --tracker-domains track1.example.com,track2.example.com \
  --acme-email [email protected] --license-key XXXXX-XXXXX-XXXXX \
  --admin-email [email protected]

Or with answers from a file: --unattended --answers-file answers.env (the format is a regular .env, KEY=VALUE line by line, see deploy/.env.example).

The full list of flags: ./deploy/install.sh --help.

How to get a license key

You buy the key yourself, with no back-and-forth with a manager, at https://inotrack.run/en/buy/ — choose the plan, the term (1, 3 or 12 months) and the email the license will be issued to. Payment is in USDT on the TRON network (TRC20); cards are not accepted yet.

  1. After you click "Buy", the order page opens: the payment address (each order has its own, never reused), a QR code and the exact USDT amount. You have 60 minutes to pay.
  2. Send exactly that amount to that address. The page updates itself: "payment found" → waiting for 19 network confirmations (usually about a minute) → "payment received".
  3. The key is shown on the order page only once — copy it right away. A copy is sent by email from [email protected], and you can always view the key in your account.
  • Less than the full amount arrived — the order waits for a top-up to the same address for 24 hours (the remaining amount is shown on the order page).
  • The payment window expired, but the full amount was already sent — the payment will still be credited: late payments are accepted for 7 days. If you have not paid yet, place a new order and do not send to the old address.
  • You lost the order link — the key will still come by email after payment.

Your account is at https://inotrack.run/en/account/: sign-in is passwordless, through a link sent by email (valid for 15 minutes, single-use). The account shows your licenses and keys and handles renewal: you can renew only from there, and no new key is issued — the current license term is extended by the paid period (from the end date, or, if it has already passed, from the payment date). Reminders arrive by email 7, 3 and 1 day before expiry.

To try it without paying, there is the 7-day trial: at https://inotrack.run/en/trial/ or right from the installer (below).

7-day trial

No key? At the key prompt press Enter and enter an email (or pass --trial-email [email protected]). The key arrives by email from [email protected] (check your spam folder); enter it and the install continues as usual. One trial per email and one per server; the server's response is the same even if a trial for this address already exists; in that case no email is sent, so look for the key in the earlier email or in your account at inotrack.run.

With --unattended there is nobody to enter the key: the installer requests a trial and stops with a hint: rerun the command with --license-key <key from the e-mail> instead of --trial-email.

Free inotrack domains

No domain of your own? Answer "y" to the free domains prompt (or pass --free-domains). Using the license key, the installer gets two domains from the license server, of the form abcd2345.lnkhop.online (zones lnkhop.online, redirly.online, nxtclk.online, gohopr.site, clkpath.site) — one for the panel and one for click intake; the license server itself points the A record at the IP the request came from, that is, at this server. The installer then waits for the domains to appear in DNS (up to 5 minutes), and the rest goes as usual: license activation, TLS from Let's Encrypt (HTTP-01).

sudo ./deploy/install.sh --unattended --free-domains \
  --license-key XXXXX-XXXXX-XXXXX --acme-email [email protected]
  • The domains are claimed before license activation: they are part of its fingerprint.
  • A free domain is issued only in place of an unset one: --panel-domain/--tracker-domains (and the tracker domain pool from a previous .env on a rerun) take priority. You can mix: your own panel domain + a free tracker domain.
  • The server itself needs a public IPv4: behind NAT or with IPv6 only, the license server will refuse (ip_not_allowed) — specify your own domains.
  • A rerun returns the same domains. The number of domains is limited by the plan (limit_exceeded); for a revoked license, or one expired more than 30 days ago, the domains are deleted.
  • Only for the central role; incompatible with --skip-license.

For an already installed instance, the same is done by tracker-ctl domain claim [--purpose clicks|panel] (clicks by default): clicks adds a domain to the tracker domain pool, panel replaces the panel domain (PANEL_DOMAIN and INOTRACK_PANEL_URL, the caddy and admin containers are recreated). Domains are part of the license fingerprint, so the command warns about it and immediately runs tracker-ctl license refresh; if the fingerprint changed, the instance is activated again. An instance_limit_exceeded response at this step means the server's previous activation still occupies a license slot — reset the activations through support and repeat tracker-ctl license refresh.

Deploying an edge node

sudo ./deploy/install.sh --role edge \
  --central https://panel.central.example.com --central-token <central-instance-token> \
  --panel-domain edge-eu.example.com --acme-email [email protected] --license-key XXXXX

⚠️ A candid note on edge mode today: the flag is accepted and the edge compose profile comes up (its own Redis + Caddy without Postgres/ClickHouse), but the core does not yet have actual replication of clicks from the edge node to the central ClickHouse — only the declared INOTRACK_CENTRAL_URL/INOTRACK_CENTRAL_TOKEN variables. In practice the edge node will accumulate clicks in a local WAL file on disk until the free space runs out, staying "green" on the healthcheck the whole time. Do not deploy --role edge in production until this is fixed — for all current customers only --role central applies (one server, the core,panel,observability profile).

What happens during the install

  1. Pre-checks: OS, CPU/RAM/disk, ports 80/443, docker. The wizard: the key (or a trial), free domains (if chosen) and waiting for them to appear in DNS, the A record of the domains.
  2. Docker installation, if it is missing.
  3. Secret generation (.env, mode 600): Postgres/ClickHouse/Redis passwords, the JWT secret, the first administrator's password.
  4. License activation on the license server (key check; with an invalid key the install stops unless --skip-license is passed explicitly).
  5. docker compose up, Postgres and ClickHouse migrations, creating the first user with the admin role.
  6. The final self-check and printing the summary.

How to check that everything is up

tracker-ctl status    # docker compose ps — all containers must be healthy/running
tracker-ctl doctor     # domain DNS/TLS, link to the license-server, PG/CH/Redis,
                        # click buffer lag, postback queue, free space

doctor is the first thing to run when anything goes wrong: it prints a specific list of what is wrong instead of generic logs.

A quick manual check of click intake (once the system has at least one flow — see the quickstart):

curl -sI "https://<tracker-domain>/go/<token>"
# expect 302 Location: <offer URL> or 204 (fallback not configured)

Common problems

  • The A record is not ready — the installer stops at the pre-check. Either wait for DNS to propagate, or pass --skip-dns-check (relevant if the domain is behind Cloudflare/another proxy and its A record does not point directly at the server).
  • Port 80/443 is taken — the server already runs its own nginx/Apache/another Caddy. Caddy will not be able to get a TLS certificate. Free the ports or split the panel/click intake onto another server (the core/panel profiles, see deploy/README.md).
  • The license key fails activation — the install stops at step 4. Check that the key is not expired/revoked and that the server can reach INOTRACK_LICENSE_URL (by default https://lic.inotrack.run) — outbound HTTPS must be allowed by the server's firewall.
  • docker compose ps shows unhealthy for tracker-core/workers — they wait for postgres/clickhouse to be service_healthy; on a slow disk the first start of the databases can take longer than the healthcheck timeout. Wait and rerun tracker-ctl status; if it persists, run tracker-ctl logs postgres clickhouse.
  • --seed-demo does nothing — it is a stub: the demo traffic generator (sandbox) was not implemented at the time the installer was written, the flag only prints a warning.
  • A duplicate/mismatch of INOTRACK_PUBLIC_URL in .env is a known point of failure: if this value points to the panel domain instead of the tracker domain, the generated tracking links will lead to the wrong place, and not a single click will arrive even though the install looks "green". After the install, compare INOTRACK_PUBLIC_URL in /opt/inotrack/.env with the actual tracker domain.

Next

First steps in a freshly installed tracker: quickstart. Instance maintenance (tracker-ctl, updates, backups): operations (in Russian).

Support