Docs
On this page

Self-Hosted Deployment Guide

Deploy HRPulsar on your own infrastructure using Docker Compose.

Prerequisites

  • Docker 24.0+
  • Docker Compose 2.20+
  • 2+ vCPU, 4+ GB RAM, 20+ GB SSD (minimum)
  • A domain name (optional, for HTTPS)

Installation

1. Clone the repository

git clone https://github.com/HRPulsar/hrpulsar.git
cd hrpulsar

2. Configure environment

cp .env.example .env

Edit .env and set at minimum:

JWT_SECRET=your-random-secret-string-here
POSTGRES_PASSWORD=a-strong-database-password
FRONTEND_URL=https://hr.yourcompany.com   # public URL of your instance

FRONTEND_URL is what links in outgoing emails (verification, password reset, invitations) point at — without it they fall back to http://localhost:3100 and only work from the server itself. It is also the default origin that browser-facing file links (logos, avatars, attachments) are signed against, since the bundled MinIO is only reachable inside the Docker network; leave S3_PUBLIC_ENDPOINT unset unless storage is served from a different origin than the app.

Configuring an email provider (SMTP_* or RESEND_API_KEY in .env) is recommended but not required to get started:

  • No email provider configured — new accounts are verified automatically at registration, so you can sign up and log in right away. Outgoing emails (invitations, password resets, notifications) are skipped.
  • Email provider configured — registration sends a standard verification email. If delivery fails, the backend prints the verification link to its log (docker compose -f docker-compose.self-hosted.yml logs backend, look for EMAIL VERIFICATION LINK) so you can complete the signup manually.

Email links are built from FRONTEND_URL (falling back to the first CORS_ORIGINS entry) — point it at your instance's public URL.

3. Start all services

docker compose -f docker-compose.self-hosted.yml up -d --build

The first start builds the backend and frontend images (several minutes) and starts 8 services:

  • Caddy — Reverse proxy (ports 80/443)
  • Backend — FastAPI application (port 8000 internal)
  • Frontend — Next.js application (port 3000 internal)
  • Celery worker — Background jobs (emails, AI processing)
  • Celery beat — Periodic task scheduler
  • PostgreSQL — Database with pgvector extension
  • Redis — Cache and task queue
  • MinIO — S3-compatible file storage (plus a one-shot minio-init container that creates the storage bucket on first boot)

4. Access the application

The root URL takes you to the sign-in page. Create your first account at /register — the form creates your company workspace and its admin account in one step. Without an email provider configured the account is verified automatically and you are signed in immediately (see step 2 above).

HTTPS with a Domain

To enable automatic HTTPS:

  1. Point your domain's A record to your server IP
  2. Edit deploy/selfhosted/Caddyfile: replace :80 with your domain (e.g., app.yourcompany.com)
  3. Restart Caddy: docker compose -f docker-compose.self-hosted.yml restart caddy

Caddy will automatically obtain a Let's Encrypt certificate. Remember to update FRONTEND_URL in .env to the new domain — uploaded files (logos, media) are served through the proxy at that origin. Set S3_PUBLIC_ENDPOINT=https://your-domain only if storage is proxied from somewhere other than FRONTEND_URL.

Running Without Docker Compose

The compose file passes DEPLOYMENT_MODE to both the backend and the frontend containers. If you run the services yourself (systemd, bare next start), set DEPLOYMENT_MODE=onprem in the environment of both processes — the frontend uses it to serve the self-serve registration form and the sign-in redirect at the root URL; without it the app falls back to the hosted-product entry surface.

Environment Variables

VariableRequiredDefaultDescription
JWT_SECRETYeschange-me-to-a-random-stringSecret key for JWT tokens
POSTGRES_PASSWORDYeshrpulsarPostgreSQL password
DATABASE_URLNoAuto-configuredPostgreSQL connection string
REDIS_URLNoAuto-configuredRedis connection string
ANTHROPIC_API_KEYNoClaude API key (for AI features)
OPENAI_API_KEYNoOpenAI API key (for AI features)
GEMINI_API_KEYNoGemini API key (for AI features)
YANDEX_API_KEYNoYandex Foundation Models API key (for AI features)
YANDEX_FOLDER_IDNoYandex Cloud folder id, required with YANDEX_API_KEY
LLM_PROVIDERNoclaudeLLM provider (claude, openai, gemini, yandex)
SMTP_HOSTNoSMTP server for email notifications
SMTP_PORTNo587SMTP port
SMTP_USERNoSMTP username
SMTP_PASSWORDNoSMTP password
FRONTEND_URLNoFirst CORS_ORIGINS entryPublic URL of your instance, used to build links in emails
S3_ENDPOINTNoS3/MinIO endpoint for file storage
S3_ACCESS_KEYNoS3 access key
S3_SECRET_KEYNoS3 secret key
S3_BUCKETNohrpulsarS3 bucket name
S3_PUBLIC_ENDPOINTNoFRONTEND_URLPublic base URL for file links when S3_ENDPOINT is internal-only (bundled MinIO). Set it only when storage is served from another origin than the app
BRAND_NAMENoHRPulsarInstallation name in outgoing emails and the API title
BRAND_LOGO_URLNoStock logoAbsolute URL of the email-header logo
BRAND_ACCENT_COLORNo#0066FFAccent color for email buttons and links
EMAIL_FROMNoHRPulsar <notifications@hrpulsar.com>From header of outgoing emails
NEXT_PUBLIC_BRAND_NAMENoHRPulsarInstallation name in the web UI (frontend)
NEXT_PUBLIC_LOGO_URLNoStock logoWeb UI logo for light backgrounds (frontend)
NEXT_PUBLIC_LOGO_DARK_URLNoStock logoWeb UI logo for dark backgrounds (frontend)
NEXT_PUBLIC_BRAND_ACCENT_COLORNo#0066FFWeb UI accent color, any CSS color (frontend)
NEXT_PUBLIC_FAVICON_URLNo/icon.svgWeb UI favicon (frontend)
NEXT_PUBLIC_SIDEBAR_LOGO_HEIGHTNo28pxSidebar logo height, px/rem value (frontend)
NEXT_PUBLIC_BRAND_THEMENodefaultTheme preset: default, teal, slate, violet (frontend)
NEXT_PUBLIC_BRAND_AUTH_BG_COLORNoStock darkLogin/register background color (frontend)
NEXT_PUBLIC_BRAND_AUTH_BG_URLNoLogin/register background image URL (frontend)
AVAILABLE_LOCALESNoenComma-separated interface locales this install offers, e.g. de,en
DEFAULT_LOCALENoenFallback interface locale; must be listed in AVAILABLE_LOCALES
NEXT_PUBLIC_AVAILABLE_LOCALESNoenFrontend counterpart of AVAILABLE_LOCALES — keep both in sync
NEXT_PUBLIC_DEFAULT_LOCALENoenFrontend counterpart of DEFAULT_LOCALE — keep both in sync

Branding

The platform is white-label ready: logo, installation name, accent color and favicon can all be replaced through environment variables, without touching the source code. A default installation is identical to the stock HRPulsar build.

Frontend (web UI) variables — set on the frontend container:

NEXT_PUBLIC_BRAND_NAME="Acme Talent"
NEXT_PUBLIC_LOGO_URL=https://cdn.acme.example/logo-light-bg.svg
NEXT_PUBLIC_LOGO_DARK_URL=https://cdn.acme.example/logo-dark-bg.svg
NEXT_PUBLIC_BRAND_ACCENT_COLOR="#AA0044"
NEXT_PUBLIC_FAVICON_URL=https://cdn.acme.example/favicon.png
  • NEXT_PUBLIC_BRAND_NAME replaces the name in browser titles, page metadata and every place the UI mentions the platform by name.
  • Logos: NEXT_PUBLIC_LOGO_URL is used on light surfaces (sidebar in light theme); NEXT_PUBLIC_LOGO_DARK_URL on dark surfaces (auth pages, sidebar in dark theme). If only NEXT_PUBLIC_LOGO_URL is set, it is used everywhere. Horizontal logos around 5:1 aspect ratio work best; the sidebar renders them at 28px height by default. If your logo's proportions need a different size, set NEXT_PUBLIC_SIDEBAR_LOGO_HEIGHT to an exact px/rem value (e.g. 24px).
  • NEXT_PUBLIC_BRAND_ACCENT_COLOR accepts any CSS color and recolors links, accent-colored buttons and badges, focus rings, and charts. Hover/darker shades are derived automatically. The neutral dark (near-black) primary buttons and body text are intentionally not affected. The same accent is applied in both light and dark themes — pick a color with sufficient contrast in both.
  • NEXT_PUBLIC_BRAND_THEME switches the whole UI to a curated theme preset — surfaces, accent, charts, sidebar and corner radius change together, in both light and dark mode. Available presets: default (stock navy + blue), teal (deep navy + teal accent), slate (neutral graphite + steel blue), violet (warm neutrals + violet accent). An explicit NEXT_PUBLIC_BRAND_ACCENT_COLOR still applies on top of the preset's accent for point tweaks.
  • The login/register pages can carry their own background: NEXT_PUBLIC_BRAND_AUTH_BG_COLOR replaces the stock dark backdrop with a solid color; NEXT_PUBLIC_BRAND_AUTH_BG_URL (absolute https:// or root-relative URL) shows a full-bleed image instead — it is scaled to cover the viewport on every screen size, and a dark scrim keeps the sign-in card readable on any image. Setting either hides the stock starfield decoration.
  • The variables are read at runtime on every request, so a prebuilt image (GHCR) picks them up from the container environment — no rebuild needed.

Backend (email) variables — set on the backend container:

BRAND_NAME="Acme Talent"
BRAND_LOGO_URL=https://cdn.acme.example/email-logo.png
BRAND_ACCENT_COLOR="#AA0044"
EMAIL_FROM="Acme Talent <notifications@acme.example>"

Outgoing emails then carry the custom name, header logo (rendered at 31px height) and button/link color. EMAIL_FROM controls the From header.

On a branded install, always set EMAIL_FROM, FRONTEND_URL and BRAND_LOGO_URL together with BRAND_NAME. EMAIL_FROM defaults to the stock HRPulsar sender — leaving its address on the stock domain means recipients see the stock brand in the From header (and in the SMTP Message-ID, which is derived from the sender's domain). FRONTEND_URL is what links inside emails point at, and without BRAND_LOGO_URL the email header renders the stock logo. The backend logs a startup warning when BRAND_NAME is customized but any of the three is left at its default.

Static assets that live in the frontend image (site.webmanifest, apple-touch-icon.png, PNG icons) can be replaced by mounting your own files over /app/public/* in the frontend container if you need a fully branded install surface (PWA icons, home-screen name).

Interface Languages

An install can offer more than one interface language. Set the pair on both containers (backend reads AVAILABLE_LOCALES / DEFAULT_LOCALE, frontend reads the NEXT_PUBLIC_* counterparts at runtime):

AVAILABLE_LOCALES=de,en
DEFAULT_LOCALE=de
NEXT_PUBLIC_AVAILABLE_LOCALES=de,en
NEXT_PUBLIC_DEFAULT_LOCALE=de

The onboarding wizard then asks for the workspace default language, and every member can pick a personal language in profile settings. The backend refuses to start when DEFAULT_LOCALE is not listed in AVAILABLE_LOCALES. Single-locale installs (the default) hide the language selects entirely. The wizard also records the AI content language — the language the AI generates competences and development plans in — which is independent of the interface language.

Upgrading

Back up the database first (see below), then:

cd hrpulsar
git pull
docker compose -f docker-compose.self-hosted.yml build
docker compose -f docker-compose.self-hosted.yml up -d

Migrations run automatically on backend startup. Building happens while the old version keeps serving; expect well under a minute of downtime during the final up -d switchover.

Local changes and upgrades:

  • Keep your customizations out of docker-compose.self-hosted.yml — put them in a separate override file and pass both files on every command, e.g. docker compose -f docker-compose.self-hosted.yml -f docker-compose.local.yml up -d. An edited compose file will conflict on git pull.
  • The one file you are expected to edit in place is deploy/selfhosted/Caddyfile (your domain). If an upgrade touches it, git pull will ask you to merge — re-apply your domain line.

Backup & Restore

Backup

./scripts/backup_db.sh ./backups

Or set up a daily cron job:

crontab -e
# Add: 0 3 * * * /path/to/hrpulsar/scripts/backup_db.sh /path/to/backups

Restore

gunzip -c backups/hrpulsar_YYYYMMDD_HHMMSS.sql.gz | \
  docker compose -f docker-compose.self-hosted.yml exec -T postgres psql -U hrpulsar hrpulsar

Troubleshooting

Check service status

docker compose -f docker-compose.self-hosted.yml ps

View logs

docker compose -f docker-compose.self-hosted.yml logs backend
docker compose -f docker-compose.self-hosted.yml logs frontend

Health check

The backend port is not published on the host — query it through the proxy:

curl http://localhost/health

"status": "ok" means database, Redis, file storage, and the Celery worker are all reachable.

Hardware Recommendations

UsersCPURAMDisk
Up to 1002 vCPU4 GB20 GB SSD
100-5004 vCPU8 GB50 GB SSD
500+8 vCPU16 GB100 GB SSD