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.

SettingFlagEnv VarDefault
Listen address-listenGOATCOUNTER_LISTEN*:8080
Database-dbGOATCOUNTER_DBsqlite3+./goatcounter-data/db.sqlite3
TLS mode-tlsGOATCOUNTER_TLSdisabled
GeoIP database-geodbGOATCOUNTER_GEODBnone
SMTP server-smtpGOATCOUNTER_SMTPnone
Auto-migrate DB-automigrateenabled (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

ResourceValue
RAM (idle)~25 MB
RAM (moderate traffic)~50–100 MB
CPUNegligible — ~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

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.

Comments