14 KiB
Self-Hosted Supabase Installation Guide
This guide walks you through deploying Pulse with a fully self-hosted Supabase stack (PostgreSQL + GoTrue + Kong) using Docker Compose. No external Supabase project required.
Table of Contents
- System Requirements
- Prerequisites
- Install Docker
- Install Bun
- Clone the Repository
- Generate Secrets
- Configure Environment
- Build and Start
- Set Up HTTPS
- Firewall Configuration
- Claim Server Ownership
- Verify Installation
- Updating Pulse
- OAuth Setup (Optional)
- Federation Setup (Optional)
- Troubleshooting
System Requirements
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 2 cores | 4 cores |
| RAM | 2 GB | 4 GB |
| Disk | 10 GB | 20 GB+ (depends on file uploads) |
| OS | Ubuntu 22.04 LTS | Ubuntu 24.04 LTS |
| Network | Public IP | Public IP + domain name |
This deployment runs four Docker containers: PostgreSQL, GoTrue (auth), Kong (API gateway), and the Pulse application.
Prerequisites
- A server with Ubuntu 22.04+ and root/sudo access
- A domain name with an A record pointing to your server's public IP (for HTTPS)
- SSH access to your server
- Git installed (
sudo apt install gitif not already present)
Install Docker
# Update system packages
sudo apt update && sudo apt upgrade -y
# Install required packages
sudo apt install -y ca-certificates curl gnupg
# Add Docker's official GPG key
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# Add Docker repository
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install Docker Engine + Compose
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# Add your user to the docker group
sudo usermod -aG docker $USER
newgrp docker
# Verify
docker --version
docker compose version
Install Bun
Bun is needed to run the key generator script:
curl -fsSL https://bun.sh/install | bash
source ~/.bashrc
bun --version
Clone the Repository
sudo mkdir -p /opt/pulse
sudo chown $USER:$USER /opt/pulse
git clone https://github.com/plsechat/pulse-chat.git /opt/pulse
cd /opt/pulse
Generate Secrets
Generate a JWT secret and two Supabase API keys:
cd /opt/pulse
bun docker/generate-keys.ts
Output:
JWT_SECRET=<base64 string>
SUPABASE_ANON_KEY=<jwt token>
SUPABASE_SERVICE_ROLE_KEY=<jwt token>
Copy all three values for the next step.
Configure Environment
cp .env.supabase.example .env
nano .env
Fill in the values:
# PostgreSQL password — strong and URL-safe (no / = + characters)
POSTGRES_PASSWORD=ChangeMeToSomethingSecure123
# Paste the three values from generate-keys.ts
JWT_SECRET=<paste here>
SUPABASE_ANON_KEY=<paste here>
SUPABASE_SERVICE_ROLE_KEY=<paste here>
# Your public domain (must match your DNS A record)
SITE_URL=https://your-domain.com
Important:
POSTGRES_PASSWORDmust be URL-safe. Avoid/,=,+, and other special characters.SITE_URLmust start withhttps://for production.- The
JWT_SECRETgenerated bygenerate-keys.tsalready meets the 32-character minimum.
Environment Variable Reference
| Variable | Required | Default | Description |
|---|---|---|---|
POSTGRES_PASSWORD |
Yes | — | Password for PostgreSQL |
JWT_SECRET |
Yes | — | Secret key for signing JWTs (min 32 chars) |
SUPABASE_ANON_KEY |
Yes | — | Supabase public/anonymous API key |
SUPABASE_SERVICE_ROLE_KEY |
Yes | — | Supabase admin service role key |
SITE_URL |
Yes | — | Public URL (e.g., https://pulse.example.com) |
PULSE_PORT |
No | 4991 |
Host port for Pulse |
JWT_EXPIRY |
No | 3600 |
Token expiry in seconds |
PUBLIC_IP |
No | auto | Public IP for WebRTC |
GOOGLE_OAUTH_ENABLED |
No | false |
Enable Google login |
GOOGLE_OAUTH_CLIENT_ID |
No | — | Google OAuth client ID |
GOOGLE_OAUTH_SECRET |
No | — | Google OAuth client secret |
DISCORD_OAUTH_ENABLED |
No | false |
Enable Discord login |
DISCORD_OAUTH_CLIENT_ID |
No | — | Discord OAuth client ID |
DISCORD_OAUTH_SECRET |
No | — | Discord OAuth client secret |
FACEBOOK_OAUTH_ENABLED |
No | false |
Enable Facebook login |
FACEBOOK_OAUTH_CLIENT_ID |
No | — | Facebook OAuth client ID |
FACEBOOK_OAUTH_SECRET |
No | — | Facebook OAuth client secret |
TWITCH_OAUTH_ENABLED |
No | false |
Enable Twitch login |
TWITCH_OAUTH_CLIENT_ID |
No | — | Twitch OAuth client ID |
TWITCH_OAUTH_SECRET |
No | — | Twitch OAuth client secret |
ADDITIONAL_REDIRECT_URLS |
No | — | Extra OAuth callback URLs |
REGISTRATION_DISABLED |
No | false |
Block new registrations (existing users can still log in; valid invite codes bypass) |
GIPHY_API_KEY |
No | — | Giphy API key for GIF search |
Build and Start
cd /opt/pulse
docker compose -f docker-compose-supabase.yml up -d
The first launch takes a few minutes while Docker downloads the required images.
Verify containers are running
docker compose -f docker-compose-supabase.yml ps
You should see four containers, all Up:
NAME IMAGE STATUS
pulse-db supabase/postgres:15.6.1.143 Up (healthy)
pulse-auth supabase/gotrue:v2.170.0 Up
pulse-kong kong:3.4 Up
pulse pulse-pulse Up
Check logs
docker logs pulse
Test the health endpoint
curl http://localhost:4991/healthz
Should return {"status":"ok","timestamp":...}.
Set Up HTTPS
Pulse does not handle TLS itself. Use a reverse proxy for HTTPS.
Option A: Caddy (recommended)
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
Configure /etc/caddy/Caddyfile:
your-domain.com {
handle /auth/v1/* {
reverse_proxy localhost:8000
}
handle {
reverse_proxy localhost:4991
}
}
sudo systemctl restart caddy
sudo systemctl enable caddy
Caddy automatically obtains and renews Let's Encrypt certificates.
Option B: Nginx
sudo apt install nginx certbot python3-certbot-nginx
Create /etc/nginx/sites-available/pulse:
server {
server_name your-domain.com;
location /auth/v1/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://127.0.0.1:4991;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
}
}
sudo ln -s /etc/nginx/sites-available/pulse /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl restart nginx
sudo certbot --nginx -d your-domain.com
Firewall Configuration
sudo ufw allow 22/tcp # SSH
sudo ufw allow 80/tcp # HTTP
sudo ufw allow 443/tcp # HTTPS
sudo ufw allow 40000:40020/udp # WebRTC voice/video
sudo ufw allow 40000:40020/tcp # WebRTC TCP fallback
sudo ufw enable
| Port | Protocol | Purpose |
|---|---|---|
| 22 | TCP | SSH access |
| 80 | TCP | HTTP (redirects to HTTPS) |
| 443 | TCP | HTTPS (web + WebSocket) |
| 4991 | TCP | Pulse (only if no reverse proxy) |
| 40000-40020 | UDP + TCP | WebRTC media (voice/video/screen share) |
Note: Docker manipulates iptables directly and can bypass ufw rules. Ports mapped in
docker-compose-supabase.ymlmay be publicly accessible even if ufw doesn't allow them. The compose file binds Kong (port 8000) to127.0.0.1so it is only accessible from the reverse proxy — do not change this to0.0.0.0.
Claim Server Ownership
- Open
https://your-domain.comin your browser - Register a new account
- Find the ownership token in the server logs:
docker logs pulse 2>&1 | grep -i token
- In the Pulse web interface, open browser DevTools (F12)
- Go to the Console tab and run:
useToken('your_token_here') - Your account is now the server owner
The ownership token is printed once on first start. Save it somewhere secure.
Verify Installation
https://your-domain.comloads the login page- You can register and log in
- You claimed ownership with the token
- Text channels work (send/receive messages)
- File uploads work
- Voice channels work (join, speak, hear others)
docker compose -f docker-compose-supabase.yml psshows all 4 containers healthy
Updating Pulse
cd /opt/pulse
docker compose -f docker-compose-supabase.yml pull
docker compose -f docker-compose-supabase.yml up -d
This pulls the latest published image and restarts the containers. Database migrations are handled automatically on startup.
OAuth Setup (Optional)
Pulse supports OAuth login via Google, Discord, Facebook, and Twitch through GoTrue. All OAuth configuration is done through your .env file — do not edit docker-compose-supabase.yml directly.
Google OAuth
- Go to Google Cloud Console
- Create a project and go to APIs & Services > Credentials
- Create an OAuth 2.0 Client ID (Web application)
- Set the authorized redirect URI to:
https://your-domain.com/auth/v1/callback - Add to your
.env:
GOOGLE_OAUTH_ENABLED=true
GOOGLE_OAUTH_CLIENT_ID=your-client-id
GOOGLE_OAUTH_SECRET=your-client-secret
- Restart:
docker compose -f docker-compose-supabase.yml up -d
Discord OAuth
- Go to Discord Developer Portal
- Create an application, go to OAuth2, add redirect:
https://your-domain.com/auth/v1/callback - Add to your
.env:
DISCORD_OAUTH_ENABLED=true
DISCORD_OAUTH_CLIENT_ID=your-client-id
DISCORD_OAUTH_SECRET=your-client-secret
- Restart:
docker compose -f docker-compose-supabase.yml up -d
The same pattern applies for Facebook and Twitch — replace DISCORD with FACEBOOK or TWITCH in the variable names.
Federation Setup (Optional)
Federation lets multiple Pulse instances connect so users can discover and join servers across instances.
- Edit the config file:
nano data/pulse/config.ini
- Add or modify:
[federation]
enabled=true
domain=your-domain.com
- Restart:
docker compose -f docker-compose-supabase.yml restart pulse
Connect Two Instances
On Instance A: Go to Server Settings > Federation > Generate Keys > Add Instance (enter Instance B's domain).
On Instance B: Server Settings > Federation > Generate Keys > Accept Instance A's request.
Troubleshooting
Containers won't start
docker logs pulse-db
docker logs pulse-auth
docker logs pulse-kong
docker logs pulse
Common issues:
- pulse-db:
POSTGRES_PASSWORDcontains special characters. Use only alphanumeric characters. - pulse-auth: Database connection failed. Check
dbcontainer is healthy:docker compose -f docker-compose-supabase.yml ps - pulse-kong: Config error. Verify
docker/kong-supabase.ymlis valid YAML.
GoTrue auth fails
docker exec pulse curl -s http://kong:8000/auth/v1/health
The supabase_auth_admin password is synced automatically on every container start via docker/db-entrypoint.sh. If you change POSTGRES_PASSWORD in your .env, just restart:
docker compose -f docker-compose-supabase.yml up -d
The DB container will re-sync the password automatically.
WebRTC voice/video not working
- Check UDP ports are open:
sudo ufw status | grep 40000 - Check Docker port mapping:
docker port pulse - If behind NAT, forward ports 40000-40020/UDP from your router
- Check public IP detection:
docker logs pulse 2>&1 | grep -i "public ip"
Database issues
docker exec -it pulse-db psql -U postgres
\dt
SELECT pg_size_pretty(pg_database_size('postgres'));
Reset everything
cd /opt/pulse
docker compose -f docker-compose-supabase.yml down -v
rm -rf data/
docker compose -f docker-compose-supabase.yml up -d
View real-time logs
docker compose -f docker-compose-supabase.yml logs -f
docker compose -f docker-compose-supabase.yml logs -f pulse