All posts

How to Self-Host Web Analytics with Docker in 15 Minutes

June 24, 20264 min read

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:

ServiceURL
Dashboardhttp://localhost:4173
APIhttp://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:

ServiceRoleNotes
postgresSource of truth for all writesTracking events, users, sites, teams
analytics-apiExpress APIIngests events; serves dashboard reads from DuckDB
dashboard-webReact dashboardStatic build served over HTTP
demo-siteExample siteCarries 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/track is blocked by CORS, your CORS_ORIGINS does 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_limit exceeding 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