Everything you need to deploy, manage, and update your own Reqcore applicant tracking system. No technical background required.
- What is Self-Hosting?
- Why Self-Host Reqcore?
- Requirements
- Quick Start — Pre-built Image (Fastest)
- Quick Start — Build from Source (5 Minutes)
- Step-by-Step Installation
- Updating Your Instance
- Backups & Data Safety
- Custom Domain & HTTPS
- Email Configuration
- Security Best Practices
- Feature Flags
- Monitoring & Health Checks
- Troubleshooting
- FAQ
Self-hosting means running Reqcore on a server you control — your own computer, a rented server, or a cloud virtual machine — instead of using a service managed by someone else. Your recruitment data, candidate documents, and hiring pipeline stay entirely under your control.
Think of it like the difference between renting an apartment and owning a house. With self-hosting, you hold the keys. Nobody else can access your data, change the terms of service, or shut down the platform on you.
What you get:
- Complete ownership of all candidate data, documents, and hiring records
- No per-seat pricing — unlimited team members at zero marginal cost
- No data leaves your network unless you choose to integrate external services
- Full source code access — you can audit, modify, and extend everything
Your candidate resumes, interview feedback, and hiring pipeline live on your infrastructure. This matters for organizations with data residency requirements (GDPR, industry regulations) or those who simply want the peace of mind that comes with data ownership.
A single $5–10/month VPS handles Reqcore for most teams. Compare that to cloud ATS platforms that charge $50–200 per seat per month. For a team of 10, that could mean saving $6,000+ annually.
The database is standard PostgreSQL. Documents are stored in S3-compatible storage (MinIO). If you ever want to switch tools or export your data, standard database and S3 tools work out of the box.
No analytics, no tracking, no data sharing with third parties. The only telemetry is PostHog analytics, which is disabled by default in self-hosted mode and must be explicitly configured if desired.
| Requirement | Details |
|---|---|
| A computer or server | Any modern Linux machine, Mac, or Windows PC with WSL2 |
| Docker Desktop | Free software that packages Reqcore and its dependencies together |
| 2 GB RAM | The minimum. 4 GB is comfortable for teams with heavy usage |
| 10 GB disk space | For the application, database, and uploaded documents |
| Internet connection | Only needed for initial setup and pulling updates |
If you need to rent a server, these providers offer affordable options suitable for Reqcore:
| Provider | Minimum Plan | Monthly Cost | Notes |
|---|---|---|---|
| Hetzner | CX22 (2 vCPU, 4 GB RAM) | ~€4/month | Best value. European data centers. |
| DigitalOcean | Basic Droplet (1 vCPU, 2 GB RAM) | $12/month | Beginner-friendly interface. |
| Vultr | Cloud Compute (1 vCPU, 2 GB RAM) | $12/month | Global data centers. |
| Railway | Hobby Plan | $5/month | One-click deploy. See README for details. |
All of these providers offer one-click Docker installation when creating a server, which simplifies the setup further.
Use the official pre-built Docker image from GitHub Container Registry. No cloning, no building — just pull and run.
Every GitHub Release ships with a reqcore-<version>.tar.gz bundle that contains setup.sh and a docker-compose.production.yml with the image tag already pinned to that exact version. This is the most reliable way to install or upgrade.
# 1. Download and extract the latest release bundle
curl -fsSL -o reqcore.tar.gz https://github.com/reqcore-inc/reqcore/releases/latest/download/reqcore-$(curl -fsSL https://api.github.com/repos/reqcore-inc/reqcore/releases/latest | grep tag_name | cut -d '"' -f 4 | sed 's/^v//').tar.gz
tar -xzf reqcore.tar.gz && cd reqcore-*
# 2. Generate secure passwords (one-time)
./setup.sh
# 3. Start everything
docker compose -f docker-compose.production.yml up -d
# 4. Open your browser
# → http://localhost:3000To upgrade later, download the newer release bundle into a new directory, copy your existing .env over, and run docker compose up -d.
# 1. Download just the files you need
mkdir reqcore && cd reqcore
curl -fsSLO https://raw.githubusercontent.com/reqcore-inc/reqcore/main/docker-compose.production.yml
curl -fsSLO https://raw.githubusercontent.com/reqcore-inc/reqcore/main/setup.sh
chmod +x setup.sh
# 2. Generate secure passwords (one-time)
./setup.sh
# 3. Start everything
docker compose -f docker-compose.production.yml up -d
# 4. Open your browser
# → http://localhost:3000That's it. Sign up, create your organization, and start hiring.
Want to pin a specific version? Edit docker-compose.production.yml and replace latest with a version tag (e.g., 1.3.0):
app:
image: ghcr.io/reqcore-inc/reqcore:1.3.0Every published image is signed with cosign using GitHub's keyless OIDC. To verify the image you pulled was actually built by the official release workflow:
cosign verify ghcr.io/reqcore-inc/reqcore:<version> \
--certificate-identity-regexp 'https://github.com/reqcore-inc/reqcore/.github/workflows/docker-publish.yml@.*' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com'A successful verification confirms the image is unmodified and was produced by the official CI pipeline.
If you prefer to build from source (useful for development or customization):
# 1. Download Reqcore
git clone https://github.com/reqcore-inc/reqcore.git
cd reqcore
# 2. Generate secure passwords (one-time)
./setup.sh
# 3. Start everything
docker compose up -d
# 4. Open your browser
# → http://localhost:3000Sign up, create your organization, and start hiring.
Want demo data to explore first?
docker compose exec app npm run db:seedThen sign in with demo@reqcore.com / demo1234.
Docker packages Reqcore and all its dependencies (database, file storage) into isolated containers. You install Docker once, and everything else is handled automatically.
On Ubuntu/Debian (most common server OS):
# Install Docker
curl -fsSL https://get.docker.com | sh
# Allow your user to run Docker without sudo
sudo usermod -aG docker $USER
# Log out and back in for the group change to take effect
exit
# Then reconnect to your serverOn Mac: Download Docker Desktop for Mac and follow the installer.
On Windows: Download Docker Desktop for Windows. Ensure WSL2 is enabled (Docker Desktop will prompt you if it's not).
Verify it's working:
docker --version
# Should print something like: Docker version 24.x.xgit clone https://github.com/reqcore-inc/reqcore.git
cd reqcoreIf you don't have git installed:
# Ubuntu/Debian
sudo apt install git
# Mac (comes with Xcode Command Line Tools)
xcode-select --installReqcore needs database passwords and authentication secrets. The setup script generates cryptographically random values for you:
./setup.shThis creates a .env file with all necessary configuration. Never share or commit this file — it contains your database passwords and authentication secrets.
docker compose up -dThe -d flag runs everything in the background. The first startup takes 2–5 minutes as Docker downloads the required images and builds the application.
What's happening behind the scenes:
- Docker starts a PostgreSQL 16 database for your data
- Docker starts MinIO (S3-compatible storage) for document uploads
- Docker builds and starts the Reqcore application
- Database migrations run automatically to create all required tables
Open your browser and navigate to:
http://localhost:3000
If you're running on a remote server, replace localhost with your server's IP address (e.g., http://203.0.113.42:3000).
- Click Sign Up
- Enter your name, email, and a strong password
- Create your organization (e.g., your company name)
- You're ready to start posting jobs and tracking candidates
Reqcore includes a built-in update system accessible from the Settings panel. No command line needed.
- Sign in to your Reqcore instance
- Go to Settings → Updates
- The page automatically checks for new versions
- If an update is available, click "Create backup first" (recommended)
- Click "Update to vX.Y.Z" and confirm
- Wait for the update to complete (usually under 2 minutes)
- Refresh the page
The UI shows the progress of each update step and clearly indicates success or failure. Your data is always preserved — database migrations run automatically.
If you're using the pre-built image (docker-compose.production.yml):
# Navigate to your Reqcore directory
cd /path/to/reqcore
# Pull the latest image and restart
docker compose -f docker-compose.production.yml pull app
docker compose -f docker-compose.production.yml up -dTo update to a specific version, edit docker-compose.production.yml and change the image tag:
app:
image: ghcr.io/reqcore-inc/reqcore:1.4.0Then run docker compose -f docker-compose.production.yml up -d.
If you cloned the repository and build locally:
# Navigate to your Reqcore directory
cd /path/to/reqcore
# Pull the latest version
git pull origin main
# Rebuild and restart
docker compose up --build -dThe entire process takes 2–5 minutes depending on your server's speed. There's about 30 seconds of downtime while the new container starts.
- Code pull: The latest version is downloaded from GitHub
- Docker rebuild: A new container image is built with the updated code
- Container restart: The old container is replaced with the new one
- Migrations: Database schema changes are applied automatically
- Ready: The application is available at the same URL
Your data is never lost during updates. The database and uploaded files live in Docker volumes that persist across container rebuilds.
The Settings → Updates page automatically checks whether a new version is available by comparing your installed version against the latest GitHub release. No data is sent to any external service — only a single API call to GitHub's public releases endpoint.
Before applying any update through the UI, you can click the "Create backup first" button. This creates a full PostgreSQL dump that you can restore from if anything goes wrong.
# Create a backup
docker compose exec db pg_dump -U reqcore reqcore > backup-$(date +%Y%m%d).sql
# Restore from a backup (⚠️ this replaces all current data)
cat backup-20260315.sql | docker compose exec -T db psql -U reqcore reqcoreAdd this to your server's crontab (crontab -e) to create daily backups:
# Daily backup at 2 AM, keep last 30 days
0 2 * * * cd /path/to/reqcore && docker compose exec -T db pg_dump -U reqcore reqcore > /path/to/backups/reqcore-$(date +\%Y\%m\%d).sql && find /path/to/backups -name "reqcore-*.sql" -mtime +30 -deleteUploaded documents (resumes, cover letters) are stored in MinIO. To back them up:
# Copy MinIO data to a local directory
docker cp reqcore_minio:/data ./minio-backup-$(date +%Y%m%d)For a complete backup of everything (database + documents + configuration):
# Stop the instance briefly
docker compose stop
# Backup Docker volumes
docker run --rm -v reqcore_postgres_data:/data -v $(pwd):/backup alpine tar czf /backup/postgres-backup.tar.gz -C /data .
docker run --rm -v reqcore_minio_data:/data -v $(pwd):/backup alpine tar czf /backup/minio-backup.tar.gz -C /data .
# Copy your .env file (contains passwords)
cp .env .env.backup
# Restart
docker compose up -dFor production deployments, place a reverse proxy (Caddy, Nginx, or Traefik) in front of Reqcore to handle HTTPS certificates automatically.
Option A: Caddy (Simplest — Automatic HTTPS)
Caddy automatically obtains and renews Let's Encrypt certificates. Install Caddy on your server, then create a Caddyfile:
ats.yourcompany.com {
reverse_proxy localhost:3000
}
Start Caddy:
caddy startThat's it. Caddy handles HTTPS certificates automatically.
Option B: Nginx + Certbot
# Install Nginx and Certbot
sudo apt install nginx certbot python3-certbot-nginx
# Configure Nginx
sudo tee /etc/nginx/sites-available/reqcore <<EOF
server {
listen 80;
server_name ats.yourcompany.com;
location / {
proxy_pass http://localhost:3000;
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;
client_max_body_size 25M;
}
}
EOF
sudo ln -s /etc/nginx/sites-available/reqcore /etc/nginx/sites-enabled/
sudo certbot --nginx -d ats.yourcompany.comPoint your domain to your server's IP address by adding an A record at your DNS provider:
| Type | Name | Value | TTL |
|---|---|---|---|
| A | ats | Your server's IP (e.g., 203.0.113.42) | 300 |
After setting up a custom domain, update your .env file:
BETTER_AUTH_URL=https://ats.yourcompany.com
NUXT_PUBLIC_SITE_URL=https://ats.yourcompany.comThen restart:
docker compose up --build -dBy default, Reqcore logs email content to the console (useful for development). For production, configure a transactional email service.
Priority: When SMTP_HOST is set, SMTP is used. Otherwise, if RESEND_API_KEY is set, Resend is used. If neither is configured, emails are logged to the console.
SMTP works with any mail server — Postfix, Gmail, Exchange, Mailcow, Mailu, etc. No external service dependency.
- Add to your
.envfile:
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=reqcore@example.com
SMTP_PASS=your-smtp-password
SMTP_FROM="Reqcore <noreply@example.com>"
SMTP_SECURE=false # true for implicit TLS (port 465), false for STARTTLS (port 587)- Restart:
docker compose up --build -d
Common setups:
| Provider | SMTP_HOST | SMTP_PORT | SMTP_SECURE |
|---|---|---|---|
| Gmail (App Password) | smtp.gmail.com |
587 |
false |
| Outlook / Office 365 | smtp.office365.com |
587 |
false |
| Mailcow / Mailu | your server hostname | 587 |
false |
| Custom Postfix | your server hostname | 587 or 465 |
false / true |
For Gmail, generate an App Password — your regular Gmail password will not work.
- Sign up at resend.com (free tier: 3,000 emails/month)
- Verify your sending domain
- Create an API key
- Add to your
.envfile:
RESEND_API_KEY=re_xxxxxxxxxxxx
RESEND_FROM_EMAIL="Reqcore <noreply@yourcompany.com>"- Restart:
docker compose up --build -d
Reqcore ships with security defaults that require no configuration:
- All services are localhost-bound — PostgreSQL, MinIO, and Adminer are never exposed to the internet. Only the application port (3000) is accessible externally.
- Automatic CSRF protection via Better Auth
- Encrypted OAuth tokens with AES-256-GCM
- Rate limiting on sensitive endpoints (in-memory, single-instance — see "Scaling horizontally" below if you run multiple replicas)
- Security headers —
X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy: strict-origin-when-cross-origin,Permissions-Policyrestricting camera/microphone/geolocation - File upload validation — MIME type verification, file size limits, filename sanitization
- Server-proxied downloads — uploaded files are never served directly from storage; they pass through the application server, which enforces authentication and authorization
- Deny-by-default access control — every API endpoint checks org membership and role permissions
- Backups never leak app secrets — the in-app
pg_dumprunner spawns the child process with a minimal env (PGPASSWORD + a small whitelist of system vars) so application secrets likeBETTER_AUTH_SECRET,S3_SECRET_KEY, and OAuth credentials are never inherited by the subprocess
Use a firewall:
# Ubuntu/Debian — only allow SSH, HTTP, HTTPS
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enableKeep Docker updated:
sudo apt update && sudo apt upgrade docker-ce docker-ce-cli containerd.ioUse strong passwords: The setup.sh script generates cryptographically random passwords. Don't replace them with weak alternatives.
Enable automatic security updates:
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgradesReqcore is designed to run as a single instance on one VPS. The built-in rate limiter keeps state in memory, which is perfect for a single container but means two replicas would each enforce their own per-IP counters independently — a determined attacker could double their effective budget by spreading requests across replicas.
If you do need to run multiple Reqcore instances behind a load balancer, move rate limiting to the edge rather than into the app:
| Edge layer | What to configure |
|---|---|
| Cloudflare (free) | Security → WAF → Rate limiting rules per path |
| Caddy | rate_limit directive in your Caddyfile |
| Nginx | limit_req_zone + limit_req directives |
This is also more efficient — abusive traffic is dropped before it ever reaches a Nuxt process — and it removes the need for any extra infrastructure inside Reqcore itself.
Reqcore supports Single Sign-On via any OIDC-compliant identity provider — Keycloak, Authentik, Authelia, Okta, Azure AD, and more. When configured, a "Sign in with SSO" button appears on the login and registration pages.
- Centralized identity — users sign in once across all internal tools
- Zero-friction onboarding — new hires get instant access, leavers are cut off centrally
- Enterprise security — MFA, session policies, and brute-force protection managed in one place
1. Create an OIDC client in your identity provider:
| Setting | Value |
|---|---|
| Client type | OpenID Connect (confidential) |
| Client ID | Any name (e.g., reqcore) |
| Client authentication | ON (confidential/secret) |
| Valid redirect URI | https://your-reqcore-domain.com/api/auth/oauth2/callback/oidc |
| Valid post-logout redirect URI | https://your-reqcore-domain.com/* |
| Scopes | openid, email, profile |
2. Set environment variables:
# All three are required to activate SSO
OIDC_CLIENT_ID=reqcore
OIDC_CLIENT_SECRET=your-client-secret-from-provider
OIDC_DISCOVERY_URL=https://keycloak.example.com/realms/master/.well-known/openid-configuration
# Optional: customize the button label (default: "SSO")
OIDC_PROVIDER_NAME=Company SSO3. Restart Reqcore:
docker compose down && docker compose up -dThe SSO button appears automatically on the sign-in and sign-up pages.
| Provider | Discovery URL format |
|---|---|
| Keycloak | https://keycloak.example.com/realms/YOUR_REALM/.well-known/openid-configuration |
| Authentik | https://authentik.example.com/application/o/YOUR_APP/.well-known/openid-configuration |
| Authelia | https://authelia.example.com/.well-known/openid-configuration |
| Okta | https://YOUR_ORG.okta.com/.well-known/openid-configuration |
| Azure AD | https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0/.well-known/openid-configuration |
- PKCE (Proof Key for Code Exchange) is enabled by default for protection against authorization code interception
- Issuer validation (RFC 9207) is enforced to prevent OAuth mix-up attacks
- OIDC discovery automatically fetches and validates all provider endpoints
- SSO is completely opt-in — it has zero impact when the environment variables are not set
Reqcore ships some features behind feature flags so they can be tested in production before being released to everyone. The full list of flags lives in shared/feature-flags.ts.
Every flag has a safe default value baked into the code. You get that default automatically — no PostHog account or external service required.
If you want to opt into an experimental feature (or disable a stable one), set an environment variable matching the pattern:
FEATURE_FLAG_<UPPERCASE_KEY_WITH_UNDERSCORES>=trueExamples:
# Enable the new chatbot experience for everyone on this instance
FEATURE_FLAG_CHATBOT_EXPERIENCE=true
# Force-disable a flag that defaults to on
FEATURE_FLAG_SOMETHING_ELSE=falseRestart the container after editing .env. Env-var overrides win over any PostHog rollout, so this is the authoritative knob for self-hosters.
- URL query string (e.g.
?ff_chatbot-experience=true) — handy for QA - Env var override (
FEATURE_FLAG_*) — what you'll use 99% of the time - PostHog rollout — only applies when
POSTHOG_PUBLIC_KEYis set - Registry default from
shared/feature-flags.ts
Optional. Set POSTHOG_PUBLIC_KEY and POSTHOG_HOST in .env, then create a flag in your PostHog project with a key matching the registry (e.g. chatbot-experience). For server-side flags without per-request HTTP calls, also set POSTHOG_FEATURE_FLAGS_KEY to a personal API key with the Feature Flags: read scope.
Navigate to Settings → Updates in your Reqcore instance to view:
- Service status — Real-time health of the application, database, and file storage
- System resources — Memory usage, uptime, deployment method
- Version info — Current version and available updates
- Changelog — What changed in each version
All services include built-in health checks. Check their status:
# View service health
docker compose ps
# View logs for a specific service
docker compose logs app # Application logs
docker compose logs db # Database logs
docker compose logs minio # Storage logsFor production instances, consider using a free uptime monitoring service:
- UptimeRobot — Free tier: 50 monitors, 5-minute checks
- Healthchecks.io — Free tier: cron job monitoring
Point the monitor at your Reqcore URL (e.g., https://ats.yourcompany.com) and get notified if your instance goes down.
Docker isn't running. Start it:
# Linux
sudo systemctl start docker
# Mac/Windows
# Open Docker Desktop applicationAnother application is using port 3000. Either stop that application or change Reqcore's port:
# In docker-compose.yml, change the ports line for the app service:
ports:
- "8080:3000" # Access Reqcore on port 8080 insteadThe database container might still be starting. Wait 30 seconds and try again:
# Check if all services are healthy
docker compose ps
# Restart everything
docker compose down && docker compose up -dchmod +x setup.sh
./setup.shCheck the logs to see what's wrong:
docker compose logs app --tail 50Common causes:
- Missing environment variables — re-run
./setup.sh - Database not ready yet — wait 30 seconds and check again
If an update fails mid-way, your previous version is still running safely. To manually recover:
# Check what went wrong
docker compose logs app --tail 100
# Rebuild from current state
docker compose up --build -d# ⚠️ This deletes ALL data (database, uploaded files, configuration)
docker compose down -v
rm .env
./setup.sh
docker compose up -dThe software is completely free. Your only costs are server hosting ($5–15/month for most teams) and a domain name (~$12/year, optional).
Yes. Docker Desktop runs on Mac, Windows, and Linux. Reqcore works fine on a laptop for small teams or evaluation purposes. For production use with a team, a dedicated server is recommended so the system stays online when your laptop is off.
Unlimited. There are no per-seat limits in self-hosted Reqcore.
Not by default. See the Backups & Data Safety section for automated backup instructions. The UI provides a one-click backup button before updates.
Reqcore uses standard PostgreSQL. If you can export your data as CSV or JSON from your current ATS, you can import it using standard database tools. We're working on import wizards for popular ATS platforms.
Go to Settings → Updates in your Reqcore dashboard. The page automatically checks for new versions and shows you exactly what changed, with a one-click update button.
Updates are designed to be safe — database migrations are tested before release, and the update process is designed to fail gracefully. If something does go wrong:
- Your previous data is always preserved in Docker volumes
- You can restore from a backup (create one before updating)
- You can roll back:
git checkout v1.0.0 && docker compose up --build -d
Yes. Back up your data (database dump + MinIO files + .env), install Docker on the new server, clone Reqcore, restore your backups, and start the containers. All your data moves with you.
For initial setup: basic familiarity with opening a terminal and copying commands is helpful. After that, day-to-day management (including updates) can be done entirely from the web UI through Settings → Updates.
Yes. Each instance needs its own directory, .env file, and ports. Change the port mapping in docker-compose.yml (e.g., 8080:3000 for the second instance).
- GitHub Issues: github.com/reqcore-inc/reqcore/issues
- Discussions: github.com/reqcore-inc/reqcore/discussions
- Documentation: This guide and the project README
For those interested in what's running under the hood:
┌─────────────────────────────────────────────────┐
│ Your Server │
│ │
│ ┌────────────┐ ┌──────────┐ ┌─────────────┐ │
│ │ Reqcore │ │PostgreSQL│ │ MinIO │ │
│ │ App │ │ 16 │ │ (S3 Storage)│ │
│ │ :3000 │ │ :5432 │ │ :9000/:9001│ │
│ └─────┬──────┘ └────┬─────┘ └──────┬──────┘ │
│ │ │ │ │
│ └──────────────┴───────────────┘ │
│ Docker Network (internal) │
│ │
│ Only port 3000 is exposed externally │
└─────────────────────────────────────────────────┘
| Component | Purpose | Storage |
|---|---|---|
| Reqcore App | Web application (Nuxt 4, Node.js) | Stateless (rebuilt on update) |
| PostgreSQL 16 | All application data (jobs, candidates, pipeline) | postgres_data Docker volume |
| MinIO | Uploaded documents (resumes, cover letters) | minio_data Docker volume |
Data lives in Docker volumes, which persist across container restarts and rebuilds. The application container is stateless and can be rebuilt at any time without data loss.
| Task | How | Difficulty |
|---|---|---|
| Install | Clone + ./setup.sh + docker compose up |
Easy (5 min) |
| Update (UI) | Settings → Updates → Click "Update" | Easy (2 min) |
| Update (CLI) | git pull + docker compose up --build -d |
Easy (2 min) |
| Backup | Settings → Updates → "Create backup" | Easy (1 click) |
| Custom domain | Add reverse proxy (Caddy recommended) | Medium (15 min) |
Add Resend API key to .env |
Easy (5 min) | |
| Monitor | Settings → Updates → System Health | Built-in |
Self-hosting Reqcore is designed to be straightforward for anyone comfortable with downloading software and opening a web browser. The built-in update system, backup tools, and health dashboard mean you rarely need to touch the command line after initial setup.