How to Self-Host GoatCounter with Docker
The Lightest Self-Hosted Analytics You Can Run
GoatCounter tracks page views without cookies, without JavaScript frameworks, and without collecting personal data. No GDPR consent banner needed. The tracking script is under 3.5 KB, the entire application uses ~25 MB of RAM at idle, and it runs on hardware as small as a Raspberry Pi. If Plausible and Umami feel like too much for what you need, GoatCounter is the minimalist answer. goatcounter.com
Prerequisites
- A Linux server (Ubuntu 22.04+ recommended)
- Docker and Docker Compose installed (guide)
- 256 MB of free RAM (generous — GoatCounter barely uses 50 MB)
- A domain name (recommended for the tracking script to work cross-site)
Docker Compose Configuration
GoatCounter supports SQLite (simplest) or PostgreSQL (better for high-traffic sites). Here’s the SQLite setup — zero dependencies:
services:
goatcounter:
image: arp242/goatcounter:2.7
container_name: goatcounter
ports:
- "8080:8080"
volumes:
- goatcounter-data:/home/goatcounter/goatcounter-data
restart: unless-stopped
volumes:
goatcounter-data:
That’s the entire setup. GoatCounter ships as a single Go binary with SQLite compiled in. No database container, no cache layer, no reverse proxy required for basic use.
For PostgreSQL (higher traffic or multi-site):
services:
goatcounter:
image: arp242/goatcounter:2.7
container_name: goatcounter
environment:
GOATCOUNTER_DB: "postgresql+postgresql://goatcounter:change-this-password@goatcounter-db:5432/goatcounter?sslmode=disable"
ports:
- "8080:8080"
volumes:
- goatcounter-data:/home/goatcounter/goatcounter-data
depends_on:
goatcounter-db:
condition: service_healthy
restart: unless-stopped
goatcounter-db:
image: postgres:17-alpine
container_name: goatcounter-db
environment:
POSTGRES_USER: goatcounter
POSTGRES_PASSWORD: change-this-password
POSTGRES_DB: goatcounter
volumes:
- goatcounter-db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U goatcounter"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
goatcounter-data:
goatcounter-db-data:
Start the stack:
docker compose up -d
Initial Setup
Open http://your-server:8080. On first launch with no existing sites, GoatCounter presents a setup page. Create your first site by specifying the domain and admin credentials.
Alternatively, create a site from the command line:
docker compose exec goatcounter goatcounter db create site \
-vhost=stats.example.com \
[email protected] \
-user.password=change-this-password
After creating the site, add the tracking script to your website’s HTML:
<script data-goatcounter="https://stats.example.com/count"
async src="//stats.example.com/count.js"></script>
Page views start appearing immediately in the dashboard.
Configuration
GoatCounter uses CLI flags as its primary configuration. Every flag can also be set via environment variable with the GOATCOUNTER_ prefix.
| Setting | Flag | Env Var | Default |
|---|---|---|---|
| Listen address | -listen | GOATCOUNTER_LISTEN | *:8080 |
| Database | -db | GOATCOUNTER_DB | sqlite3+./goatcounter-data/db.sqlite3 |
| TLS mode | -tls | GOATCOUNTER_TLS | disabled |
| GeoIP database | -geodb | GOATCOUNTER_GEODB | none |
| SMTP server | -smtp | GOATCOUNTER_SMTP | none |
| Auto-migrate DB | -automigrate | — | enabled (default CMD) |
The Docker image runs goatcounter serve -automigrate by default, so database migrations are applied automatically on upgrades.
No Config File
GoatCounter has no config file — no YAML, no TOML, no .env. All configuration happens through CLI flags or environment variables. This is intentional: fewer moving parts, fewer things to misconfigure.
Advanced Configuration
TLS Without a Reverse Proxy
GoatCounter can handle TLS directly with automatic Let’s Encrypt certificates:
environment:
GOATCOUNTER_TLS: "tls,rdr,acme"
ports:
- "80:80" # HTTP redirect
- "443:443" # HTTPS
This requests a certificate from Let’s Encrypt automatically. The ACME challenge files and certificates are stored in the data volume.
GeoIP Support
GoatCounter v2.7+ supports automatic MaxMind GeoIP database downloads:
environment:
GOATCOUNTER_GEODB: "maxmind:YOUR_ACCOUNT_ID:YOUR_LICENSE_KEY"
This adds country-level visitor location data to your dashboard. Requires a free MaxMind account.
Multi-Site
Run multiple sites on one GoatCounter instance. Create additional sites via CLI:
docker compose exec goatcounter goatcounter db create site \
-vhost=analytics.other-site.com \
[email protected]
Caveat: Sites sharing an instance will merge paths with identical names. If site-a.com/about and site-b.com/about both exist, they appear as one /about entry. Use separate vhosts and filter by site in the dashboard.
Reverse Proxy
When running behind a reverse proxy (Nginx Proxy Manager, Caddy, Traefik), use port 8080 without TLS flags:
environment:
GOATCOUNTER_LISTEN: ":8080"
ports:
- "8080:8080"
The reverse proxy handles TLS. GoatCounter v2.7+ auto-detects Secure and SameSite cookie attributes from the connection, so no special proxy header configuration is needed. See Reverse Proxy Setup.
Backup
SQLite setup: Back up the data volume. The database file lives at /home/goatcounter/goatcounter-data/db.sqlite3:
docker compose exec goatcounter cp /home/goatcounter/goatcounter-data/db.sqlite3 /home/goatcounter/goatcounter-data/db-backup.sqlite3
# Then copy from the volume to your host
docker cp goatcounter:/home/goatcounter/goatcounter-data/db-backup.sqlite3 ./
PostgreSQL setup: Standard pg_dump:
docker compose exec goatcounter-db pg_dump -U goatcounter goatcounter > goatcounter-backup.sql
See Backup Strategy.
Troubleshooting
No page views appearing
Symptom: Tracking script is added but the dashboard stays empty.
Fix: Check browser devtools for errors loading count.js. The script URL must match the site’s vhost exactly. If using a reverse proxy, ensure WebSocket connections aren’t being blocked (v2.7 auto-detects WebSocket availability).
”Site not found” error
Symptom: GoatCounter returns an error when accessing the dashboard.
Fix: The vhost you’re accessing doesn’t match any configured site. Create a site matching your domain: goatcounter db create site -vhost=your-domain.com.
Database migration errors on upgrade
Symptom: GoatCounter fails to start after upgrading the image.
Fix: The -automigrate flag (enabled by default) should handle this. If it fails, check logs: docker compose logs goatcounter. You may need to run migrations manually: docker compose exec goatcounter goatcounter db migrate.
Resource Requirements
| Resource | Value |
|---|---|
| RAM (idle) | ~25 MB |
| RAM (moderate traffic) | ~50–100 MB |
| CPU | Negligible — ~15 min cumulative CPU per week in real deployments |
| Disk (image) | ~17 MB |
| Disk (data) | A few hundred MB over months for a moderate-traffic site |
GoatCounter runs as a non-root goatcounter user inside the container. It’s one of the lightest self-hosted analytics tools available.
Verdict
GoatCounter is the right choice when you want analytics without the complexity. No database server needed (SQLite works), no JavaScript framework, no consent banners, 25 MB RAM. It gives you what you actually need — page views, referrers, browser/OS stats, screen sizes — without the overhead of Plausible or Matomo.
The trade-off: GoatCounter’s dashboard is simpler than Plausible’s. No funnels, no custom events (basic event support exists but it’s limited), no team features. If you need those, use Plausible. If you just want to know how many people read your blog, GoatCounter is all you need.
FAQ
Does GoatCounter need a consent banner?
No. GoatCounter does not use cookies for tracking and does not collect personal data. It counts page views using a lightweight approach that doesn’t fall under GDPR’s cookie consent requirements.
How does GoatCounter compare to Plausible?
Plausible has a more polished dashboard, custom events, goals, funnels, and team features. GoatCounter is lighter (25 MB vs 500+ MB), simpler, and works with SQLite. Choose Plausible for a Google Analytics replacement; choose GoatCounter for minimal tracking on a blog or small site. See Plausible vs GoatCounter for the full comparison.
Can I import data from Google Analytics?
Not directly. GoatCounter has CSV import capabilities but there’s no automated GA-to-GoatCounter migration path. Start fresh — historical data in GA stays in GA.
Does GoatCounter work without JavaScript?
GoatCounter supports a no-JavaScript tracking pixel. Add an <img> tag instead of the script and it counts the page view. The pixel approach loses referrer and screen size data but works for users with JavaScript disabled.
Related
- GoatCounter vs Plausible: Minimalist Analytics Compared
- GoatCounter vs Umami: Which Analytics to Self-Host?
- Matomo vs GoatCounter: Full Analytics or Just Enough?
- Umami vs GoatCounter: Lightweight Analytics Face-Off
- How to Self-Host Plausible
- How to Self-Host Umami
- How to Self-Host Matomo
- Plausible vs Umami
- Plausible vs GoatCounter
- Best Self-Hosted Analytics
- Replace Google Analytics
- Docker Compose Basics
- Reverse Proxy Setup
Get self-hosting tips in your inbox
Get the Docker Compose configs, hardware picks, and setup shortcuts we don't put in articles. Weekly. No spam.
Comments