Self-Hosting
Run Kitbase on your own server in minutes with our interactive setup script.
Prerequisites
- A server running Ubuntu (the setup script is only tested on Ubuntu)
- Git installed (
sudo apt install gitif not already present) - At least 4 GB of RAM
- At least 20 GB of storage
Quick Start
Run the setup script
git clone https://github.com/scr2em/kitbase-sdk.git
cd kitbase-sdk/self-host
sudo bash setup.shWhy sudo?
The setup script installs Docker and Docker Compose if they're not already present, which requires root privileges. Running with sudo ensures the script can install dependencies and manage Docker without permission errors.
The script will check and install dependencies (Docker, Docker Compose), walk you through the configuration, generate your .env file, and start all services.
Open Kitbase
Once the script finishes, open your configured domain in the browser.
Architecture
Everything runs behind Caddy, which handles SSL automatically:
Configuration
The setup script walks you through each step below. All values are saved to a .env file that you can edit later.
Domain
Enter your domain name — the URL where you'll access Kitbase.
| Example input | Result |
|---|---|
| (press Enter) | http://localhost (default, for local testing) |
kitbase.example.com | https://kitbase.example.com (HTTPS auto-detected for real domains) |
https://kitbase.example.com | https://kitbase.example.com |
192.168.1.100 | http://192.168.1.100 (HTTP auto-detected for IPs) |
The script extracts the protocol and hostname automatically. For real domains, Caddy provisions an SSL certificate from Let's Encrypt.
SSL terminated by a load balancer
If you entered an https:// domain, the script asks whether SSL is handled by an upstream load balancer (e.g., AWS ALB, Cloudflare). If yes, Caddy listens on HTTP only and does not manage certificates — this avoids redirect loops.
Security
The script generates a random JWT secret for signing auth tokens. You can accept the generated value or provide your own.
| Variable | Description |
|---|---|
JWT_SECRET | Required. Secret key for JWT tokens. Generate one using the command below. |
To generate a JWT secret, open a separate terminal window and run:
openssl rand -base64 32Copy the output and paste it as the value of JWT_SECRET.
You can customize token lifetimes by adding these variables to your .env file after setup:
| Variable | Default | Description |
|---|---|---|
JWT_EXPIRATION | 3600000 | Access token lifetime in milliseconds (default: 1 hour) |
JWT_REFRESH_EXPIRATION | 604800000 | Refresh token lifetime in milliseconds (default: 7 days) |
For example, to set a 30-minute access token and a 24-hour refresh token:
JWT_EXPIRATION=1800000
JWT_REFRESH_EXPIRATION=86400000Database
Set the MySQL root password. This is used by both the MySQL container and the backend.
| Variable | Description |
|---|---|
DATABASE_PASSWORD | Required. MySQL root password |
Email
Required for password resets, team invitations, and notifications. You can skip this during setup and configure it later in .env.
The script lets you choose between three providers:
| Provider | Best for |
|---|---|
| SMTP | Any SMTP service (Mailgun, SendGrid, Postmark, your own mail server) |
| AWS SES | Teams already on AWS who want to use IAM credentials instead of SMTP |
| Resend | Simple API-key setup with no infrastructure to manage |
SMTP
MAIL_PROVIDER=smtp
MAIL_FROM=noreply@yourdomain.com
SMTP_HOST=smtp.mailgun.org
SMTP_PORT=587
SMTP_USERNAME=your-username
SMTP_PASSWORD=your-passwordWorks with any SMTP server. Default port is 587 with STARTTLS.
| Variable | Default | Description |
|---|---|---|
SMTP_HOST | — | SMTP server hostname |
SMTP_PORT | 587 | SMTP server port |
SMTP_USERNAME | — | SMTP username |
SMTP_PASSWORD | — | SMTP password |
SMTP_AUTH | true | Enable SMTP authentication |
SMTP_STARTTLS | true | Enable STARTTLS |
SMTP_STARTTLS_REQUIRED | true | Require STARTTLS |
SMTP_CONNECTION_TIMEOUT | 5000 | Connection timeout in ms |
SMTP_TIMEOUT | 5000 | Read timeout in ms |
SMTP_WRITE_TIMEOUT | 5000 | Write timeout in ms |
AWS SES
MAIL_PROVIDER=ses
MAIL_FROM=noreply@yourdomain.com
SES_ACCESS_KEY=AKIA...
SES_SECRET_KEY=...
SES_REGION=us-east-1Prerequisites
- The
MAIL_FROMaddress (or its domain) must be verified in SES. - If your SES account is still in the sandbox, recipient addresses must also be verified. Request production access to lift this restriction.
- The IAM user needs the
ses:SendEmailpermission.
Minimal IAM policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "ses:SendEmail",
"Resource": "*"
}
]
}| Variable | Default | Description |
|---|---|---|
SES_ACCESS_KEY | — | AWS access key ID |
SES_SECRET_KEY | — | AWS secret access key |
SES_REGION | us-east-1 | AWS region for SES |
SES_ENDPOINT | — | Custom SES endpoint (optional, for testing with LocalStack) |
Resend
MAIL_PROVIDER=resend
MAIL_FROM=noreply@yourdomain.com
RESEND_API_KEY=re_xxxxxxxxxPrerequisites
- Create an API key at resend.com/api-keys.
- The domain in
MAIL_FROMmust be verified in Resend. For testing, useonboarding@resend.dev.
| Variable | Default | Description |
|---|---|---|
RESEND_API_KEY | — | Resend API key (starts with re_) |
Common email variables
| Variable | Default | Description |
|---|---|---|
MAIL_PROVIDER | smtp | Email provider: smtp, ses, or resend |
MAIL_FROM | noreply@localhost | Sender email address |
MAIL_INVITATION_SUBJECT | You've been invited to join an organization | Invitation email subject line |
MAIL_BASE_URL | http://localhost | Base URL used in email links |
Switching providers
To switch from one provider to another:
- Edit your
.envand changeMAIL_PROVIDERto the new provider (smtp,ses, orresend). - Add the required variables for the new provider.
- You can leave the old provider's variables in place — they will be ignored.
- Restart the backend:
docker compose up -d backendVerifying email delivery
After configuring your provider, test by inviting a team member or resetting a password. Check the backend logs for confirmation:
docker compose logs -f backend | grep -i emailYou should see lines like:
Invitation email sent successfully to: user@example.comIf emails are failing, the logs will show the error from the provider (e.g., invalid credentials, unverified sender address).
Social Login (OAuth)
Allow users to sign in with Google or GitHub. This is optional and can be configured later in .env.
To set this up, you'll need to create OAuth apps:
Google
| Variable | Default | Description |
|---|---|---|
OAUTH_GOOGLE_CLIENT_ID | — | Google OAuth client ID |
OAUTH_GOOGLE_CLIENT_SECRET | — | Google OAuth client secret |
OAUTH_GOOGLE_REDIRECT_URI | http://localhost/api/auth/oauth/google/callback | OAuth callback URL |
OAUTH_GOOGLE_SCOPES | openid email profile | OAuth scopes to request |
GitHub
| Variable | Default | Description |
|---|---|---|
OAUTH_GITHUB_CLIENT_ID | — | GitHub OAuth client ID |
OAUTH_GITHUB_CLIENT_SECRET | — | GitHub OAuth client secret |
OAUTH_GITHUB_REDIRECT_URI | http://localhost/api/auth/oauth/github/callback | OAuth callback URL |
OAUTH_GITHUB_SCOPES | user:email | OAuth scopes to request |
TIP
If you're using a custom domain, update the redirect URIs to match:
OAUTH_GOOGLE_REDIRECT_URI=https://kitbase.example.com/api/auth/oauth/google/callback
OAUTH_GITHUB_REDIRECT_URI=https://kitbase.example.com/api/auth/oauth/github/callbackFile Storage
Kitbase uses file storage for OTA update files (app builds you push to your users). By default, files are stored locally on disk — no setup needed.
For production deployments, you can use any S3-compatible storage provider.
Supported providers
| Provider | S3_ENDPOINT | Notes |
|---|---|---|
| Local disk | (default, no setup needed) | Files stored on the server |
| AWS S3 | (leave empty) | No endpoint needed |
| Cloudflare R2 | https://<account_id>.r2.cloudflarestorage.com | No egress fees |
| MinIO | http://minio:9000 | Self-hosted object storage |
| DigitalOcean Spaces | https://<region>.digitaloceanspaces.com | — |
| Backblaze B2 | https://s3.<region>.backblazeb2.com | — |
| Google Cloud Storage | https://storage.googleapis.com | Via S3-compatible XML API |
S3 configuration
Add these variables to your .env file:
# Required — enables S3 storage (disables local storage)
S3_ACCESS_KEY=your-access-key
S3_SECRET_KEY=your-secret-key
S3_BUCKET_NAME=my-bucket
S3_REGION=us-east-1
# Required for non-AWS providers — the S3-compatible API endpoint
S3_ENDPOINT=https://abc123.r2.cloudflarestorage.com
# Optional — custom public URL for serving files
# Use this if your files are served from a different domain (e.g., a CDN or custom domain)
S3_PUBLIC_URL=https://files.example.comTIP
Setting S3_ACCESS_KEY is what switches Kitbase from local storage to S3. If it's empty, files are stored locally.
| Variable | Default | Description |
|---|---|---|
STORAGE_LOCAL_ROOT_PATH | ./storage | Local file storage directory |
STORAGE_LOCAL_BASE_URL | http://localhost/api/storage | Public URL for local storage |
S3_BUCKET_NAME | — | S3 bucket name (enables S3 storage) |
S3_REGION | us-east-1 | AWS region (use auto for Cloudflare R2) |
S3_ACCESS_KEY | — | S3 access key (setting this enables S3 storage) |
S3_SECRET_KEY | — | S3 secret key |
S3_ENDPOINT | — | Custom S3-compatible API endpoint. Required for non-AWS providers |
S3_PUBLIC_URL | — | Custom public URL prefix for stored files. Overrides the default AWS URL format |
Provider setup guides
Cloudflare R2
- In the Cloudflare dashboard, go to R2 Object Storage and create a bucket.
- Go to R2 → Manage R2 API Tokens → Create API token.
- Copy the Access Key ID, Secret Access Key, and your Account ID (shown in the R2 dashboard URL).
S3_ACCESS_KEY=your-r2-access-key
S3_SECRET_KEY=your-r2-secret-key
S3_BUCKET_NAME=my-bucket
S3_REGION=auto
S3_ENDPOINT=https://<account_id>.r2.cloudflarestorage.comTo serve files from a custom domain, enable Public Access on the bucket and set:
S3_PUBLIC_URL=https://files.example.comMinIO
If you're already running MinIO (or want to add it to your Docker Compose setup):
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin
S3_BUCKET_NAME=kitbase
S3_REGION=us-east-1
S3_ENDPOINT=http://minio:9000
S3_PUBLIC_URL=http://your-server:9000/kitbaseDigitalOcean Spaces
- In the DigitalOcean dashboard, create a Space.
- Go to API → Spaces Keys → Generate New Key.
S3_ACCESS_KEY=your-spaces-key
S3_SECRET_KEY=your-spaces-secret
S3_BUCKET_NAME=my-space
S3_REGION=nyc3
S3_ENDPOINT=https://nyc3.digitaloceanspaces.com
S3_PUBLIC_URL=https://my-space.nyc3.digitaloceanspaces.comGoogle Cloud Storage
GCS offers an S3-compatible XML API using HMAC keys:
- In the Google Cloud Console, go to Cloud Storage → Settings → Interoperability.
- Create an HMAC key for a service account.
S3_ACCESS_KEY=GOOG1E...
S3_SECRET_KEY=your-hmac-secret
S3_BUCKET_NAME=my-gcs-bucket
S3_REGION=auto
S3_ENDPOINT=https://storage.googleapis.com
S3_PUBLIC_URL=https://storage.googleapis.com/my-gcs-bucketHow S3 storage works
When S3_ACCESS_KEY is set, Kitbase uses the AWS S3 SDK with the configured endpoint. The SDK speaks the same S3 protocol regardless of the provider — only the endpoint URL changes.
S3_ENDPOINT— tells the SDK where to send API requests (uploads, downloads, deletions). Leave empty for AWS S3.S3_PUBLIC_URL— controls the URL returned when referencing stored files. If not set, defaults to the standard AWS S3 URL format (https://<bucket>.s3.<region>.amazonaws.com/<path>).- Presigned URLs — generated using the configured endpoint, so they work correctly with any provider.
Billing
Self-hosted instances have unlimited usage with no plan restrictions.
Changing Configuration
The setup script generates two files: .env (environment variables) and Caddyfile (reverse proxy). To change anything after the initial setup, edit these files directly and restart:
# Edit configuration
nano .env
# Apply changes
docker compose up -dIf you changed the Caddyfile, restart Caddy specifically:
docker compose restart caddyTIP
You can also re-run sudo bash setup.sh to go through the interactive setup again. This will overwrite your .env and Caddyfile with the new values.
Custom Domain
Point your domain's DNS to your server's IP, then re-run the setup script with https://yourdomain.com as the domain. Caddy will automatically provision an SSL certificate from Let's Encrypt.
Alternatively, update the files manually:
- Update your
.env:
APP_DOMAIN=kitbase.example.com
APP_PROTOCOL=https
MAIL_BASE_URL=https://kitbase.example.com- Update your
Caddyfile:
kitbase.example.com {
encode gzip
handle /api/* {
reverse_proxy backend:8100
}
handle {
reverse_proxy dashboard:80
}
}- If using OAuth, update the redirect URIs in
.env:
OAUTH_GOOGLE_REDIRECT_URI=https://kitbase.example.com/api/auth/oauth/google/callback
OAUTH_GITHUB_REDIRECT_URI=https://kitbase.example.com/api/auth/oauth/github/callback- Restart:
docker compose restart caddyTIP
Make sure ports 80 and 443 are open in your firewall / security group. Caddy needs port 80 for the ACME challenge and HTTP → HTTPS redirect, and port 443 for HTTPS.
External Databases
By default, the Docker Compose setup includes MySQL, ClickHouse, and Redis containers. If you prefer to use managed or external instances (e.g., Amazon RDS, ClickHouse Cloud, ElastiCache), you can point Kitbase to them instead.
External MySQL
- Add the connection details to your
.env:
DATABASE_URL=jdbc:mysql://your-mysql-host:3306/flyway_db?createDatabaseIfNotExist=true&useSSL=true
DATABASE_USERNAME=kitbase
DATABASE_PASSWORD=your-password- Remove the
mysqlservice fromdocker-compose.ymland removemysqlfrom the backend'sdepends_on.
TIP
The database must be MySQL 8.0+. Kitbase runs migrations automatically on startup, so the database can be empty.
External ClickHouse
- Add the connection details to your
.env:
CLICKHOUSE_URL=jdbc:clickhouse://your-clickhouse-host:8123/analytics
CLICKHOUSE_USERNAME=default
CLICKHOUSE_PASSWORD=your-password- Remove the
clickhouseservice fromdocker-compose.ymland removeclickhousefrom the backend'sdepends_on.
External Redis
- Add the connection details to your
.env:
REDIS_HOST=your-redis-host
REDIS_PORT=6379- Remove the
redisservice fromdocker-compose.ymland removeredisfrom the backend'sdepends_on.
WARNING
When removing a service from docker-compose.yml, make sure to also remove it from the depends_on section of the backend service, otherwise Docker Compose will fail to start.
SDK Configuration
When using the Kitbase SDK with a self-hosted instance, point baseUrl to your server's /api path:
import { init } from '@kitbase/analytics';
const kitbase = init({
sdkKey: '<YOUR_SDK_KEY>',
baseUrl: 'https://kitbase.example.com/api',
});For the tracking script, load lite.js from your own dashboard host — the dashboard ships with the script bundled, so you don't need to depend on kitbase.dev:
<script>
window.KITBASE_CONFIG = {
sdkKey: 'YOUR_SDK_KEY',
baseUrl: 'https://kitbase.example.com/api',
};
</script>
<script defer src="https://kitbase.example.com/lite.js"></script>TIP
The lite.js script is a lightweight loader served as a static asset by the dashboard container. It sends all data to the baseUrl you configure — your self-hosted server receives all the analytics data directly, with no external requests to Kitbase infrastructure.
Other Integrations
Slack
| Variable | Default | Description |
|---|---|---|
SLACK_CLIENT_ID | — | Slack app client ID |
SLACK_CLIENT_SECRET | — | Slack app client secret |
SLACK_REDIRECT_URI | http://localhost/api/integrations/slack/oauth/callback | Slack OAuth callback URL |
Error Handling
| Variable | Default | Description |
|---|---|---|
ERROR_INCLUDE_STACKTRACE | false | Include stack traces in API error responses |
ERROR_NOTIFICATION_ENABLED | false | Send email notifications on errors |
ERROR_NOTIFICATION_EMAIL | — | Email address for error notifications |
Logging
| Variable | Default | Description |
|---|---|---|
LOG_LEVEL_APP | INFO | Application log level |
LOG_LEVEL_JOOQ | INFO | Database query log level |
LOG_LEVEL_SECURITY | WARN | Security log level |
Managing Data
Backups
Back up your data by dumping the Docker volumes:
# MySQL
docker exec kitbase_mysql mysqldump -u root -p flyway_db > backup.sql
# ClickHouse
docker exec kitbase_clickhouse clickhouse-client --query "SELECT * FROM analytics.custom_events FORMAT Native" > events.backupData Volumes
| Volume | Contents |
|---|---|
mysql_data | User accounts, projects, feature flags, settings |
clickhouse_data | Analytics events, sessions, pageviews |
redis_data | Cache and rate limiting data |
Updating
When a new version of Kitbase is released, update your self-hosted instance by pulling the latest images and restarting:
cd ~/kitbase-sdk/self-host
# Pull latest changes (e.g. docker-compose.yml, setup script)
git pull
# Pull latest images
docker compose pull
# Restart with new images
docker compose up -dYour data is stored in Docker volumes and is preserved across updates. To update a specific service only:
# Update just the dashboard
docker compose pull dashboard
docker compose up -d dashboard
# Update just the backend
docker compose pull backend
docker compose up -d backendTIP
The backend may take up to a minute to start while it runs database migrations and loads resources. During this time, API requests will return 502. The dashboard will remain accessible.
Troubleshooting
Checking logs
# All services
docker compose logs -f
# Specific service
docker compose logs -f backendBackend won't start
The backend waits for MySQL, ClickHouse, and Redis to be healthy before starting. Check their health:
docker compose psReset everything
docker compose down -v
docker compose up -dWARNING
This deletes all data. Only use this if you want a fresh start.
All Environment Variables
Complete reference of all environment variables. All variables are configured in your .env file.
Required
| Variable | Description |
|---|---|
JWT_SECRET | Secret key for JWT tokens. Run openssl rand -base64 32 in a separate terminal and paste the output. |
DATABASE_PASSWORD | MySQL root password |
App Settings
| Variable | Default | Description |
|---|---|---|
PORT | 80 | Port to expose the dashboard and API |
APP_DOMAIN | localhost | Your domain name (without protocol) |
APP_PROTOCOL | http | http or https |
SUPPORT_EMAIL | support@localhost | Support contact email shown to users |
Database
| Variable | Default | Description |
|---|---|---|
DATABASE_URL | jdbc:mysql://mysql:3306/flyway_db?... | JDBC connection URL |
DATABASE_USERNAME | root | MySQL username |
DATABASE_PASSWORD | root | MySQL password |
ClickHouse
| Variable | Default | Description |
|---|---|---|
CLICKHOUSE_URL | jdbc:clickhouse://clickhouse:8123/analytics | JDBC connection URL |
CLICKHOUSE_USERNAME | default | ClickHouse username |
CLICKHOUSE_PASSWORD | clickhouse123 | ClickHouse password |
Redis
| Variable | Default | Description |
|---|---|---|
REDIS_HOST | redis | Redis hostname |
REDIS_PORT | 6379 | Redis port |