Self-Hosting Aptabase on a Mac Mini - A Troubleshooting Guide

I recently went looking for an analytics tool for an app I’m developing. There are plenty of options out there, but most are either too heavyweight or raise privacy concerns. After some research, Aptabase caught my eye — it’s privacy-first, uses no unique user identifiers, fully complies with GDPR/CCPA, comes with a clean and intuitive dashboard, and offers over 10 SDKs covering most major frameworks. Best of all, it supports self-hosting, so your data stays entirely under your control.

The official self-hosting repository makes it look simple — just clone, tweak a few configs, run docker compose up -d, and you’re done. But the actual deployment process had quite a few gotchas, and many others in the Issues have run into similar problems. Here’s what I learned, hoping it saves you some trouble.

Aptabase doesn’t configure SMTP out of the box. After registering an account, you won’t receive any email. However, it prints the activation link to the container logs, so you need to check them manually:

1
docker logs -f aptabase_app

Look for a link like this in the output, then open it in your browser to activate your account:

1
https://your-domain/api/_auth/continue?token=eyJhbGciOiJIUzI1NiIs...

This is just a temporary workaround — we’ll configure SMTP later for proper email delivery.

Gotcha #2: HTTPS Is Required — Otherwise Activation Loops Forever

After clicking the activation link, the page kept redirecting back to the login page in an endless loop. After debugging, I found that Aptabase requires BASE_URL to use HTTPS — otherwise cookies and redirects in the auth flow break.

My setup runs on a Mac Mini at home with no public IP, and I didn’t want to deal with certificates manually. The solution: Cloudflare Tunnel with a custom domain.

Setting Up Cloudflare Tunnel

  1. Log in to the Cloudflare Dashboard, go to Zero TrustNetworksTunnels
  2. Click Create a tunnel, choose the Cloudflared type, and give it a name (e.g., mac-mini)
  3. Follow the instructions to install and run cloudflared on your Mac Mini. On macOS, use Homebrew:
1
brew install cloudflared

Then connect the tunnel using the command shown on the page (which includes a token):

1
cloudflared service install <your-token>
  1. On the tunnel’s Public Hostname page, add a record:
    • Subdomain: stats (or whatever you prefer)
    • Domain: select a domain hosted on Cloudflare, e.g., pastepaw.com
    • Service: http://localhost:3200 (this port matches the Nginx mapping in docker-compose)

Once configured, Cloudflare handles HTTPS certificates automatically. External traffic to https://stats.pastepaw.com is securely forwarded through the tunnel to your Mac Mini.

Gotcha #3: Can’t Get Real User IPs

After deployment, I noticed the dashboard showed the same IP for every user — Cloudflare’s IP, not the actual user’s.

This is a classic Cloudflare Tunnel issue. After traffic passes through Cloudflare’s proxy, the source IP becomes Cloudflare’s server IP. The real user IP is placed in the CF-Connecting-IP HTTP header, but Aptabase (built on ASP.NET Core) doesn’t read this header by default.

The fix is to add an Nginx reverse proxy in front of Aptabase that converts CF-Connecting-IP into the standard X-Forwarded-For header, then point Cloudflare Tunnel at Nginx instead of directly at Aptabase.

Here’s the nginx.conf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
events {
worker_connections 1024;
}

http {
server {
listen 80;

location / {
proxy_pass http://aptabase:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $http_cf_connecting_ip;
proxy_set_header X-Forwarded-For $http_cf_connecting_ip;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}

With this in place, Aptabase can correctly identify users’ real IPs.

Gotcha #4: Configuring SMTP for Email Delivery

Digging through container logs for activation links isn’t sustainable. Aptabase supports SMTP configuration via environment variables. I chose Resend as the email service — it’s free to sign up and offers 3,000 emails per month, more than enough for a personal project.

Setting Up a Domain in Resend

  1. Sign up for a Resend account
  2. Go to the Domains page and click Add Domain
  3. Enter the domain you want to send emails from (e.g., mail.pastepaw.com — it doesn’t need to match the Aptabase domain)
  4. Resend will provide several DNS records to add, typically:
    • An MX record
    • An SPF (TXT) record
    • Several DKIM (TXT) records
  5. Add these records in your DNS management panel (e.g., Cloudflare)
  6. Go back to Resend, click Verify, and wait for verification to complete (usually within a few minutes)

Generating an API Key

  1. In Resend’s left menu, go to the API Keys page
  2. Click Create API Key
  3. Name it (e.g., aptabase), set permission to Sending access, and optionally restrict it to the domain you just configured
  4. Copy the generated key (starts with re_) — this is your SMTP password

Configuring Environment Variables

Add these environment variables to the Aptabase service in your docker-compose.yml:

1
2
3
4
5
SMTP_HOST: smtp.resend.com
SMTP_PORT: 587
SMTP_USERNAME: resend
SMTP_PASSWORD: re_your_API_key
SMTP_FROM_ADDRESS: [email protected]

Note: Use port 587 (STARTTLS), not 465 (Implicit TLS). In my testing, port 465 failed to send emails in a Docker environment, while 587 worked immediately. This is another easy pitfall.

Complete Docker Compose Configuration

Here’s the final, working docker-compose.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
services:
nginx:
container_name: aptabase_nginx
image: nginx:alpine
restart: always
depends_on:
- aptabase
ports:
- 3200:80
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
mem_limit: 256m
cpus: 0.5
logging:
driver: json-file
options:
max-size: 10m
max-file: "3"

aptabase_db:
container_name: aptabase_db
image: postgres:15-alpine
restart: always
mem_limit: 2g
cpus: 2
volumes:
- ./db-data:/var/lib/postgresql/data
environment:
POSTGRES_USER: aptabase
POSTGRES_PASSWORD: ${PASSWORD}
logging:
driver: json-file
options:
max-size: 10m
max-file: "3"

aptabase_events_db:
container_name: aptabase_events_db
image: clickhouse/clickhouse-server:23.8.4.69-alpine
restart: always
mem_limit: 4g
cpus: 2
volumes:
- ./events-db-data:/var/lib/clickhouse
environment:
CLICKHOUSE_USER: aptabase
CLICKHOUSE_PASSWORD: ${PASSWORD}
ulimits:
nofile:
soft: 262144
hard: 262144
logging:
driver: json-file
options:
max-size: 10m
max-file: "3"

aptabase:
container_name: aptabase_app
image: ghcr.io/aptabase/aptabase:main
restart: always
depends_on:
- aptabase_events_db
- aptabase_db
mem_limit: 2g
cpus: 2
environment:
BASE_URL: https://stats.pastepaw.com
AUTH_SECRET: replace_with_your_random_secret
DATABASE_URL: Server=aptabase_db;Port=5432;User Id=aptabase;Password=${PASSWORD};Database=aptabase
CLICKHOUSE_URL: Host=aptabase_events_db;Port=8123;Username=aptabase;Password=${PASSWORD}
SMTP_HOST: smtp.resend.com
SMTP_PORT: 587
SMTP_USERNAME: resend
SMTP_PASSWORD: replace_with_your_Resend_API_key
SMTP_FROM_ADDRESS: [email protected]
logging:
driver: json-file
options:
max-size: 10m
max-file: "3"

Create a .env file for the database password:

1
echo "PASSWORD=your_strong_database_password" > .env

Reminder: Make sure to replace AUTH_SECRET with your own random string — you can generate one at RandomKeygen. Don’t use the example value from the official docs.

Starting the Services

1
docker compose up -d

Once all containers are up, visit https://stats.pastepaw.com, register an account, and this time you should receive the activation email properly.

Architecture Overview

Here’s the overall architecture with all components deployed:

Architecture Overview

Component responsibilities:

  • Cloudflare: Manages HTTPS certificates and CDN acceleration, securely forwards external traffic to the Mac Mini at home via Tunnel
  • Nginx: Reverse proxy whose core job is converting Cloudflare’s CF-Connecting-IP header into X-Forwarded-For so Aptabase can identify real user IPs
  • Aptabase: The core application service — processes events reported by SDKs, manages user accounts, and provides the dashboard
  • PostgreSQL: Stores user accounts, app configurations, API keys, and other relational data
  • ClickHouse: High-performance OLAP engine that stores all reported event data and powers the dashboard’s real-time analytics
  • Resend: External SMTP email service for sending account activation emails, etc.

Event Data Flow

When an SDK in your app reports an event, the data follows this path:

Event Data Flow

In short: every event sent by the SDK passes through Cloudflare for TLS termination and IP tagging, Nginx for header conversion, and Aptabase for validation and geo-resolution, before being written into ClickHouse in a structured format. All the charts and metrics you see on the dashboard are queried from ClickHouse in real time.

Conclusion

Aptabase itself is an excellent lightweight analytics tool, but the self-hosting documentation is fairly minimal, and there are several things you need to figure out on your own during deployment. Here’s a summary of the key gotchas:

  1. No emails by default — activation links are in the container logs, check with docker logs -f
  2. HTTPS is required — otherwise the auth flow loops endlessly; Cloudflare Tunnel is the recommended solution
  3. Real IP resolution — after Cloudflare Tunnel proxying, you need an Nginx layer to convert headers
  4. SMTP port — use 587 (STARTTLS), not 465; the latter may not work in Docker environments
  5. Security configuration — always replace the default AUTH_SECRET and keep your API keys and database passwords safe

I hope this article helps anyone else looking to self-host Aptabase and saves you from some of these pitfalls.