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.comat install time only — used by/api/license/redeemto bind the license key to this host'sinstanceId. After install the instance runs fully offline for the entire license lifetime: the key is HMAC-signed withexpiresAt,planandmaxEmployeesembedded 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 key —
dtp_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) if80is 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_KEYin.envanddocker compose restart app - Or, if already blocked, paste the new key on the
/blockedpage
Troubleshooting
Installer rejects the license key at the prompt:
Couldn't reach the license server— outbound HTTPS todtpulse.comis blocked from this host. Open egress fordtpulse.com:443and 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 withdtp_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_KEYenv 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-stoppedshould handle this. If not:
docker compose restart
Support
- Sales / license renewal:
[email protected] - Security disclosures:
[email protected]