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 activitypulse system 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 activitypulse systemd 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 correct
  • JWT_SECRET — generated automatically; replace if you prefer your own value
  • APP_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 .env manually:

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
VariableRequiredDefaultDescription
POSTGRES_PASSWORDYesPostgreSQL password
JWT_SECRETYesJWT signing secret (auto-generated if missing, but not persistent)
HTTP_PORTNo8080Host port for HTTP (mapped to container port 8080)
JWT_EXPIRY_HOURSNo24Token expiry in hours
RUST_LOGNoinfoLog level (info, debug, warn)
APP_URLNoAuto-detectedBase 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 ports mapping to the postgres service 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

  1. On the old server — export the database:
DATABASE_URL="postgres://activitypulse:oldpass@localhost:5432/activitypulse" \
  ./scripts/export-db.sh -o activitypulse-migration.tar.bz2
  1. Copy files to the new server:
scp activitypulse-migration.tar.bz2 .env docker-compose.yml scripts/import-db.sh \
  newserver:/path/to/activitypulse/
  1. 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_URL and POSTGRES_PASSWORD in your target environment match the credentials of the target PostgreSQL instance. The import script connects using the DATABASE_URL you 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 activitypulse system 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 string
  • JWT_SECRET - Generate a secure random string
  • APP_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:

  1. Update nginx configuration:
   sudo nano /etc/nginx/sites-available/activitypulse

Change server_name _; to your domain:

   server_name example.com www.example.com;
  1. Reload nginx:
   sudo systemctl reload nginx
  1. 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.conf configuration
  • 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:

  1. Verify domain ownership via an HTTP challenge
  2. Issue a certificate signed by Let’s Encrypt
  3. 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:

  1. Stop the conflicting service and re-run the installer (Debian: sudo dpkg --configure activitypulse; tarball: sudo ./infra/deploy.sh).
  2. Run ActivityPulse on a different port — set PORT=<n> in /etc/activitypulse/activitypulse.conf (Debian package) or /opt/activitypulse/activitypulse.conf (tarball), update the upstream activitypulse_backend block in /etc/nginx/sites-available/activitypulse to 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_URL in activitypulse.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

  1. Check that files exist:
   ls -la /var/www/activitypulse/
  1. Check nginx configuration:
   sudo nginx -t
  1. 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

  1. Firewall: Only expose ports 80 and 443
   sudo ufw allow 80/tcp
   sudo ufw allow 443/tcp
   sudo ufw enable
  1. SSL/TLS: Always use HTTPS in production with Let’s Encrypt

  2. Database: Use a strong password and restrict access to localhost

  3. JWT Secret: Use a cryptographically secure random string (minimum 64 characters)

  4. 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