Skip to documentation
HMS MiniDocumentation

Self-host HMS Mini

This guide takes a fresh checkout to a working HR workspace. Replace every example hostname, password, bucket name and email address with your own. No live credentials are included in this guide.

Configured production domains

ServiceHTTPS addressPrivate configuration
Static frontendhttps://hrmsminiapp.giraffai.inhms-mini-next-js/.env.prod
NestJS backendhttps://hrmsminiapi.giraffai.inhms-mini-nest-js/.env.prod

The prepared private files are local-only and Git-ignored. Copy them securely to the corresponding host/project directory. The backend reuses the configured service connections and has a separate production JWT secret, secure cookies, NODE_ENV=production, trusted-proxy support, and the frontend's exact HTTPS CORS origin. No cookie Domain override is needed: API-host cookies work with the existing credentialed requests between these same-site subdomains.

For direct Node/static hosting, use Node.js 24:

cd hms-mini-nest-js
npm ci --include=dev
npm run build
npm run db:migrate:prod
npm run storage:cors:prod
npm run start:prod:env

In the frontend checkout:

npm ci --include=dev
npm run build:prod

Host out/ at the frontend domain, preserving directory index routing. The explicit production command loads .env.prod before building; Next.js does not automatically load that custom filename. The API URL is embedded at build time. The API process also needs explicit environment loading or hosting-platform environment variables.

For the full Docker workspace:

docker compose -f docker-compose.prod.yml up --build -d

This publishes the web service on 127.0.0.1:3000 and API on 127.0.0.1:3001. Point both domain DNS records to the server and configure an HTTPS reverse proxy. For example, these Caddy site blocks route each domain to its service:

hrmsminiapp.giraffai.in {
    reverse_proxy 127.0.0.1:3000
}
hrmsminiapi.giraffai.in {
    reverse_proxy 127.0.0.1:3001
}

On a container hosting platform, configure those domains on their respective apps. Supply the frontend build argument NEXT_PUBLIC_API_URL=https://hrmsminiapi.giraffai.in and the backend environment from .env.prod. Production frontend images using an absolute API URL do not require an api Docker DNS entry. DNS/TLS and deployment are performed on your host; creating these files does not deploy the domains.

Both Dockerfiles explicitly install development dependencies in the build stage with npm ci --include=dev. This keeps Tailwind, TypeScript and the Nest CLI available even when Coolify injects NODE_ENV=production or npm is configured to omit development dependencies. The final frontend image contains only nginx and the static export; the API runtime installs production dependencies only. Use the Dockerfile build pack with container port 80 for the frontend and 3001 for the backend. After updating the source, redeploy the latest main commit in each app.

The sections below also describe a generic fresh installation and the default local/same-origin Compose setup.

1. Prepare the services

Use Node.js 24 with npm, Docker Engine and Docker Compose. Keep both project folders beside this guide. The Compose file runs the API and static website; it does not provision the external services below.

  • PostgreSQL: an empty database with a dedicated account allowed to create schemas, tables and indexes. Migrations create hms_admin and hms_hr.
  • Redis: a private, authenticated instance for sessions and login rate limits.
  • RabbitMQ: a dedicated user and virtual host with permission to declare queues, publish and consume. Match amqp:// or amqps:// to your broker listener; a port number alone does not enable TLS. Percent-encode credentials and virtual-host names in connection URLs.
  • Two private AWS S3 buckets: one staging bucket and one primary bucket in the same configured region. Choose globally unique names. Keep public access blocked. The API identity needs bucket lookup and object read/write access for this pipeline, including reading staging objects and writing primary copies. Running the CORS setup command also requires reading and updating the staging bucket CORS configuration. Versioned buckets need permission to read object versions.
  • SMTP, optional: a sender account permitted to authenticate and send mail. Use your provider's SMTP/app password. The supplied example uses Zoho India with implicit TLS on port 465; substitute your provider's settings.
  • Location PostgreSQL, optional: reserved for future location features. Core HR does not query it. The full infrastructure check reports it as unconfigured if omitted; location/vector search is not implemented.

Keep these services reachable from the API host. Inside Docker, localhost refers to that container, not your computer or another service. Use service DNS names or reachable hostnames.

2. Create private configuration

Run from the checkout root on a fresh installation. If configuration already exists, edit it rather than overwriting it.

cp hms-mini-nest-js/.env.example hms-mini-nest-js/.env
chmod 600 hms-mini-nest-js/.env
cp hms-mini-next-js/.env.example hms-mini-next-js/.env.local

Edit the backend .env. Use .env.example for the complete supported settings. A typical production configuration includes:

PORT=3001
WEB_ORIGIN=https://hr.example.com
DATABASE_URL=postgresql://hr_user:REPLACE_PASSWORD@db.example.com:5432/hr
REDIS_URL=redis://default:REPLACE_PASSWORD@redis.example.com:6379/0
ADMIN_JWT_SECRET=REPLACE_WITH_GENERATED_RANDOM_SECRET
ADMIN_SESSION_TTL_SECONDS=3600
ADMIN_COOKIE_SECURE=true
TRUST_PROXY=true
ORG_TIMEZONE=Asia/Kolkata

AWS_REGION=ap-southeast-2
AWS_ACCESS_KEY_ID=REPLACE_ACCESS_KEY
AWS_SECRET_ACCESS_KEY=REPLACE_SECRET_KEY
S3_BUCKET_STAGING=your-unique-hr-staging
S3_BUCKET_PRIMARY=your-unique-hr-primary
S3_SIGNED_URL_TTL_SECONDS=300
UPLOAD_MAX_BYTES=26214400

AMQP_URL=amqps://hr_user:REPLACE_PASSWORD@mq.example.com:5671/%2F
AMQP_PROMOTION_QUEUE=hms-mini.files.promote
FILE_WORKER_ENABLED=true

EMAIL_ENABLED=true
EMAIL_WORKER_ENABLED=true
_APP_SMTP_HOST=smtp.zoho.in
_APP_SMTP_PORT=465
_APP_SMTP_SECURE=true
_APP_SMTP_USERNAME=noreply@example.com
_APP_SMTP_PASSWORD=REPLACE_SMTP_PASSWORD
EMAIL_FROM_NAME=HMS Mini
EMAIL_MAX_ATTEMPTS=5
AMQP_EMAIL_QUEUE=hms-mini.email.deliver

Generate the JWT secret locally and paste it into the backend environment:

node -e "console.log(require('node:crypto').randomBytes(48).toString('hex'))"

For local HTTP, use WEB_ORIGIN=http://localhost:3000 and ADMIN_COOKIE_SECURE=false. The browser must use this exact origin, including scheme and port; do not include a path or trailing slash. Direct local API development uses TRUST_PROXY=false. The supplied Compose stack overrides it to true behind its nginx proxy.

NEXT_PUBLIC_API_URL is the only frontend environment variable. Direct local development uses http://localhost:3001. Docker excludes .env.local and uses the default /api proxy. Never put service credentials or JWT secrets into a NEXT_PUBLIC_ variable. Browser configuration is fixed when the static site is built.

If you do not want email, set EMAIL_ENABLED=false. In-app notifications still work. Do not disable workers while expecting queued uploads or emails to finish.

3. Initialize the database and administrator

cd hms-mini-nest-js
npm ci
npm run db:migrate
npm run admin:create -- --email admin@example.com
npm run storage:cors
npm run smtp:check
cd ..

The administrator command securely prompts for your own password. Save it privately. Existing accounts are preserved unless you explicitly use --reset-password, which resets the password and revokes their sessions. No default production password is supplied. The bootstrap administrator is not an employee; onboard a separate employee record when testing that portal.

storage:cors allows the exact WEB_ORIGIN on the staging bucket and preserves unrelated CORS rules. Repeat it after changing your domain. smtp:check verifies the TLS connection and authentication without sending an email; skip it if email is disabled. npm run services:check checks PostgreSQL, location PostgreSQL, Redis, both buckets and AMQP without printing credentials.

4. Start the Docker deployment

docker compose up --build -d
docker compose ps
docker compose logs --tail=100 api web

The web container listens on host port 3000. It serves the static Next.js export through nginx and forwards /api/ to the internal API port 3001. PostgreSQL migrations run automatically before the API starts; the web container waits for API health. Background upload and email workers run inside the API process.

For local use, open http://localhost:3000/. For production, point your domain to the host and put an HTTPS reverse proxy in front of port 3000. Forward all paths, including /api/, to this web service. Bind the Compose web port to 127.0.0.1:3000:80 if the HTTPS proxy runs on the same host, or restrict it to your trusted proxy network. The API must remain unexposed because it trusts the supplied nginx proxy. Configure the proxy's upload/body limit for regular JSON API requests; file bytes go directly to S3.

After configuring TLS, use your HTTPS origin in .env, enable secure cookies, reapply staging CORS and recreate the API with docker compose up -d. A website served over HTTPS must not call an HTTP API. Same-origin /api avoids that mismatch.

Open /admin/login/, /employee/login/ and /docs/ on your domain. Sign in, create departments/designations/shifts, onboard an employee, and test password change, a photo punch, leave/regularization approval and task assignment. These browser/device acceptance checks are performed by the owner.

5. Understand files and email

Uploads always go staging → backend completion verification → RabbitMQ → primary. Staging and primary are successive steps, not development/production alternatives. Files become usable after the worker verifies the copy. Keep the staging retention period longer than your expected queue delays and retries; staged originals are retained unless you configure a lifecycle policy.

HR actions save in-app notifications and email outbox rows in the same database transaction. The worker confirms RabbitMQ publication and sends through SMTP. Temporary failures retry with backoff; permanent failures or exhausted attempts appear in Admin → Email delivery. Viewing requires settings.view; retrying requires settings.edit. Emails cover onboarding, task assignment/reassignment, leave and regularization requests/decisions, and password-change/reset notices. Passwords are never included.

“Sent” means the SMTP server accepted the email, not that it reached the inbox. Configure the sender domain's SPF, DKIM and DMARC with your provider. Delivery is at least once: a crash after SMTP acceptance but before database commit can resend a message with the same Message-ID. A disabled or changed recipient account cancels a pending delivery.

6. Updates, backups and recovery

Back up PostgreSQL and uploaded primary objects according to your retention requirements. Preserve the private environment separately. RabbitMQ durable storage and Redis persistence should be managed with their hosting services; the database outbox can republish unacknowledged work. Test restoring backups before depending on them.

Before updating, review /docs/changelogs/, back up data and synchronize documentation. From the frontend run npm run docs:sync, then from the checkout root run docker compose up --build -d. Pending migrations run on startup. Rebuild the frontend after changing browser-visible configuration. For a standalone frontend Docker context, keep lib/docs-content.json synchronized and include it in the source checkout.

For rollback, restore compatible application images and database backups together when schema changes require it. docker compose down stops this application's containers; it does not erase the external databases or S3 objects.

For Redis diagnostics, run npm run redis:check from the backend folder. It reports latency from that machine, health counters and a bounded session-expiry sample without exposing keys or values. Place the API, PostgreSQL and Redis in the same region/private network where possible; remote network latency affects each authenticated request. This diagnostic is read-only and does not benchmark load capacity.

Troubleshooting

  • CORS or sign-in fails: compare the browser's exact origin with WEB_ORIGIN; check secure cookies match HTTPS; check /api/ reaches the API and Redis is reachable.
  • Uploads remain queued: check both bucket permissions, the broker URL, FILE_WORKER_ENABLED and API logs. Confirm the uploaded object has not expired under a bucket lifecycle policy.
  • Email remains queued: check EMAIL_ENABLED, EMAIL_WORKER_ENABLED and RabbitMQ. Use smtp:check to diagnose authentication without sending mail; retry failed items from Email delivery after fixing the settings.
  • Pages are missing: rebuild the frontend, confirm nginx serves out/, and preserve trailing-slash directory routing. This project requires a running API even though its frontend is static.
  • API is unhealthy: inspect API logs for migration or environment validation failures and confirm the database account can create/update the schemas. Do not paste credentials into shared logs or screenshots.

For direct local development and isolated API checks, see the README. For the implemented product scope and limitations, see Features.

Documentation overview