Skip to main content

Manual Installation Guide

This guide provides step-by-step instructions for manually deploying GeoPulse on bare metal servers or VMs without Docker/Kubernetes.

Prerequisites​

  • Root access (sudo)
  • Linux system (Ubuntu, Debian, RHEL, Rocky Linux, AlmaLinux, or Fedora)
  • Internet connection
  • Basic command-line knowledge
Running in Containers or as Root

If you're running in a minimal container (LXC, Docker, etc.) or already operating as root:

  • Skip sudo: If you're root, run commands without sudo prefix

    # Instead of: sudo apt-get install ...
    # Use: apt-get install ...
  • PostgreSQL commands: Replace sudo -u postgres with one of these:

    # Option 1: Switch user (then run psql commands)
    su - postgres

    # Option 2: Use runuser (from root, single command)
    runuser -u postgres -- psql -d geopulse -c "SELECT 1;"
  • Install sudo (optional): For consistency with these instructions:

    apt-get install -y sudo  # Debian/Ubuntu
    dnf install -y sudo # RHEL/Rocky/Fedora

Table of Contents​

Installation Steps:

  1. Choose Your Backend Type
  2. Determine Your CPU Architecture
  3. Install System Dependencies
  4. Create System User
  5. Create Directory Structure
  6. Configure PostgreSQL
  7. Generate Security Keys
  8. Download GeoPulse Artifacts
  9. Install Backend
  10. Install Frontend
  11. Create Configuration File
  12. Create Systemd Service
  13. Configure Nginx Web Server
  14. Start and Verify Services

Post-Installation:


1. Choose Your Backend Type​

GeoPulse offers two backend options:

  • ✅ No Java required
  • ✅ Lower memory usage (50-100 MB)
  • ✅ Faster startup (milliseconds)
  • ✅ Better performance
  • ❌ Architecture-specific binary
Best Use Cases

Production deployments, resource-constrained environments, Raspberry Pi

JVM Backend​

  • ✅ Platform-independent (works on any Java 25+ system)
  • ✅ Easier debugging
  • ❌ Requires Java 25+ runtime
  • ❌ Higher memory usage (512-1024 MB)
  • ❌ Slower startup
Best Use Cases

Development, testing, cross-platform compatibility


2. Determine Your CPU Architecture​

Before downloading, identify your CPU architecture and capabilities to select the correct binary.

Check Architecture​

uname -m

Output meanings:

  • x86_64 → AMD64 (Intel/AMD processors)
  • aarch64 or arm64 → ARM64 (ARM processors, Raspberry Pi, AWS Graviton)

Check CPU Features (AMD64 only)​

For AMD64 systems, check if your CPU supports modern instruction sets:

# Check for x86-64-v3 support (AVX2, BMI2, FMA)
grep -q avx2 /proc/cpuinfo && grep -q bmi2 /proc/cpuinfo && grep -q fma /proc/cpuinfo && echo "Modern CPU (x86-64-v3)" || echo "Older CPU (x86-64-v2)"

Result interpretation:

  • "Modern CPU (x86-64-v3)": Use geopulse-backend-native-amd64-{version} CPUs from ~2013+: Intel Haswell, AMD Zen or newer
  • "Older CPU (x86-64-v2)": Use geopulse-backend-native-amd64-compat-{version} Older Intel/AMD CPUs, VPS with older hypervisors

Check CPU Model (ARM64 only)​

For ARM64 systems, check if it's a Raspberry Pi:

cat /proc/cpuinfo | grep -i "raspberry\|bcm" && echo "Raspberry Pi - use compat build" || echo "Modern ARM64 - use optimized build"

Result interpretation:

  • "Raspberry Pi - use compat build": Use geopulse-backend-native-arm64-compat-{version} Raspberry Pi 3, 4, or similar
  • "Modern ARM64 - use optimized build": Use geopulse-backend-native-arm64-{version} AWS Graviton, Ampere Altra, Apple Silicon (if running Linux)

Summary Table​

ArchitectureCPU TypeBinary to Download
AMD64Modern (2013+)geopulse-backend-native-amd64-{version}
AMD64Older (pre-2013)geopulse-backend-native-amd64-compat-{version}
ARM64Moderngeopulse-backend-native-arm64-{version}
ARM64Raspberry Pi 3/4geopulse-backend-native-arm64-compat-{version}
AnyJVM Backendgeopulse-backend-jvm-{version}.tar.gz

3. Install System Dependencies​

Ubuntu / Debian​

# Update package list
sudo apt-get update

# Install core dependencies (including timezone data and lsb-release for repository setup)
sudo apt-get install -y curl wget tar openssl ca-certificates tzdata lsb-release

# Configure timezone (required for PostgreSQL)
sudo ln -sf /usr/share/zoneinfo/UTC /etc/localtime
sudo dpkg-reconfigure -f noninteractive tzdata

# Install PostgreSQL 18 + PostGIS
sudo apt-get install -y postgresql-18 postgresql-18-postgis-3

# Install Nginx
sudo apt-get install -y nginx

# If using JVM backend, install Java 25
# sudo apt-get install -y openjdk-25-jre-headless
PostgreSQL 18 Repository

If PostgreSQL 18 is not available in your distribution's repositories, you can use PostgreSQL's official APT repository. GeoPulse also supports PostgreSQL 17; existing installations can move to 18 later as described in Upgrading to PostgreSQL 18:

# Ensure lsb-release is installed (needed for release detection)
sudo apt-get install -y lsb-release

# Add PostgreSQL APT repository key (modern method)
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | \
sudo gpg --dearmor -o /usr/share/keyrings/postgresql-keyring.gpg

# Add PostgreSQL repository with signed-by keyring
sudo sh -c 'echo "deb [signed-by=/usr/share/keyrings/postgresql-keyring.gpg] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'

# Update package lists
sudo apt-get update

# Install PostgreSQL 18
sudo apt-get install -y postgresql-18 postgresql-18-postgis-3

RHEL / Rocky Linux / AlmaLinux​

# Install core dependencies (including timezone data)
sudo dnf install -y curl wget tar openssl ca-certificates tzdata

# Configure timezone (required for PostgreSQL)
sudo ln -sf /usr/share/zoneinfo/UTC /etc/localtime

# Install PostgreSQL 18 + PostGIS
sudo dnf install -y postgresql18-server postgresql18-contrib postgis36_18

# Install Nginx (optional)
sudo dnf install -y nginx

# If using JVM backend, install Java 25
# sudo dnf install -y java-25-openjdk-headless

Fedora​

# Install core dependencies (including timezone data)
sudo dnf install -y curl wget tar openssl ca-certificates tzdata

# Configure timezone (required for PostgreSQL)
sudo ln -sf /usr/share/zoneinfo/UTC /etc/localtime

# Install PostgreSQL + PostGIS (17 or newer)
sudo dnf install -y postgresql-server postgresql-contrib postgis

# Install Nginx (optional)
sudo dnf install -y nginx

# If using JVM backend, install Java 25
# sudo dnf install -y java-25-openjdk-headless

4. Create System User​

This user will own all GeoPulse files and run the backend service. It cannot log in interactively, making it secure.

sudo useradd -r -s /bin/false -d /opt/geopulse -c "GeoPulse Service" geopulse

5. Create Directory Structure​

# Application directory (backend, keys)
sudo mkdir -p /opt/geopulse/backend
sudo mkdir -p /opt/geopulse/keys

# Configuration directory
sudo mkdir -p /etc/geopulse

# Frontend directory (for Nginx deployment)
sudo mkdir -p /var/www/geopulse

# Data directory (dumps, backups)
sudo mkdir -p /var/lib/geopulse/dumps

# Logs directory
sudo mkdir -p /var/log/geopulse/backend
sudo mkdir -p /var/log/geopulse/nginx

# Set ownership and permissions
sudo chown -R geopulse:geopulse /opt/geopulse
sudo chown -R geopulse:geopulse /var/lib/geopulse
sudo chown -R geopulse:geopulse /var/log/geopulse

# Secure the keys directory
sudo chmod 750 /opt/geopulse/keys
Directory Structure
  • /opt/geopulse - Application installation
  • /etc/geopulse - Configuration files
  • /var/www/geopulse - Frontend static files (Nginx)
  • /var/lib/geopulse - Runtime data, backups
  • /var/log/geopulse - Application logs

6. Configure PostgreSQL​

Initialize and Start PostgreSQL​

Ubuntu/Debian:

sudo systemctl start postgresql
sudo systemctl enable postgresql

RHEL/Rocky/AlmaLinux:

# Initialize database cluster (first time only)
sudo /usr/pgsql-18/bin/postgresql-18-setup initdb

# Start and enable
sudo systemctl start postgresql-18
sudo systemctl enable postgresql-18

Create Database and User​

sudo -u postgres psql << 'EOF'
-- Create user
CREATE USER geopulse WITH PASSWORD 'your_secure_password_here';

-- Create database
CREATE DATABASE geopulse OWNER geopulse;

-- Connect to geopulse database
\c geopulse

-- Enable PostGIS extensions (must be done as superuser)
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS postgis_topology;

-- Grant privileges
GRANT ALL PRIVILEGES ON DATABASE geopulse TO geopulse;
GRANT ALL ON SCHEMA public TO geopulse;
GRANT ALL ON ALL TABLES IN SCHEMA public TO geopulse;
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO geopulse;
EOF
Important

Replace 'your_secure_password_here' with a strong, random password. Save this password - you'll need it in step 11.

Verify PostGIS Installation​

sudo -u postgres psql -d geopulse -c "SELECT PostGIS_Version();"

You should see PostGIS version information.


7. Generate Security Keys​

GeoPulse requires one cryptographic key: the encryption key that protects stored secrets (OIDC client secrets, GPS source and integration credentials, AI provider API keys, backups). Sign-in sessions are stored in the database and need no keys.

# Generate the encryption key (256-bit)
sudo bash -c 'openssl rand -base64 32 > /opt/geopulse/keys/ai-encryption-key.txt'

# Set secure permissions
sudo chmod 640 /opt/geopulse/keys/ai-encryption-key.txt
sudo chown geopulse:geopulse /opt/geopulse/keys/ai-encryption-key.txt

# Verify files were created
sudo ls -lh /opt/geopulse/keys/
Security Notice

These keys are critical. Never commit them to version control or share them. The private key should only be readable by the geopulse user.


8. Download GeoPulse Artifacts​

Version Selection

Set your desired version (check GitHub Releases for the latest version):

VERSION=1.39.0

Create a temporary download directory:

mkdir -p /tmp/geopulse-install
cd /tmp/geopulse-install

Download Frontend (Required)​

The frontend is architecture-independent:

wget https://github.com/tess1o/geopulse/releases/download/v${VERSION}/geopulse-frontend-${VERSION}.tar.gz

Download Backend (Choose ONE Option)​

Based on your CPU architecture determined in Step 2:

Option A: Native AMD64 (Modern CPUs - Intel Haswell 2013+, AMD Zen 2017+)

wget https://github.com/tess1o/geopulse/releases/download/v${VERSION}/geopulse-backend-native-amd64-${VERSION}

Option B: Native AMD64 Compatible (Older CPUs, x86-64-v2)

wget https://github.com/tess1o/geopulse/releases/download/v${VERSION}/geopulse-backend-native-amd64-compat-${VERSION}

Option C: Native ARM64 (Modern ARM64)

wget https://github.com/tess1o/geopulse/releases/download/v${VERSION}/geopulse-backend-native-arm64-${VERSION}

Option D: Native ARM64 Compatible (Raspberry Pi 3/4)

wget https://github.com/tess1o/geopulse/releases/download/v${VERSION}/geopulse-backend-native-arm64-compat-${VERSION}

Option E: JVM Backend (Any platform with Java 25+)

wget https://github.com/tess1o/geopulse/releases/download/v${VERSION}/geopulse-backend-jvm-${VERSION}.tar.gz

Download and Verify Checksums​

Security Best Practice

Always verify checksums to ensure downloaded files haven't been tampered with.

# Download checksums
wget https://github.com/tess1o/geopulse/releases/download/v${VERSION}/SHA256SUMS

# Verify checksums
sha256sum -c SHA256SUMS --ignore-missing

You should see:

geopulse-frontend-1.39.0.tar.gz: OK
geopulse-backend-native-amd64-1.39.0: OK
Checksum Verification

If verification fails, do not proceed. Re-download the files.


9. Install Backend​

For Native Backend​

Choose the command that matches your architecture from step 2:

AMD64 Modern:

# Copy binary and set permissions
sudo cp geopulse-backend-native-amd64-${VERSION} /opt/geopulse/backend/geopulse-backend
sudo chmod +x /opt/geopulse/backend/geopulse-backend
sudo chown geopulse:geopulse /opt/geopulse/backend/geopulse-backend

AMD64 Compatible:

# Copy binary and set permissions
sudo cp geopulse-backend-native-amd64-compat-${VERSION} /opt/geopulse/backend/geopulse-backend
sudo chmod +x /opt/geopulse/backend/geopulse-backend
sudo chown geopulse:geopulse /opt/geopulse/backend/geopulse-backend

ARM64 Modern:

# Copy binary and set permissions
sudo cp geopulse-backend-native-arm64-${VERSION} /opt/geopulse/backend/geopulse-backend
sudo chmod +x /opt/geopulse/backend/geopulse-backend
sudo chown geopulse:geopulse /opt/geopulse/backend/geopulse-backend

ARM64 Compatible (Raspberry Pi):

# Copy binary and set permissions
sudo cp geopulse-backend-native-arm64-compat-${VERSION} /opt/geopulse/backend/geopulse-backend
sudo chmod +x /opt/geopulse/backend/geopulse-backend
sudo chown geopulse:geopulse /opt/geopulse/backend/geopulse-backend

Verify installation:

ls -lh /opt/geopulse/backend/geopulse-backend
# Should show: -rwxr-xr-x 1 geopulse geopulse [size] [date] geopulse-backend

For JVM Backend​

# Extract quarkus-app folder
sudo tar -xzf geopulse-backend-jvm-${VERSION}.tar.gz -C /opt/geopulse/backend

# Set permissions
sudo chown -R geopulse:geopulse /opt/geopulse/backend

# Verify installation
ls -la /opt/geopulse/backend/quarkus-app/

You should see: quarkus-run.jar, lib/, app/, quarkus/


10. Install Frontend​

Extract frontend to Nginx web directory and set permissions:

Ubuntu / Debian:

# Extract frontend files
sudo tar -xzf geopulse-frontend-${VERSION}.tar.gz -C /var/www/geopulse

# Set permissions for Nginx
sudo chown -R www-data:www-data /var/www/geopulse

# Verify extraction
ls -la /var/www/geopulse/

RHEL / Rocky Linux / AlmaLinux / Fedora:

# Extract frontend files
sudo tar -xzf geopulse-frontend-${VERSION}.tar.gz -C /var/www/geopulse

# Set permissions for Nginx
sudo chown -R nginx:nginx /var/www/geopulse

# Verify extraction
ls -la /var/www/geopulse/

You should see: index.html, assets/, geopulse-logo.svg, etc.


11. Create Configuration File​

Create /etc/geopulse/geopulse.env:

sudo tee /etc/geopulse/geopulse.env > /dev/null << 'EOF'
# GeoPulse Configuration

# Recommended public base URL (used for callbacks/links)
GEOPULSE_PUBLIC_BASE_URL=http://your-server-ip

# CORS (manual install uses nginx same-origin by default)
GEOPULSE_CORS_ENABLED=false
GEOPULSE_CORS_ORIGINS=

# Legacy fallback (deprecated)
GEOPULSE_UI_URL=http://your-server-ip

# Backend port
QUARKUS_HTTP_PORT=8080

# Database connection (update password from step 6)
GEOPULSE_POSTGRES_URL=jdbc:postgresql://localhost:5432/geopulse
GEOPULSE_POSTGRES_HOST=localhost
GEOPULSE_POSTGRES_PORT=5432
GEOPULSE_POSTGRES_DB=geopulse
GEOPULSE_POSTGRES_USERNAME=geopulse
GEOPULSE_POSTGRES_PASSWORD=your_secure_password_here

# AI Encryption (must use file: URI scheme)
GEOPULSE_AI_ENCRYPTION_KEY_LOCATION=file:/opt/geopulse/keys/ai-encryption-key.txt

# Logging
QUARKUS_LOG_FILE_ENABLE=true
QUARKUS_LOG_FILE_PATH=/var/log/geopulse/backend/geopulse.log
QUARKUS_LOG_FILE_ROTATION_MAX_FILE_SIZE=10M
QUARKUS_LOG_FILE_ROTATION_MAX_BACKUP_INDEX=5

EOF
CRITICAL: Update Required Configuration Values

Before continuing, you MUST edit the configuration file and update these values:

sudo nano /etc/geopulse/geopulse.env

Required changes:

  1. GEOPULSE_PUBLIC_BASE_URL - Replace http://your-server-ip with your actual server IP or domain

    • Example: GEOPULSE_PUBLIC_BASE_URL=http://192.168.1.100
  2. GEOPULSE_POSTGRES_PASSWORD - Replace your_secure_password_here with the password you set in step 6

Set secure permissions:

sudo chmod 640 /etc/geopulse/geopulse.env
sudo chown geopulse:geopulse /etc/geopulse/geopulse.env

12. Create Systemd Service​

For Native Backend​

Create /etc/systemd/system/geopulse-backend.service:

sudo tee /etc/systemd/system/geopulse-backend.service > /dev/null << 'EOF'
[Unit]
Description=GeoPulse Backend Service (Native)
After=network.target postgresql.service
Wants=postgresql.service

[Service]
Type=simple
User=geopulse
Group=geopulse
WorkingDirectory=/opt/geopulse/backend
EnvironmentFile=/etc/geopulse/geopulse.env

# Start the native binary with GC options
ExecStart=/opt/geopulse/backend/geopulse-backend -Dquarkus.http.host=0.0.0.0 -XX:MaximumHeapSizePercent=70 -XX:MaximumYoungGenerationSizePercent=15

# Restart policy
Restart=on-failure
RestartSec=10

# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/geopulse /var/log/geopulse /opt/geopulse/keys

# Logging
StandardOutput=append:/var/log/geopulse/backend/geopulse-stdout.log
StandardError=append:/var/log/geopulse/backend/geopulse-stderr.log

[Install]
WantedBy=multi-user.target
EOF

For JVM Backend​

Create /etc/systemd/system/geopulse-backend.service:

sudo tee /etc/systemd/system/geopulse-backend.service > /dev/null << 'EOF'
[Unit]
Description=GeoPulse Backend Service (JVM)
After=network.target postgresql.service
Wants=postgresql.service

[Service]
Type=simple
User=geopulse
Group=geopulse
WorkingDirectory=/opt/geopulse/backend/quarkus-app
EnvironmentFile=/etc/geopulse/geopulse.env

# JVM options
Environment="JAVA_OPTS=-Xmx512m -Xms128m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/lib/geopulse/dumps/"

# Start the JVM
ExecStart=/usr/bin/java ${JAVA_OPTS} -jar /opt/geopulse/backend/quarkus-app/quarkus-run.jar

# Restart policy
Restart=on-failure
RestartSec=10

# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/geopulse /var/log/geopulse /opt/geopulse/keys

# Logging
StandardOutput=append:/var/log/geopulse/backend/geopulse-stdout.log
StandardError=append:/var/log/geopulse/backend/geopulse-stderr.log

[Install]
WantedBy=multi-user.target
EOF

Reload Systemd and Enable Service​

sudo systemctl daemon-reload
sudo systemctl enable geopulse-backend
Service Startup

Don't start the service yet - we'll do that in step 14 after configuring the web server.


13. Configure Nginx Web Server​

Create OSM Tile Cache Directory​

GeoPulse can cache OpenStreetMap tiles for better performance. Create the cache directory before configuring Nginx:

Ubuntu/Debian:

sudo mkdir -p /var/cache/nginx/osm_tiles
sudo chown -R www-data:www-data /var/cache/nginx

RHEL/Rocky/Fedora:

sudo mkdir -p /var/cache/nginx/osm_tiles
sudo chown -R nginx:nginx /var/cache/nginx

Create Nginx Configuration​

Ubuntu/Debian:

sudo tee /etc/nginx/sites-available/geopulse > /dev/null << 'EOF'
# OSM tile cache configuration
proxy_cache_path /var/cache/nginx/osm_tiles levels=1:2 keys_zone=osm_cache:100m max_size=10g inactive=30d use_temp_path=off;

# Map for OSM tile subdomain selection
map $uri $osm_subdomain {
~^/osm/tiles/a/ "a";
~^/osm/tiles/b/ "b";
~^/osm/tiles/c/ "c";
default "a";
}

server {
listen 80;
server_name _;

root /var/www/geopulse;
index index.html;

# Maximum upload size (for GPX files, location imports, etc.)
client_max_body_size 100M;

# Gzip compression for better performance
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_comp_level 6;
gzip_min_length 1000;

# Runtime config and app shell must stay fresh after deployments
location = /config.js {
try_files $uri =404;
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
}

location = /index.html {
try_files $uri =404;
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}

# PWA entry files must stay fresh so service worker updates are not delayed
location = /manifest.webmanifest {
types {
application/manifest+json webmanifest;
}
try_files $uri =404;
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
}

location = /sw.js {
try_files $uri =404;
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
}

location = /registerSW.js {
try_files $uri =404;
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
}

# Cache static assets (but exclude OSM tiles path)
location ~* ^/(?!osm/).*\.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000";
}

# ES module assets (the MapLibre map worker). The stock mime.types has no mjs entry, and browsers refuse to
# start a module worker served as application/octet-stream.
location ~* ^/(?!osm/).*\.mjs$ {
types {
application/javascript mjs;
}
expires 1y;
add_header Cache-Control "public, max-age=31536000";
}

# OSM tiles proxy with caching
location ^~ /osm/tiles/ {
resolver 8.8.8.8 valid=300s;
resolver_timeout 10s;

# Rewrite: /osm/tiles/a/19/299420/178830.png -> /19/299420/178830.png
rewrite ^/osm/tiles/[abc]/(.*)$ /$1 break;

proxy_pass https://$osm_subdomain.tile.openstreetmap.org;
proxy_cache osm_cache;
proxy_cache_key "$scheme$proxy_host$uri";
proxy_cache_valid 200 30d;
proxy_cache_valid 404 1m;
proxy_cache_valid 502 503 504 1m;
proxy_ignore_headers Cache-Control Expires Set-Cookie;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_background_update on;
proxy_cache_lock on;

proxy_set_header Cookie "";
proxy_set_header Authorization "";
proxy_set_header User-Agent "GeoPulse/1.16";
proxy_set_header Host $osm_subdomain.tile.openstreetmap.org;
proxy_http_version 1.1;
proxy_set_header Connection "";

proxy_connect_timeout 10s;
proxy_read_timeout 10s;

expires 30d;
add_header Cache-Control "public, immutable";
add_header X-Cache-Status $upstream_cache_status always;
}

# Backend API - reverse proxy with long timeouts for bulk operations
location /api/ {
proxy_pass http://localhost:8080/api/;

# Long timeouts for GPX imports and bulk operations (60 minutes)
proxy_connect_timeout 3600s;
proxy_send_timeout 3600s;
proxy_read_timeout 3600s;

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;
}

# Handle all routes for SPA (Single Page Application)
location / {
try_files $uri $uri/ /index.html;
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}

# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;

# Logging
access_log /var/log/geopulse/nginx/access.log;
error_log /var/log/geopulse/nginx/error.log;
}
EOF

# Enable the site
sudo ln -s /etc/nginx/sites-available/geopulse /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default

# Test configuration
sudo nginx -t

# Restart Nginx
sudo systemctl restart nginx
sudo systemctl enable nginx

RHEL/Rocky/Fedora:

sudo tee /etc/nginx/conf.d/geopulse.conf > /dev/null << 'EOF'
# OSM tile cache configuration
proxy_cache_path /var/cache/nginx/osm_tiles levels=1:2 keys_zone=osm_cache:100m max_size=10g inactive=30d use_temp_path=off;

# Map for OSM tile subdomain selection
map $uri $osm_subdomain {
~^/osm/tiles/a/ "a";
~^/osm/tiles/b/ "b";
~^/osm/tiles/c/ "c";
default "a";
}

server {
listen 80;
server_name _;

root /var/www/geopulse;
index index.html;

# Maximum upload size (for GPX files, location imports, etc.)
client_max_body_size 100M;

# Gzip compression for better performance
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_comp_level 6;
gzip_min_length 1000;

# Runtime config and app shell must stay fresh after deployments
location = /config.js {
try_files $uri =404;
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
}

location = /index.html {
try_files $uri =404;
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}

# PWA entry files must stay fresh so service worker updates are not delayed
location = /manifest.webmanifest {
types {
application/manifest+json webmanifest;
}
try_files $uri =404;
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
}

location = /sw.js {
try_files $uri =404;
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
}

location = /registerSW.js {
try_files $uri =404;
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
}

# Cache static assets (but exclude OSM tiles path)
location ~* ^/(?!osm/).*\.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000";
}

# ES module assets (the MapLibre map worker). The stock mime.types has no mjs entry, and browsers refuse to
# start a module worker served as application/octet-stream.
location ~* ^/(?!osm/).*\.mjs$ {
types {
application/javascript mjs;
}
expires 1y;
add_header Cache-Control "public, max-age=31536000";
}

# OSM tiles proxy with caching
location ^~ /osm/tiles/ {
resolver 8.8.8.8 valid=300s;
resolver_timeout 10s;

# Rewrite: /osm/tiles/a/19/299420/178830.png -> /19/299420/178830.png
rewrite ^/osm/tiles/[abc]/(.*)$ /$1 break;

proxy_pass https://$osm_subdomain.tile.openstreetmap.org;
proxy_cache osm_cache;
proxy_cache_key "$scheme$proxy_host$uri";
proxy_cache_valid 200 30d;
proxy_cache_valid 404 1m;
proxy_cache_valid 502 503 504 1m;
proxy_ignore_headers Cache-Control Expires Set-Cookie;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_background_update on;
proxy_cache_lock on;

proxy_set_header Cookie "";
proxy_set_header Authorization "";
proxy_set_header User-Agent "GeoPulse/1.16";
proxy_set_header Host $osm_subdomain.tile.openstreetmap.org;
proxy_http_version 1.1;
proxy_set_header Connection "";

proxy_connect_timeout 10s;
proxy_read_timeout 10s;

expires 30d;
add_header Cache-Control "public, immutable";
add_header X-Cache-Status $upstream_cache_status always;
}

# Backend API - reverse proxy with long timeouts for bulk operations
location /api/ {
proxy_pass http://localhost:8080/api/;

# Long timeouts for GPX imports and bulk operations (60 minutes)
proxy_connect_timeout 3600s;
proxy_send_timeout 3600s;
proxy_read_timeout 3600s;

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;
}

# Handle all routes for SPA (Single Page Application)
location / {
try_files $uri $uri/ /index.html;
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}

# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;

# Logging
access_log /var/log/geopulse/nginx/access.log;
error_log /var/log/geopulse/nginx/error.log;
}
EOF

# Test configuration
sudo nginx -t

# Restart Nginx
sudo systemctl restart nginx
sudo systemctl enable nginx
Configuration Details

Port changes: If you change the backend port from 8080, update proxy_pass http://localhost:8080/api/ in the Nginx configuration above. The backend port is set via QUARKUS_HTTP_PORT in /etc/geopulse/geopulse.env

OSM tile caching: The configuration includes OpenStreetMap tile caching (up to 10GB) for better performance. Tiles are cached for 30 days in /var/cache/nginx/osm_tiles

Upload size: client_max_body_size is set to 100M to support GPX file uploads and bulk location imports

Timeouts: API timeouts are set to 60 minutes (3600s) to support long-running operations like bulk imports


14. Start and Verify Services​

Start the Backend​

sudo systemctl start geopulse-backend

Check Backend Status​

sudo systemctl status geopulse-backend

You should see: Active: active (running)

Monitor Backend Logs​

# Watch logs in real-time
sudo journalctl -u geopulse-backend -f

# Or check log file
sudo tail -f /var/log/geopulse/backend/geopulse.log

Test Backend Health Endpoint​

curl http://localhost:8080/api/v1/system/health

Expected response:

{"status":"success","message":null,"data":{"database":"UP","status":"UP"}}

Verify Database Connection​

# Check if Flyway migrations ran successfully
sudo journalctl -u geopulse-backend | grep -i flyway

You should see successful migration messages.

Access the Frontend​

Open your browser and navigate to: http://your-server-ip

You should see the GeoPulse login page.


Post-Installation​

Create First Admin User​

Admin User Setup

By default, the first registered account becomes administrator automatically.

  1. Open GeoPulse in browser
  2. Register the owner account
  3. Your account will have admin privileges

If the instance may be reachable by other people before you register, set an explicit admin email first:

sudo bash -c 'echo "GEOPULSE_ADMIN_EMAIL=admin@example.com" >> /etc/geopulse/geopulse.env'
sudo systemctl restart geopulse-backend

Configuration Management​

Applying Configuration Changes​

After modifying /etc/geopulse/geopulse.env:

# 1. Restart the backend service to apply changes
sudo systemctl restart geopulse-backend

# 2. Verify the service started successfully
sudo systemctl status geopulse-backend

# 3. Check logs if there are issues
sudo journalctl -u geopulse-backend -n 50
Configuration Changes

Changes to environment variables require a service restart. Frontend changes (Nginx config) require sudo systemctl restart nginx.

Common Configuration Changes​

Change Backend Port​

  1. Update QUARKUS_HTTP_PORT in /etc/geopulse/geopulse.env
  2. Update proxy_pass http://localhost:8080 in Nginx config to match new port
  3. Restart both services:
    sudo systemctl restart geopulse-backend
    sudo systemctl restart nginx

Add Allowed Origins (CORS)​

  1. Set GEOPULSE_CORS_ENABLED=true in /etc/geopulse/geopulse.env
  2. Set GEOPULSE_CORS_ORIGINS to your allowed origins (comma-separated)
  3. Restart backend: sudo systemctl restart geopulse-backend

Monitoring and Logs​

Backend Logs​

Systemd journal (recommended for troubleshooting):

# View recent logs
sudo journalctl -u geopulse-backend -n 100

# Follow logs in real-time
sudo journalctl -u geopulse-backend -f

# View logs since last boot
sudo journalctl -u geopulse-backend -b

# View logs for specific time period
sudo journalctl -u geopulse-backend --since "1 hour ago"

Application log files:

# Main application log
sudo tail -f /var/log/geopulse/backend/geopulse.log

# Standard output
sudo tail -f /var/log/geopulse/backend/geopulse-stdout.log

# Standard error (startup errors)
sudo tail -f /var/log/geopulse/backend/geopulse-stderr.log

Frontend/Nginx Logs​

# Nginx access log (requests)
sudo tail -f /var/log/geopulse/nginx/access.log

# Nginx error log (errors, proxy issues)
sudo tail -f /var/log/geopulse/nginx/error.log

# Nginx status
sudo systemctl status nginx

# Test Nginx configuration
sudo nginx -t

Upgrading GeoPulse​

Manual Upgrade Process​

1. Download New Version​

# Set new version
NEW_VERSION=1.39.0

# Create temporary directory
mkdir -p /tmp/geopulse-upgrade
cd /tmp/geopulse-upgrade

# Download new artifacts
wget https://github.com/tess1o/geopulse/releases/download/v${NEW_VERSION}/geopulse-frontend-${NEW_VERSION}.tar.gz
wget https://github.com/tess1o/geopulse/releases/download/v${NEW_VERSION}/geopulse-backend-native-arm64-${NEW_VERSION} # or your architecture
wget https://github.com/tess1o/geopulse/releases/download/v${NEW_VERSION}/SHA256SUMS

# Verify checksums
sha256sum -c SHA256SUMS --ignore-missing
Architecture Selection

Adjust download links to match your architecture (arm64, amd64, compatible images, jvm, etc).

2. Stop Services​

sudo systemctl stop geopulse-backend

3. Upgrade Backend​

# For native backend
sudo cp geopulse-backend-native-arm64-${NEW_VERSION} /opt/geopulse/backend/geopulse-backend
sudo chmod +x /opt/geopulse/backend/geopulse-backend
sudo chown geopulse:geopulse /opt/geopulse/backend/geopulse-backend

# For JVM backend
sudo rm -rf /opt/geopulse/backend/quarkus-app
sudo tar -xzf geopulse-backend-jvm-${NEW_VERSION}.tar.gz -C /opt/geopulse/backend
sudo chown -R geopulse:geopulse /opt/geopulse/backend
Architecture Selection

Adjust filename (geopulse-backend-native-arm64-${NEW_VERSION}) to match your architecture (arm64, amd64, compatible images, jvm, etc).

4. Upgrade Frontend​

# Remove old frontend files
sudo rm -rf /var/www/geopulse/*

# Extract new frontend
sudo tar -xzf geopulse-frontend-${NEW_VERSION}.tar.gz -C /var/www/geopulse

# Set permissions
sudo chown -R www-data:www-data /var/www/geopulse # Ubuntu/Debian
# OR
sudo chown -R nginx:nginx /var/www/geopulse # RHEL/Rocky/Fedora
Upgrading from 1.x: add the .mjs location to nginx

GeoPulse 2.0 loads the map worker from a .mjs file. The stock nginx mime.types has no entry for it, so nginx serves it as application/octet-stream and browsers refuse to start the worker: vector and 3D maps do not load. If your nginx configuration was created before 2.0, add the location ~* ^/(?!osm/).*\.mjs$ block from Configure Nginx Web Server next to the static assets location, then reload nginx:

sudo nginx -t && sudo systemctl reload nginx

5. Start Services​

# Start backend
sudo systemctl start geopulse-backend

# Check status
sudo systemctl status geopulse-backend

# Verify health
curl http://localhost:8080/api/v1/system/health

6. Verify Upgrade​

# Check logs for successful startup
sudo journalctl -u geopulse-backend -n 50

# Access frontend in browser
# Version should be visible in UI (bottom right corner or About page)

7. Cleanup​

rm -rf /tmp/geopulse-upgrade

Troubleshooting​

Backend Won't Start​

Check service status:

sudo systemctl status geopulse-backend

Check logs for errors:

# Recent logs
sudo journalctl -u geopulse-backend -n 100

# Startup errors
sudo cat /var/log/geopulse/backend/geopulse-stderr.log

Common Issues​

1. Database Connection Failed​

  • Verify PostgreSQL is running: sudo systemctl status postgresql
  • Check credentials in /etc/geopulse/geopulse.env
  • Test connection: sudo -u postgres psql -d geopulse -c "SELECT 1;"

2. Port Already in Use​

  • Change QUARKUS_HTTP_PORT in /etc/geopulse/geopulse.env
  • Update Nginx proxy_pass port to match
  • Restart services

3. Permission Denied (Binary Not Executable)​

sudo chmod +x /opt/geopulse/backend/geopulse-backend
sudo chown geopulse:geopulse /opt/geopulse/backend/geopulse-backend

4. Keys Not Found​

  • Verify files exist: sudo ls -la /opt/geopulse/keys/
  • Check paths in /etc/geopulse/geopulse.env use file: prefix
  • Regenerate if needed (see Step 7)

5. Timezone Error​

Error: could not stat file "/usr/share/zoneinfo/localtime"

sudo apt-get install -y tzdata  # Ubuntu/Debian
# or: sudo dnf install -y tzdata # RHEL/Rocky/Fedora
sudo ln -sf /usr/share/zoneinfo/UTC /etc/localtime
sudo systemctl restart postgresql geopulse-backend

6. Extension Permission Denied​

Error: permission denied to create extension "postgis_topology"

sudo -u postgres psql -d geopulse -c "CREATE EXTENSION IF NOT EXISTS postgis_topology;"
sudo systemctl restart geopulse-backend

Frontend Not Loading​

Check Nginx:

# Nginx status
sudo systemctl status nginx

# Test configuration
sudo nginx -t

# Restart if needed
sudo systemctl restart nginx

Verify files:

# Check frontend files exist
ls -la /var/www/geopulse/
# Should show: index.html, assets/, etc.

# Check permissions
sudo chown -R www-data:www-data /var/www/geopulse # Ubuntu/Debian
# OR
sudo chown -R nginx:nginx /var/www/geopulse # RHEL/Rocky/Fedora

Check Nginx logs:

sudo tail -50 /var/log/geopulse/nginx/error.log

Request Failed with Status Code 403 (CORS Error)​

This error means the backend is rejecting requests from your frontend URL due to CORS policy.

How to Fix
  1. Enable CORS and configure origins in /etc/geopulse/geopulse.env:

    GEOPULSE_CORS_ENABLED=true

    # Single domain
    GEOPULSE_CORS_ORIGINS=http://your-server-ip

    # Multiple domains (separate with commas)
    GEOPULSE_CORS_ORIGINS=http://192.168.1.100,http://example.com,https://example.com
  2. Restart backend:

    sudo systemctl restart geopulse-backend
  3. Clear browser cache and reload

API Calls Failing / Cannot Connect to Backend​

Test backend directly:

curl http://localhost:8080/api/v1/system/health

If backend responds but frontend can't connect:

  1. Check Nginx proxy configuration has correct backend port
  2. Check Nginx logs: sudo tail -f /var/log/geopulse/nginx/error.log
  3. Verify Nginx is running: sudo systemctl status nginx

If backend doesn't respond:

  1. Check backend is running: sudo systemctl status geopulse-backend
  2. Check backend logs: sudo journalctl -u geopulse-backend -n 50
  3. Verify backend port matches Nginx proxy_pass