Self-hostable website analytics on Django: a straightforward collector API, dashboards, a world map, and PDF reports.
analyticsdjangodockerhandcodedpythonself-hostedsqliteviteweb-analytics
1> **Archived.** This project is no longer in use and no longer maintained. Last updated April 2026.
2
3# Analytics
4
5A self-hostable analytics service with a straightforward API to collect events
6from any source.
7
8
9## Motivation
10
11I was bored and felt like writing my own analytics service over the weekend.
12
13
14## Features
15
16- Standard website analytics collection
17- Custom metrics collection
18- UTM query collection
19- Optional anonymized location collection
20- Customizable UI
21- Date range selection and comparison
22- Public URL sharing
23- Customizable
24
25
26## Requirements
27
28You need docker + docker-compose installed for a quick production start or you
29can figure out how we install and run things via the `Dockerfile` and set it up
30yourself.
31
32If you want to install things without docker then you'll need the following
33dependencies:
34
35- python
36- uv
37- bun
38- chromium (used for server-side PDF report generation via a subprocess wrapper)
39
40You can also check the `Dockerfile` for an exact list of dependencies and adjust
41package names for your desired platform.
42
43This is a standard Django project. If you know how to run Django, or want to
44look up any Django tutorial on how to run Django, you shouldn't have a problem
45getting this project running on almost anything.
46
47
48## Running locally
49
50If you have all of the above dependencies installed you can use my Makefile to
51run and install python and node dependencies locally. Running `make` will check
52that you have the proper dependencies installed and if not it will try and
53install them for you. It will then create you a fresh database and run
54everything.
55
56
57## Checking outdated dependencies
58
59This can be done in both bun and uv with the following two commands:
60
61 uv lock --upgrade --dry-run
62 bun outdated
63
64You can then upgrade all dependencies at once with:
65
66 make update
67
68I recommend testing everything after this to make sure it's all working.
69
70
71## Optimizing images with webp
72
73My development system runs Ubuntu so I installed the official webp utils from
74Google with `apt install webp`.
75
76 cwebp -q 90 -m 6 -o output.webp input.png
77
78
79## Using docker-compose
80
81The easiest way to run this project is to run it using
82`docker-compose up --build -d` if you have `docker-compose` and `docker`
83installed. This will start the server and have you running at port 8000. The
84first time you do this make sure you run migrations with
85`docker-compose run web python manage.py migrate`. Make sure you setup the
86`.env` file before running, you can copy the sample from
87`samplefiles/env.sample` into the root of the project as `.env` and change the
88variables.
89
90
91## Default user
92
93The default user is `admin` with the password `admin`. We also create an example
94property so you can see how the analytics look and a property to collect metrics
95from ourselves.
96
97
98## User location data
99
100I'm not interested in someone's personal location but I do like to know where
101people are coming from region wise. This helps me know if I need to add
102translations to my projects or if I need to add a CDN/caching/server to a new
103region. We don't store user IPs so location data isn't retroactive.
104
105The dashboard ships with a `refresh_geoip` management command that downloads
106the [DB-IP City Lite](https://db-ip.com/db/download/ip-to-city-lite) database
107(CC-BY-4.0, no signup, MaxMind-compatible MMDB format) into `GEOIP_PATH`. It
108runs automatically on container start; if the file already exists and is less
109than 30 days old it skips the download.
110
111To keep it fresh, add a host cron entry on the server that re-runs it monthly:
112
113```cron
114# /etc/crontabs/root — refresh GeoIP on the 4th of each month
1150 3 4 * * docker exec analytics_web python manage.py refresh_geoip --force
116```
117
118The 4th gives DB-IP a few days to publish their monthly build (1st of the
119month). Failures are non-fatal — the collector silently skips GeoIP
120enrichment if the database is missing.
121
122If you'd rather use a different MMDB (MaxMind GeoLite2, IPLocate, etc.) just
123drop it at `GEOIP_PATH` (defaults to `/data/db.mmdb` in production) and
124disable the cron — any MaxMind-format database works.
125
126
127## Backups
128
129All data is stored in `/srv/data/analytics/` and your repo is stored in
130`/srv/git/analytics.git/`. You can backup both of these folders and you'll have
131a 100% backup of everything except changes you may have made to the `Caddyfile`
132and the `.env` file which should be easy enough to recreate but you can back
133those up too!
134
135
136## Server guide
137
138This quickstart requires that you have an Alpine Linux server running with a
139domain name pointed to it. I'm currently using Linode as my host since they
140support Alpine Linux nicely. If you don't want to use Linode or Alpine Linux
141you can use these instructions and just change the apk commands at the start to
142whatever Linux distro you're using.
143
144**IMPORTANT NOTE**: Change `analytics.bythewood.me` to your domain name where
145relevant in these instructions.
146
147**TIP**: During the ufw portion to enable the firewall I recommend only allowing
148your IP address or your ISP's IP address range which you can find on whois
149lookups at the top. For example, replace `192.230.176.0/20` with your IP or your
150ISP's IP range.
151
152 ufw allow from 192.230.176.0/20 proto tcp to any port 22
153
154I allow my local ISP's range because I have a DHCP lease from them and I get
155tired of logging into my server from my hosting provider's UI to update it. It's
156good enough security and much better than nothing!
157
158Server:
159
160 apk update && apk upgrade && apk add docker docker-compose caddy git iptables ip6tables ufw
161 ufw allow 22/tcp && ufw allow 80/tcp && ufw allow 443/tcp && ufw --force enable
162 echo -e "#!/bin/sh\napk upgrade --update | sed \"s/^/[\`date\`] /\" >> /var/log/apk-autoupgrade.log" > /etc/periodic/daily/apk-autoupgrade && chmod 700 /etc/periodic/daily/apk-autoupgrade
163 rc-update add docker boot && service docker start
164 mkdir -p /srv/git/analytics.git && cd /srv/git/analytics.git && git init --bare
165
166Local:
167
168 git clone [email protected]:overshard/analytics-django.git && cd analytics-django
169 git remote remove origin && git remote add origin [email protected]:/srv/git/analytics.git
170 git push --set-upstream origin master
171
172Server:
173
174 mkdir -p /srv/docker && cd /srv/docker && git clone /srv/git/analytics.git analytics && cd /srv/docker/analytics
175 cp samplefiles/Caddyfile.sample /etc/caddy/Caddyfile && sed -i 's/analytics.example.com/analytics.bythewood.me/g' /etc/caddy/Caddyfile
176 cp samplefiles/env.sample .env && sed -i 's/analytics.example.com/analytics.bythewood.me/g' .env
177 cp samplefiles/post-receive.sample /srv/git/analytics.git/hooks/post-receive
178 mkdir -p /srv/data/analytics/db && chown -R 1000:1000 /srv/data/analytics
179 docker-compose up --build --detach && docker-compose run web python3 manage.py migrate --noinput && docker-compose run web sqlite3 db.sqlite3 "PRAGMA journal_mode=WAL;" ".exit"
180 rc-update add caddy boot && service caddy start
181
182
183## Scaling
184
185I choose to use an sqlite3 database since that handles all my usecases just
186fine. My first recommendation for scaling this project would be to use a
187PostgreSQL database. If you want to get fancy then a time-series database like
188Timescale would make a lot of sense. The foundation of this project is pure
189Django so it shouldn't be hard to swap in a different database.
190
191
192## Support
193
194I won't be providing any user support for this project. I'm more than happy to
195accept good pull requests and fix bugs but I don't have the time to help people
196run or use this project. I appologize in advance for this. Maintaining
197mutliple OSS projects has taught me that I need to step back from trying to
198provide support to avoid burnout.