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/inotrackand 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 needsREFRESHABLE 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:
- 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). - The visitor's real IP behind a proxy (A7):
install.shfetches the current Cloudflare ranges itself and writes them toINOTRACK_CLOUDFLARE_IPS/CADDY_CLOUDFLARE_IPS(seedeploy/.env.example, the "trusted proxies" section, anddeploy/Caddyfile) on EVERY install/reinstall, for the central role — always, without flags. Caddy takes the client fromCF-Connecting-IPonly from Cloudflare's own networks (otherwise it uses the TCP connection address, as before) and passes it to the tracker inX-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 inINOTRACK_EXTRA_PROXY_IPS(comma-separated CIDRs — in.env, in the environment or in--answers-file) and reruninstall.sh: it keeps this variable between runs and appends it to both lists. Such a proxy must pass the visitor inCF-Connecting-IPor append it toX-Forwarded-For— Caddy parsesX-Forwarded-Forright 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, reruninstall.shover the same install — it will fetch the current list fromhttps://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) oredge; - 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.
- 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.
- Send exactly that amount to that address. The page updates itself: "payment found" → waiting for 19 network confirmations (usually about a minute) → "payment received".
- 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.envon 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
centralrole; 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
- 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.
- Docker installation, if it is missing.
- Secret generation (
.env, mode600): Postgres/ClickHouse/Redis passwords, the JWT secret, the first administrator's password. - License activation on the license server (key check; with an invalid key the install
stops unless
--skip-licenseis passed explicitly). docker compose up, Postgres and ClickHouse migrations, creating the first user with the admin role.- 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/panelprofiles, seedeploy/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 defaulthttps://lic.inotrack.run) — outbound HTTPS must be allowed by the server's firewall. docker compose psshowsunhealthyfortracker-core/workers— they wait forpostgres/clickhouseto beservice_healthy; on a slow disk the first start of the databases can take longer than the healthcheck timeout. Wait and reruntracker-ctl status; if it persists, runtracker-ctl logs postgres clickhouse.--seed-demodoes 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_URLin.envis 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, compareINOTRACK_PUBLIC_URLin/opt/inotrack/.envwith the actual tracker domain.
Next
First steps in a freshly installed tracker: quickstart.
Instance maintenance (tracker-ctl, updates, backups): operations (in Russian).