How to Self-Host Web Analytics with Docker in 15 Minutes
Why self-host your analytics?
Self-hosting means your visitors' data never leaves infrastructure you control. No third party, no data sharing, no monthly per-seat bill. With Docker, standing up a complete analytics stack takes minutes.
What you need
- Docker and Docker Compose installed
- A server or your local machine
- About 15 minutes
Step 1 — Clone the repository
git clone https://github.com/NishikantaRay/InsightTrack.git cd InsightTrack
Step 2 — Configure environment
Copy the example environment file and fill in your secrets (database password, JWT secret):
cp .env.example .env
For production, generate strong values:
openssl rand -base64 48 # use for JWT_SECRET and POSTGRES_PASSWORD
Step 3 — Start the stack
docker-compose up --build -d
That single command brings up PostgreSQL, the analytics API, the dashboard, and a demo site:
| Service | URL |
|---|---|
| Dashboard | http://localhost:4173 |
| API | http://localhost:3001 |
Step 4 — Add the tracking script
Create a site in Settings, then paste the snippet into your website's <head>:
<script src="https://your-domain/api/sites/YOUR_SITE_ID/script"></script>
The script is about 9 KB gzipped and loads asynchronously, so it does not block rendering. Pageviews, sessions, clicks, scroll depth, Web Vitals, and heatmap data start flowing immediately.
What each container does
The compose file brings up four services, and it helps to know which is which when something misbehaves:
| Service | Role | Notes |
|---|---|---|
postgres | Source of truth for all writes | Tracking events, users, sites, teams |
analytics-api | Express API | Ingests events; serves dashboard reads from DuckDB |
dashboard-web | React dashboard | Static build served over HTTP |
demo-site | Example site | Carries the tracking script so you have data immediately |
DuckDB is not a service. It is embedded inside the API process and reads a single file on a mounted volume — which is exactly why this stack is four containers and not six. See the architecture.
Persisting data across restarts
The single most common self-hosting mistake is losing data on docker-compose down. Two volumes matter:
volumes: pgdata: # PostgreSQL — the source of truth duckdata: # DuckDB file — rebuildable, but slow to rebuild
PostgreSQL's volume is the one you must never lose. The DuckDB file is derived state and can be rebuilt from Postgres by re-running the sync — but at any real data volume that takes a while, so persist it too.
Note that docker-compose down -v deletes volumes. Use plain docker-compose down unless you genuinely want a clean slate.
Backups
Derived state does not need backing up; the source of truth does.
# nightly, to off-box storage docker compose exec -T postgres pg_dump -U trafficuser analytics | gzip > backup-$(date +%F).sql.gz
Restore into a fresh stack and re-run the sync to rebuild DuckDB. Test this at least once before you need it — an untested backup is a hypothesis, not a backup.
Troubleshooting
- No data appearing. Open the browser console on the tracked site. If the request to
/api/trackis blocked by CORS, yourCORS_ORIGINSdoes not include the site's origin. - Dashboard empty but events are in Postgres. The sync has not run or is failing. Check the API logs for sync lag; it is the first metric to look at for almost any "where is my data" question — see the sync design.
- Container restarts on boot. Usually
memory_limitexceeding what the container actually has. Set DuckDB's limit to match the container, not the host. - Tracking works locally, not in production. Almost always mixed content or a missing HTTPS certificate — the script must load over the same scheme as the page.
Frequently asked questions
How long does the Docker setup actually take?
About fifteen minutes on a machine that already has Docker installed, most of which is the image build and pulling PostgreSQL. Adding the tracking script to your site and seeing the first pageview arrive is another couple of minutes.
What server size do I need?
For most sites, 1 vCPU and 1 GB of RAM is enough — the stack is PostgreSQL, a Node API, and a static dashboard. Give it 2 GB if you are running the demo seed data alongside real traffic, and watch disk rather than CPU as your event volume grows.
Do I need to expose PostgreSQL to the internet?
No, and you should not. Compose puts the database on an internal network reachable only by the API container. Only the API and dashboard ports need to be published, and the database port should stay unmapped.
How do I back this up?
pg_dump against the PostgreSQL container is the whole backup — it holds every write. The DuckDB file is derived state rebuilt by the sync, so it does not need backing up, though snapshotting it saves a resync on restore.
Can I run it behind an existing reverse proxy?
Yes. Point Nginx, Caddy, or Traefik at the published API and dashboard ports and terminate TLS there. The tracking script must be served over HTTPS from the same origin your site uses, or browsers will block it as mixed content.
Step 5 — Go to production
Put the stack behind HTTPS (Caddy or Nginx + Let's Encrypt), set your real CORS_ORIGINS and APP_BASE_URL, and mount a volume for the DuckDB file so data persists across restarts.
That is it — self-hosted, privacy-first analytics running in about 15 minutes. Next: understand how cookieless tracking works, or read the architecture behind the dashboard.
Try InsightsTrack free
Open-source, self-hosted, privacy-first analytics — explore the live demo, no install required.
Open live dashboard