DTPulseOn-prem documentation

DTPulse on-prem

Self-hosted DTPulse — full feature parity with our SaaS at dtpulse.com, running on your own infrastructure. Single Docker stack, license-keyed, phone-home heartbeat to our license server.

Requirements

  • Linux host (any distro that runs Docker)
  • Docker 24+ and Docker Compose v2
  • 4 GB RAM, 20 GB free disk
  • Ports 80 and 443 open from the public internet (Caddy auto-provisions Let's Encrypt TLS certs)
  • A DNS A/AAAA record pointing the chosen domain at this host
  • Outbound HTTPS to dtpulse.com at install time only — used by /api/license/redeem to bind the license key to this host's instanceId. After install the instance runs fully offline for the entire license lifetime: the key is HMAC-signed with expiresAt, plan and maxEmployees embedded in the payload, all validated locally on every request without any phone-home.
Air-gapped after install. The optional license heartbeat is disabled by default (LICENSE_HEARTBEAT_ENABLED env unset). Operators who want seamless renewals and remote-revocation capability can flip the env flag — recommended cadence daily, not hourly. Truly air-gapped deployments (no outbound during install either) — contact sales for a pre-bound key.

Step 1 — Install Docker

The DTPulse stack runs entirely inside Docker containers, so Docker is a hard prerequisite: without it, the one-line installer in step 2 will refuse to proceed.

If Docker is not installed on this host yet:

curl -fsSL https://get.docker.com | sh

This is the official installer from docker.com; it detects the distro, adds the repositories, and installs Docker Engine + the Docker Compose v2 plugin in one command. Works on Ubuntu, Debian, RHEL/CentOS/Rocky/AlmaLinux, Fedora, SUSE.

Verify the install:

docker version
docker compose version

Both should print Docker Engine 24+ and Docker Compose v2+. If you're on a distro that ships an older Docker (Debian stable, older RHEL), use the get.docker.com script above instead of the distro package.

If you're not root, add yourself to the docker group so subsequent docker commands don't need sudo:

sudo usermod -aG docker $USER && newgrp docker

Step 2 — Install DTPulse (10 minutes)

Once Docker is ready, run the one-line installer:

sh -c "$(curl -fsSL https://dtpulse.com/install)"

The installer will prompt for:

  • License keydtp_op_… token issued by sales. Verified against the license server immediately; revoked, mistyped, or wrong-platform keys re-prompt instead of letting install proceed.
  • Admin email — first admin's login
  • Company name — displayed in UI and emails
  • HTTP port — default 80; you can choose another (e.g. 8080) if 80 is already taken
  • SMTP host / user / password / from — for invites, password reset, license-expiry warnings (skippable, but heavily recommended)

It pulls Docker images, brings up the stack, and prints the one-time admin password directly to the install output between SEED_CREDENTIALS markers. The whole thing takes 2–5 minutes on a fresh VPS.

Advanced installer (with domain + Caddy + automatic TLS) — if you have a domain ready and want HTTPS from the start, contact sales for the production install kit that runs Caddy with Let's Encrypt out of the box. The one-line installer above is HTTP-only on a bare IP.

First-time login

1. Open https://your-domain.com/ (TLS auto-provisioned by Caddy)
2. Sign in with the admin email and the one-time password from the install output
3. The app will prompt you to change the password immediately

Lost the one-time password before signing in? Re-fetch from logs:

cd /opt/dtpulse
docker compose logs app | grep -A4 SEED_CREDENTIALS

(Available until logs roll. Once SMTP is configured, "Forgot password?" on /signin works too.)

Configuring email

The installer asks for SMTP credentials up-front — if you skipped that step, the app still runs but:

  • Invites can't be sent
  • Password-reset is broken (lock-out risk if admin forgets password)
  • Absence-approval emails don't fire
  • License-expiry warnings won't reach you — you'll find out the day the app blocks access, not 14 days before. This is the most critical reason not to skip SMTP

To add SMTP later, edit /opt/dtpulse/.env:

SMTP_HOST=smtp.resend.com        # or your own Postfix, smtp.sendgrid.net, AWS SES SMTP, etc.
SMTP_PORT=465
SMTP_USER=[email protected]
SMTP_PASS=your-password-or-api-key
EMAIL_FROM=DTPulse [email protected]

Then restart:

cd /opt/dtpulse && docker compose restart app

Verify it works: in DTPulse, go to /admin/staff, invite yourself at a different address, and confirm the invite email arrives. If it doesn't, check the app logs:

docker compose logs app | grep -i 'email\|smtp'

SSO setup (optional)

DTPulse supports SAML 2.0 (Okta, Azure AD, Google Workspace, OneLogin). Configure in /admin/integrations after first login. Per-tenant config — no env vars needed.

Changing the domain after install

If you installed against one hostname (e.g. an IP, or dtpulse.staging.example.com) and later want to move to a different one, update NEXTAUTH_URL and reload:

cd /opt/dtpulse
sed -i 's|^NEXTAUTH_URL=.*|NEXTAUTH_URL=https://new-domain.example.com|' .env
sed -i 's|^DOMAIN=.*|DOMAIN=new-domain.example.com|' .env
docker compose up -d app caddy

Why this matters: every email (password reset, verify-email, absence approvals, lifecycle warnings, review invitations) builds links as ${NEXTAUTH_URL}/.... If NEXTAUTH_URL still points at the old hostname, users will get email links to a host that no longer serves the app — and password-reset / verify-email tokens are stored in your local DB, so they can't be opened from anywhere else either. The app logs a warning at startup if NEXTAUTH_URL is empty or points at our SaaS domain:

docker compose logs app | grep '\[on-prem startup\]'

Backups

Two volumes to back up:

  • pgdata — Postgres data (everything: tenants, employees, KB articles, etc.)
  • ./data/ — runtime files (license cache, initial credentials)

Recommended: nightly pg_dump:

docker compose exec db pg_dump -U dtpulse dtpulse | gzip > backups/$(date +%F).sql.gz

Upgrades

The /admin/billing page shows whether an update is available, how old the current install is, and — once the upgrade agent is enabled — a one-click Upgrade to latest button. Before running an upgrade, back up:

cd /opt/dtpulse && mkdir -p backups && docker compose exec -T db pg_dump -U dtpulse dtpulse | gzip > backups/dtpulse-$(date +%F-%H%M).sql.gz

Manual upgrade (works on every install):

cd /opt/dtpulse && docker compose pull && docker compose up -d

Pulls the latest published image, rolls the app + cron containers. Postgres data and configs are preserved. Migrations run automatically; on a migration failure docker-compose keeps the previous container running, so you have a safe rollback.

To pin a specific version instead of :latest, edit .env:

IMAGE_TAG=v1.4.2

One-click upgrade from the UI — for installs that include the upgrade agent sidecar. New installs from the one-line installer get this by default. For installs that predate the sidecar, add this service to your docker-compose.yml under services: (next to app/cron/db) and run docker compose up -d:

upgrader:
    image: ghcr.io/monteteam/dtpulse-upgrader:latest
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./:/compose:ro
      - ./data:/data
    environment:
      POLL_INTERVAL: "10"

The sidecar mounts the docker socket (this is the ONLY container in the stack that does — the main app never gets docker socket access) and polls data/.upgrade-requested.json every 10 s. When the button in /admin/billing writes that trigger, the sidecar runs docker compose pull && docker compose up -d and the app comes back on the new image ~60–90 s later.

License renewal

Your license has an expiry date. The app warns admins by email 14 days before, 7 days before, and on the day of expiry; after 30 more days of grace period, login is suspended. No data is deleted at any point — entering a new license key on the /blocked page (or via email contact to sales) restores access immediately.

Renewal: request a new key from [email protected], then either:

  • Edit LICENSE_KEY in .env and docker compose restart app
  • Or, if already blocked, paste the new key on the /blocked page

Troubleshooting

Installer rejects the license key at the prompt:

  • Couldn't reach the license server — outbound HTTPS to dtpulse.com is blocked from this host. Open egress for dtpulse.com:443 and retry the prompt.
  • Key not recognized — typo, or you used a key issued for a different platform. Double-check the key sales sent you; it must start with dtp_op_.
  • This key was revoked / already bound to another installation — contact sales for a replacement or to rebind for a legitimate VPS migration.

Site doesn't load (no TLS, no response):

  • DNS: confirm domain points to this host:
dig +short your-domain.com
  • Firewall: ports 80 and 443 open:
sudo ufw status
  • Caddy logs:
docker compose logs caddy

/blocked page appears immediately after install:

  • License-server unreachable from this host
  • Test:
curl https://dtpulse.com/api/license/verify
  • Confirm LICENSE_KEY env matches the key sales issued

Initial admin password not shown / scrolled off:

  • Re-fetch from logs:
docker compose logs app | grep -A4 SEED_CREDENTIALS
  • If logs rolled and nothing matches, use "Forgot password?" on /signin (requires SMTP configured).

Database connection failures after host reboot:

  • Postgres started before Docker; restart: unless-stopped should handle this. If not:
docker compose restart

Support