ActivityPulse Deployment Guide
This guide covers deploying ActivityPulse on Linux.
Architecture Overview
- Backend: Rust binary running as a systemd service on port 8080
- Frontend: Static files served by nginx
- Database: PostgreSQL (one control database + per-organization databases created at runtime)
┌─────────────────┐
│ nginx │
│ (port 80/443) │
└────────┬────────┘
│
┌──────────────┴──────────────┐
│ │
▼ ▼
┌────────────────┐ ┌──────────────────┐
│ /api/* proxy │ │ /* static files │
│ to backend │ │ SPA routing │
└───────┬────────┘ └──────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────┐
│ activitypulse │ │ /var/www/ │
│ (port 8080) │ │ activitypulse/ │
└────────┬────────┘ └──────────────────┘
│
▼
┌─────────────────┐
│ PostgreSQL │
│ (port 5432) │
└─────────────────┘
Prerequisites
Ubuntu Server Requirements
- Ubuntu 22.04 LTS or newer
- Minimum 2 GB RAM
- 20 GB disk space
- Root or sudo access
Required Software
Install the following packages:
sudo apt update
sudo apt install -y nginx postgresql postgresql-client
Installation via Debian Package
The easiest way to install ActivityPulse on Debian/Ubuntu is using the .deb package. It handles all setup automatically: dependencies, system user, database initialization, nginx configuration, and the systemd service.
Quick Install
curl -fsSL https://github.com/ActivityPulse/activitypulse-release/releases/latest/download/install-deb.sh | sudo bash
This downloads and installs the latest .deb package with all dependencies (nginx, PostgreSQL). The package post-install script handles database initialization, JWT secret generation, and service startup. The script is non-interactive and safe to inspect before running.
If ActivityPulse is already installed, the script prints the current version and upgrade instructions, then exits without making changes.
Manual Install
1. Install the Package
sudo apt install ./activitypulse_1.0.0_amd64.deb
apt will install the required dependencies (nginx, postgresql, postgresql-client) and then run the post-install script, which:
- Creates the
activitypulsesystem user - Initializes the PostgreSQL database and user
- Generates a random JWT secret
- Enables and activates the nginx site (removing the default site if present)
- Enables and starts the
activitypulsesystemd service
2. Configure the Application
Review and update the configuration file:
sudo nano /etc/activitypulse/activitypulse.conf
At minimum verify:
DATABASE_URL— set automatically during install, confirm it is correctJWT_SECRET— generated automatically; replace if you prefer your own valueAPP_URL— auto-detected from hostname; update to your actual domain if using DNS or TLS (required for SSO)
3. Start the Service
sudo systemctl restart activitypulse
sudo systemctl status activitypulse
Updating via Debian Package
To upgrade to a newer version, install the new package the same way:
sudo apt install ./activitypulse_1.1.0_amd64.deb
The post-install script will automatically run any pending database migrations and restart the service. Your existing configuration in /etc/activitypulse/activitypulse.conf is preserved.
Uninstalling the Debian Package
1. Remove the package:
sudo apt remove activitypulse
This stops the systemd service, removes the binary, frontend files, and nginx configuration. The configuration file at /etc/activitypulse/activitypulse.conf is kept in case you reinstall later.
To also remove the configuration file:
sudo apt purge activitypulse
2. (Optional) Remove the PostgreSQL database:
The package does not drop the database on removal. Because ActivityPulse creates a separate database for each organization, you must drop all of them before removing the user.
Drop the control database and all organization databases owned by activitypulse:
# Drop all databases owned by the activitypulse user
sudo -u postgres psql -tAc \
"SELECT datname FROM pg_database WHERE datdba = (SELECT oid FROM pg_roles WHERE rolname = 'activitypulse');" \
| while read -r db; do
sudo -u postgres psql -c "DROP DATABASE IF EXISTS \"$db\";"
done
# Now drop the user
sudo -u postgres psql -c "DROP USER IF EXISTS activitypulse;"
This permanently deletes all data. If you want to keep a backup, run export-db.sh first.
3. (Optional) Remove nginx:
If nginx was installed only for ActivityPulse and is not used by other applications:
sudo apt purge nginx nginx-common
sudo apt autoremove
This removes nginx, its configuration files, and any dependencies that are no longer needed.
4. (Optional) Remove PostgreSQL:
If PostgreSQL was installed only for ActivityPulse and is not used by other applications:
sudo apt purge postgresql postgresql-client
sudo apt autoremove
Warning: This removes the PostgreSQL server and all databases on the system, not just the ActivityPulse database. Only do this if no other applications depend on PostgreSQL.
Deployment via Docker
Quick Install
curl -fsSL https://github.com/ActivityPulse/activitypulse-release/releases/latest/download/install-docker.sh | sudo bash
This sets up ActivityPulse in /opt/activitypulse with auto-generated credentials. Requires Docker with the Compose plugin. The script is non-interactive and safe to inspect before running.
If ActivityPulse is already installed, the script prints update instructions and exits without making changes.
Manual Docker Setup
Docker is the easiest way to run ActivityPulse when you don’t want to manage system dependencies directly. A single docker compose command starts the application and a PostgreSQL database.
The Docker container runs as a non-root user (activitypulse) for improved security. Inside the container, nginx listens on port 8080 and the backend on port 8081 — both unprivileged ports. The HTTP_PORT variable (default 8080) controls which port is exposed on the host. To serve on port 80 or 443, place a reverse proxy (e.g. host nginx or Caddy with TLS) in front of the container.
Prerequisites
- Docker Engine 24+ and Docker Compose v2
1. Get the Docker Image
Option A — Pull from GitHub Container Registry (recommended)
docker pull ghcr.io/activitypulse/activitypulse:latest
To pin to a specific version:
docker pull ghcr.io/activitypulse/activitypulse:0.6.0
Option B — Load from a downloaded archive
If you have a activitypulse-<version>-docker.tar.gz file:
docker load -i activitypulse-0.6.0-docker.tar.gz
2. Get the Compose File
Download docker-compose.yml from the releases page or copy it directly:
curl -fsSL https://github.com/ActivityPulse/activitypulse-release/releases/latest/download/docker-compose.yml \
-o docker-compose.yml
The released docker-compose.yml is preconfigured to pull ghcr.io/activitypulse/activitypulse:latest. If you loaded the image from a downloaded archive instead, edit the image: line in docker-compose.yml to match the local tag (e.g. activitypulse:0.6.0).
3. Generate Credentials
Run the setup script once to create a .env file with randomly generated credentials:
./docker-setup.sh
This creates infra/.env with a strong random POSTGRES_PASSWORD and JWT_SECRET. The script will not overwrite an existing .env, so it is safe to re-run.
Manual setup (optional): If you prefer to set credentials yourself, create
.envmanually:POSTGRES_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=' | head -c 32) JWT_SECRET=$(openssl rand -base64 64 | tr -d '\n') # HTTP_PORT=8080 # JWT_EXPIRY_HOURS=24 # RUST_LOG=info
| Variable | Required | Default | Description |
|---|---|---|---|
POSTGRES_PASSWORD | Yes | — | PostgreSQL password |
JWT_SECRET | Yes | — | JWT signing secret (auto-generated if missing, but not persistent) |
HTTP_PORT | No | 8080 | Host port for HTTP (mapped to container port 8080) |
JWT_EXPIRY_HOURS | No | 24 | Token expiry in hours |
RUST_LOG | No | info | Log level (info, debug, warn) |
APP_URL | No | Auto-detected | Base URL of the instance (e.g. http://host:8080). Required for SSO. |
4. Start the Stack
docker compose --env-file .env up -d
ActivityPulse will be available at http://localhost:8080 (or the port set by HTTP_PORT).
On first start the application automatically initializes the database schema before accepting connections.
Viewing Logs
Run these commands from the directory containing docker-compose.yml (e.g. /opt/activitypulse):
# Follow all logs
docker compose logs -f
# Backend only
docker compose logs -f activitypulse
Updating
If using GHCR:
docker pull ghcr.io/activitypulse/activitypulse:latest
docker compose up -d
If using a downloaded archive:
docker load -i activitypulse-1.1.0-docker.tar.gz
docker compose up -d
The database and configuration in .env are preserved between updates.
Uninstalling
To completely remove the Docker deployment, stop the containers, remove them along with the data volume, and delete the install directory.
1. Stop and remove containers and volumes:
cd /opt/activitypulse
docker compose --env-file .env down -v
The -v flag removes the pgdata volume containing all database data. This permanently deletes all data. If you want to keep a backup, run export-db.sh first (see Backup below).
To stop and remove containers but keep the data volume for later use:
docker compose --env-file .env down
2. Remove the install directory:
sudo rm -rf /opt/activitypulse
3. (Optional) Remove the Docker image:
docker rmi ghcr.io/activitypulse/activitypulse:latest
docker rmi postgres:16
4. (Optional) Remove orphaned volumes:
If you ran down without -v and later decide to delete the data:
docker volume rm activitypulse_pgdata
Persistent Data
PostgreSQL data is stored in the pgdata Docker volume and survives container restarts and image updates.
The volume is managed by Docker and its location on the host filesystem is determined by the Docker storage driver. You can inspect the exact path with:
docker volume inspect activitypulse_pgdata
The Mountpoint field shows the host directory (typically under /var/lib/docker/volumes/). Do not modify files in this directory directly — use the backup and restore procedures below.
Backup (Export)
ActivityPulse uses a multi-database architecture: one control database plus a separate database for each organization. The bundled export-db.sh script handles this automatically — it discovers all organization databases, dumps each one, and packages everything into a single archive with a manifest.
DATABASE_URL="postgres://activitypulse:yourpassword@localhost:5432/activitypulse" \
./scripts/export-db.sh
This produces a file like activitypulse-export-20260320-143000.tar.bz2. To specify a custom output path:
DATABASE_URL="..." ./scripts/export-db.sh -o /backups/activitypulse-backup.tar.bz2
Docker users: The script runs on the host and connects to PostgreSQL over the network. Ensure the database port is reachable. If you have not published the PostgreSQL port, you can either:
- Temporarily add a
portsmapping to thepostgresservice and recreate the container, or - Run the export from inside the postgres container:
docker compose exec postgres \
pg_dumpall -U activitypulse > activitypulse-backup.sql
Note that pg_dumpall produces a single SQL file (not the structured archive format), which cannot be used with import-db.sh. For full export/import compatibility, use the scripts with a reachable DATABASE_URL.
Restore (Import)
The import-db.sh script restores an export archive. It reads the manifest, drops existing databases, and recreates everything from the archive.
WARNING: Import destroys all existing data in the target databases.
DATABASE_URL="postgres://activitypulse:yourpassword@localhost:5432/activitypulse" \
./scripts/import-db.sh activitypulse-export-20260320-143000.tar.bz2
The script will show a summary and ask for confirmation before proceeding. To skip the confirmation prompt (useful for scripted restores):
DATABASE_URL="..." ./scripts/import-db.sh -y export.tar.bz2
Migrating to Another Host
- On the old server — export the database:
DATABASE_URL="postgres://activitypulse:oldpass@localhost:5432/activitypulse" \
./scripts/export-db.sh -o activitypulse-migration.tar.bz2
- Copy files to the new server:
scp activitypulse-migration.tar.bz2 .env docker-compose.yml scripts/import-db.sh \
newserver:/path/to/activitypulse/
- On the new server — start the stack and import:
# Start the stack (the entrypoint initializes an empty database)
docker compose up -d
# Import the backup (replace credentials with those from the new .env)
DATABASE_URL="postgres://activitypulse:newpass@localhost:5432/activitypulse" \
./scripts/import-db.sh activitypulse-migration.tar.bz2
Migrating Between Docker and Bare-Metal
The export/import scripts work against any reachable PostgreSQL instance — Docker or bare-metal. Just point DATABASE_URL at the appropriate server.
Docker → bare-metal:
# Export from Docker PostgreSQL
DATABASE_URL="postgres://activitypulse:pass@localhost:5432/activitypulse" \
./scripts/export-db.sh -o migration.tar.bz2
# Import into bare-metal PostgreSQL
DATABASE_URL="postgres://activitypulse:pass@localhost:5432/activitypulse" \
./scripts/import-db.sh migration.tar.bz2
Bare-metal → Docker:
# Export from bare-metal PostgreSQL
DATABASE_URL="postgres://activitypulse:pass@localhost:5432/activitypulse" \
./scripts/export-db.sh -o migration.tar.bz2
# Import into Docker PostgreSQL (ensure the postgres container port is reachable)
DATABASE_URL="postgres://activitypulse:dockerpass@localhost:5432/activitypulse" \
./scripts/import-db.sh migration.tar.bz2
Note: After migrating, verify that the
DATABASE_URLandPOSTGRES_PASSWORDin your target environment match the credentials of the target PostgreSQL instance. The import script connects using theDATABASE_URLyou provide — it does not carry over credentials from the source.
Installation from a tar.gz bundle
Quick Install
curl -fsSL https://github.com/ActivityPulse/activitypulse-release/releases/latest/download/install-tarball.sh | sudo bash
This downloads the latest release tarball and performs a full bare-metal installation: installs dependencies (nginx, PostgreSQL) if missing, initializes the database with auto-generated credentials, deploys the backend binary, frontend files, nginx configuration, and systemd service. The script is non-interactive and safe to inspect before running.
If ActivityPulse is already installed, the script prints upgrade instructions and exits without making changes.
Manual Install
1. Extract Release
# Create a temporary directory for extraction
cd /tmp
# Extract the release
tar -xzf activitypulse-v1.0.0-linux-amd64.tar.gz
# Navigate to the release directory
cd activitypulse-v1.0.0
2. Database Setup
Create the PostgreSQL database and user:
sudo -u postgres psql
-- Create user (CREATEDB is required because ActivityPulse creates per-organization databases)
CREATE USER activitypulse WITH ENCRYPTED PASSWORD 'your_secure_password' CREATEDB;
-- Create database
CREATE DATABASE activitypulse OWNER activitypulse;
-- Grant privileges
GRANT ALL PRIVILEGES ON DATABASE activitypulse TO activitypulse;
-- Connect to the database
\c activitypulse
-- Enable required extensions
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
\q
Initialize the schema using the provided SQL:
PGPASSWORD=<your_secure_password> psql -h localhost -p 5432 -U activitypulse -d activitypulse -f infra/init.sql
3. Run the Deployment Script
For first-time installation:
sudo ./infra/deploy.sh
The script will:
- Create the
activitypulsesystem user - Install the backend binary to
/opt/activitypulse/ - Install frontend files to
/var/www/activitypulse/ - Set up systemd service
- Configure nginx
Important: If nginx still shows the default “Welcome to nginx!” page after deployment, disable the default site — it conflicts because both listen on port 80 with server_name _:
sudo rm /etc/nginx/sites-enabled/default
sudo systemctl reload nginx
4. Configure the Application
Edit the configuration file:
sudo nano /opt/activitypulse/activitypulse.conf
At minimum, update:
DATABASE_URL- PostgreSQL connection stringJWT_SECRET- Generate a secure random stringAPP_URL- Auto-detected from hostname; update to your actual domain if using DNS or TLS (required for SSO)
# Generate a secure JWT secret
openssl rand -base64 64 | tr -d '\n'
5. Start the Service
sudo systemctl restart activitypulse
sudo systemctl status activitypulse
6. Configure Domain (Optional)
If you have a domain name:
- Update nginx configuration:
sudo nano /etc/nginx/sites-available/activitypulse
Change server_name _; to your domain:
server_name example.com www.example.com;
- Reload nginx:
sudo systemctl reload nginx
- Set up SSL with Let’s Encrypt:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
Updating
To update to a new version, download the new release tarball, then:
1. Run Update
sudo ./infra/deploy.sh --update
The --update flag:
- Skips user/directory creation
- Preserves existing
activitypulse.confconfiguration - Only updates binaries and frontend files
2. Database Migrations
If the release includes database migrations, run them:
sudo -u postgres psql -d activitypulse -f /path/to/migration.sql
Uninstalling
1. Stop and disable the service:
sudo systemctl stop activitypulse
sudo systemctl disable activitypulse
2. Remove installed files:
# Remove the systemd unit file
sudo rm /etc/systemd/system/activitypulse.service
sudo systemctl daemon-reload
# Remove the backend binary and configuration
sudo rm -rf /opt/activitypulse
# Remove the frontend files
sudo rm -rf /var/www/activitypulse
# Remove the nginx site configuration
sudo rm /etc/nginx/sites-enabled/activitypulse
sudo rm /etc/nginx/sites-available/activitypulse
sudo systemctl reload nginx
3. (Optional) Remove the system user:
sudo userdel activitypulse
4. (Optional) Remove the PostgreSQL database:
Because ActivityPulse creates a separate database for each organization, you must drop all of them before removing the user.
# Drop all databases owned by the activitypulse user
sudo -u postgres psql -tAc \
"SELECT datname FROM pg_database WHERE datdba = (SELECT oid FROM pg_roles WHERE rolname = 'activitypulse');" \
| while read -r db; do
sudo -u postgres psql -c "DROP DATABASE IF EXISTS \"$db\";"
done
# Now drop the user
sudo -u postgres psql -c "DROP USER IF EXISTS activitypulse;"
This permanently deletes all data. If you want to keep a backup, run export-db.sh first (see Backup).
5. (Optional) Remove nginx:
If nginx was installed only for ActivityPulse and is not used by other applications:
sudo apt purge nginx nginx-common
sudo apt autoremove
6. (Optional) Remove PostgreSQL:
If PostgreSQL was installed only for ActivityPulse and is not used by other applications:
sudo apt purge postgresql postgresql-client
sudo apt autoremove
Warning: This removes the PostgreSQL server and all databases on the system, not just the ActivityPulse database. Only do this if no other applications depend on PostgreSQL.
TLS/HTTPS with Let’s Encrypt
Serving ActivityPulse over HTTPS is strongly recommended for any public-facing deployment. Certbot automates certificate issuance and renewal via Let’s Encrypt.
Prerequisites
- A domain name pointed at your server (DNS A record)
- Port 80 open (required for the HTTP-01 ACME challenge)
- ActivityPulse already running and reachable at
http://your-domain.com
1. Install Certbot
sudo apt install -y certbot python3-certbot-nginx
2. Obtain a Certificate
sudo certbot --nginx -d example.com -d www.example.com
Certbot will:
- Verify domain ownership via an HTTP challenge
- Issue a certificate signed by Let’s Encrypt
- Automatically update the nginx configuration to enable HTTPS and redirect HTTP → HTTPS
When prompted, choose Redirect to enforce HTTPS for all traffic.
3. Verify Automatic Renewal
Certbot installs a systemd timer (or cron job) that renews certificates before they expire. Test it with:
sudo certbot renew --dry-run
A successful dry-run means certificates will renew automatically.
4. Update APP_URL
After enabling HTTPS, update APP_URL in your configuration file to use https://:
# Debian package
sudo nano /etc/activitypulse/activitypulse.conf
# Tarball
sudo nano /opt/activitypulse/activitypulse.conf
Change APP_URL=http://example.com to APP_URL=https://example.com, then restart the service:
sudo systemctl restart activitypulse
This ensures invitation links and SSO redirects use the correct protocol.
5. Check Certificate Status
sudo certbot certificates
Firewall
Make sure port 443 is open in addition to port 80:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Port 80 must remain open — Certbot needs it for renewal challenges.
Troubleshooting
Certificate issuance fails: Confirm the domain resolves to your server’s public IP and that port 80 is reachable from the internet.
nginx fails to reload after renewal: Check sudo nginx -t for configuration errors, then sudo systemctl reload nginx.
Certificate expires unexpectedly: Verify the Certbot timer is active: sudo systemctl status certbot.timer.
Troubleshooting
Permission Denied to Create Database
If the logs show permission denied to create database, the PostgreSQL user is missing the CREATEDB privilege. ActivityPulse creates a separate database for each organization, so this privilege is required.
Fix:
sudo -u postgres psql -c "ALTER USER activitypulse CREATEDB;"
sudo systemctl restart activitypulse
Port Already in Use
The installer and deployment scripts pre-flight check that the backend port (default 8080) is free before starting the service. If something else is already bound to that port, the install or upgrade aborts with a message like:
[ERROR] Port 8080 is already in use:
[ERROR] LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("some-process",pid=1234,fd=3))
Identify the offending process:
sudo ss -tlnp 'sport = :8080'
Then either:
- Stop the conflicting service and re-run the installer (Debian:
sudo dpkg --configure activitypulse; tarball:sudo ./infra/deploy.sh). - Run ActivityPulse on a different port — set
PORT=<n>in/etc/activitypulse/activitypulse.conf(Debian package) or/opt/activitypulse/activitypulse.conf(tarball), update theupstream activitypulse_backendblock in/etc/nginx/sites-available/activitypulseto match, then restart:
sudo systemctl restart activitypulse
sudo systemctl reload nginx
For Docker deployments, set HTTP_PORT in .env (or pass it to install-docker.sh) — the installer’s port check uses that value.
Service Won’t Start
Check the logs:
sudo journalctl -u activitypulse -f
Common issues:
- Database connection failed: Verify
DATABASE_URLinactivitypulse.conf - Port already in use: Check if another service is using port 8080
- Permission denied: Ensure proper ownership of
/opt/activitypulse
Nginx Returns 502 Bad Gateway
The backend service isn’t running or isn’t responding:
# Check if backend is running
sudo systemctl status activitypulse
# Check if port 8080 is listening
ss -tlnp | grep 8080
# Check nginx error logs
sudo tail -f /var/log/nginx/activitypulse_error.log
Frontend Shows Blank Page
- Check that files exist:
ls -la /var/www/activitypulse/
- Check nginx configuration:
sudo nginx -t
- Check browser console for JavaScript errors
Checking Service Health
# Service status
sudo systemctl status activitypulse
# Recent logs
sudo journalctl -u activitypulse -n 50
# Test API endpoint
curl http://localhost:8080/health
Security Recommendations
- Firewall: Only expose ports 80 and 443
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
-
SSL/TLS: Always use HTTPS in production with Let’s Encrypt
-
Database: Use a strong password and restrict access to localhost
-
JWT Secret: Use a cryptographically secure random string (minimum 64 characters)
-
Updates: Regularly update Ubuntu packages
sudo apt update && sudo apt upgrade
Directory Structure Reference
/opt/activitypulse/
├── activitypulse-backend # Main binary
└── activitypulse.conf # Environment configuration
/var/www/activitypulse/
├── index.html # Frontend entry point
├── assets/ # JS/CSS bundles
└── ... # Other static files
/etc/nginx/sites-available/
└── activitypulse # Nginx configuration
/etc/systemd/system/
└── activitypulse.service # Systemd unit
Support
- Check service logs:
sudo journalctl -u activitypulse -f - Check nginx logs:
sudo tail -f /var/log/nginx/activitypulse_error.log - Check PostgreSQL logs:
sudo tail -f /var/log/postgresql/postgresql-*-main.log