Hinatadocs

Production deployment

This walkthrough takes you from a clean host to a running, health-checked instance behind your reverse proxy. It uses the prod profile: a MongoDB replica set with TLS and X.509 client authentication.

Read the self-hosting overview first. What each setting means is in the Configuration reference.

Prerequisites

  • A Linux host with Docker Engine and the Docker Compose plugin (docker compose, v2).
  • openssl, keytool (from a JRE/JDK) and a POSIX shell for the scripts in deploy/.
  • Two DNS names: one for the API, one for the web app. This page uses api.track.example.com (API) and track.example.com (web).
  • A reverse proxy terminating HTTPS (Nginx, Caddy, Traefik, a NAS reverse proxy, …). See Reverse proxy & TLS.
  • An SMTP relay for outbound mail. See E-mail & SMTP.

1. Get the server repository

git clone https://github.com/hinata-platform/hinata-server.git
cd hinata-server

Later updates are a git pull in this directory. The images come prebuilt from GHCR, so you never build on the host.

2. Create your .env

cp .env.example .env

.env.example is fully commented. Every value also works as a plain environment variable on the container.

3. Generate secrets

./deploy/generate-secrets.sh

The script:

  • creates deploy/mongo-keyfile (the replica set's internal auth keyfile) if it does not exist yet.
  • prints random values for HINATA_JWT_SECRET, MONGO_ROOT_PASSWORD and MINIO_ROOT_PASSWORD.

Copy the values into your .env.

The JWT secret is required in production

HINATA_JWT_SECRET must be a random string of at least 64 characters (HS512). The server refuses to start in prod without it. Without the generator, create one with:

openssl rand -base64 64 | tr -d '\n'

Rotating this secret invalidates every issued token. All users must log in again.

4. Generate the MongoDB X.509 PKI

Production MongoDB uses TLS plus X.509 client authentication, so the connection string holds no password. Generate the CA, the server certificate and the app's client certificate:

./deploy/x509/generate-certs.sh prod

This writes to deploy/x509/prod/:

  • the CA (ca.crt/ca.key)
  • the mongod server cert (server.pem)
  • the app's JVM keystore (hinata-app.p12)
  • the truststore (truststore.p12)
  • the replica set keyfile
  • app-subject-dn.txt: the client certificate's subject DN, which becomes the Mongo username

The keystore and truststore passwords default to changeit. Change them and set the matching values in .env:

HINATA_MONGO_TLS_KEYSTORE_PASSWORD=change-me-keystore
HINATA_MONGO_TLS_TRUSTSTORE_PASSWORD=change-me-truststore

Export HINATA_MONGO_TLS_KEYSTORE_PASSWORD and HINATA_MONGO_TLS_TRUSTSTORE_PASSWORD before running the certificate generator, and the PKCS#12 files are built with your passwords from the start. Run it before setting them and you get the default. Full detail on the MongoDB & X.509 page.

5. A realistic .env

A typical production .env. Placeholders read change-me…. Secrets look as if the generator made them, yours will differ. Adjust the hosts.

# Profile
SPRING_PROFILES_ACTIVE=prod

# Public URLs
HINATA_BASE_URL=https://api.track.example.com
HINATA_WEB_BASE_URL=https://track.example.com

# Image tags (pin a release instead of latest for reproducible deploys)
HINATA_SERVER_TAG=10.4.0
HINATA_APP_TAG=10.4.0

# JWT: from ./deploy/generate-secrets.sh
HINATA_JWT_SECRET=Kf3mS0pQ9xR2vN7wY1bZ8cH4dJ6gL5aT0eU3iO2rW9kP1sX4nC7mB6vD8fA2hQ0

# MongoDB SCRAM root (admin/internal only, the app uses X.509)
MONGO_ROOT_USERNAME=hinata
MONGO_ROOT_PASSWORD=9f1c7a4e2b6d8039a5c1e7f2b4d6a8c0
HINATA_MONGO_TLS_KEYSTORE_PASSWORD=change-me-keystore
HINATA_MONGO_TLS_TRUSTSTORE_PASSWORD=change-me-truststore

# Reverse proxy: CIDR the proxy reaches the container from (see step 8)
HINATA_TRUSTED_PROXIES=172.16.0.0/12

# SMTP: a real relay so mail is delivered
HINATA_SMTP_HOST=smtp.example.com
HINATA_SMTP_PORT=587
HINATA_SMTP_USERNAME=hinata@example.com
HINATA_SMTP_PASSWORD=change-me-smtp
HINATA_SMTP_AUTH=true
HINATA_SMTP_STARTTLS=true
HINATA_MAIL_FROM=hinata@example.com

# Object storage: bundled MinIO (set COMPOSE_PROFILES= and HINATA_STORAGE_* /
# HINATA_S3_* / HINATA_AZURE_* instead for AWS S3, GCS or Azure; see the
# Object storage page)
COMPOSE_PROFILES=local-storage
MINIO_ROOT_USER=hinata
MINIO_ROOT_PASSWORD=3b8e0d5f7a2c9146e0b3d7f1a5c8e2b4
HINATA_S3_BUCKET=hinata

# App integration
HINATA_PRIVACY_POLICY_URL=https://example.com/privacy
HINATA_APP_MIN_VERSION=10.4.0
HINATA_CORS_ALLOWED_ORIGINS=https://track.example.com
HINATA_DOCS_ENABLED=false

# Push + deep links: default gateway; override only to run your own
HINATA_GATEWAY_BASE_URL=https://connect.hinata.ahmadre.com

# First run (leave blank to use the in-app wizard)
HINATA_SETUP_AUTO_COMPLETE=false

# Demo seed: NEVER enable in production
HINATA_DEMO_SEED=false
HINATA_DEMO_RESET=false

# Rate limiting / brute force
HINATA_RATE_LIMIT_ENABLED=true
HINATA_RATE_LIMIT_API=300
HINATA_RATE_LIMIT_AUTH=10
HINATA_MAX_LOGIN_FAILURES=5
HINATA_LOGIN_BLOCK_MINUTES=15

# Published host ports (the reverse proxy forwards to these)
HINATA_PORT=3356
HINATA_APP_PORT=3456

Change every default

The stock .env.example ships development defaults: MONGO_ROOT_PASSWORD=hinata-dev-secret, changeit keystore passwords and an empty JWT secret. Any of these left in production is a serious hole. Generate real secrets for all of them.

6. Bring up the stack

Start MongoDB first, so the replica set can initiate and you can register the X.509 user. Then start the rest.

# Start the database nodes
docker compose up -d mongo1 mongo2 mongo-arbiter

# Register the app's X.509 certificate as the $external Mongo user
./deploy/x509/init-prod-user.sh

# Start everything (server + MinIO)
docker compose up -d

To serve the Flutter web app from this host too, add the app overlay:

docker compose -f docker-compose.yml -f docker-compose.app.yml up -d

init-prod-user.sh reads the subject DN from deploy/x509/prod/app-subject-dn.txt. Using the SCRAM root account from .env, it creates a matching $external user with readWrite and dbAdmin on the hinata database. Run it once, after the replica set is healthy.

7. Verify health

The container's own HEALTHCHECK polls this endpoint too:

curl -fsS https://api.track.example.com/actuator/health
# {"status":"UP"}

Before the proxy is wired up, hit the published port directly:

curl -fsS http://localhost:3356/actuator/health

If it is not UP, watch the logs:

docker compose logs -f hinata-server

Mongo auth often fails on first boot. X.509 authentication errors usually mean one of two things:

  • The $external user was not registered. Re-run ./deploy/x509/init-prod-user.sh.
  • The keystore password in .env does not match the one used to build hinata-app.p12.

8. DNS, reverse proxy and ports

The server publishes two host ports. Your reverse proxy terminates TLS and forwards to them:

Public namePurposeForwards to host portEnv var
api.track.example.comREST API + SSE3356HINATA_PORT
track.example.comFlutter web app3456HINATA_APP_PORT

Point both DNS records at the proxy, issue certificates and proxy each hostname to its port. A minimal Nginx sketch (full config on Reverse proxy & TLS):

location / {
    proxy_pass http://127.0.0.1:3356;   # api.track.example.com → server
    proxy_set_header Host              $host;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_buffering off;                # keep SSE streaming
}

Two settings must match your proxy:

  • CORS: HINATA_CORS_ALLOWED_ORIGINS must list the web app's origin (https://track.example.com). The web client calls the API cross-origin. A missing origin shows up as blocked requests in the browser.
  • Trusted proxies: HINATA_TRUSTED_PROXIES is the CIDR the proxy reaches the container from. The server trusts X-Forwarded-For only from those addresses, so rate limiting and logs see the real client IP. Empty means trust nobody. Too wide lets clients spoof their IP.

Keep SSE alive through the proxy

Live updates use Server-Sent Events. Disable response buffering on the API location (proxy_buffering off; in Nginx) or clients get updates late.

9. First run

With the stack healthy, open https://track.example.com (or point a native app at https://api.track.example.com). The setup wizard creates the organization and the first admin. To automate this, for example with infrastructure as code, set the HINATA_SETUP_* variables. See Setup & first run.

Updating and redeploying

An update is a new image tag. Set the tag, pull, and recreate only the app and the server. Leave the data services alone.

# Pin the new release in .env
HINATA_SERVER_TAG=2.3.0
HINATA_APP_TAG=2.3.0

# Pull the new images and recreate only server + app
docker compose pull hinata-server
docker compose up -d hinata-server
# if you serve the web app too:
docker compose -f docker-compose.yml -f docker-compose.app.yml pull hinata-app
docker compose -f docker-compose.yml -f docker-compose.app.yml up -d hinata-app

A redeploy updates only app and server, never Mongo or MinIO

Issues, attachments and users live in the mongo1-data, mongo2-data and minio-data Docker volumes. Recreating or removing the database or storage services (for example a full down -v, or a stack redeploy that prunes volumes) destroys that data. When updating, name the hinata-server and hinata-app services explicitly, as above. Take a backup before any change to the data services. See Backups & upgrades.

Pin tags for reproducible deploys

latest is convenient but changes under you. Pin HINATA_SERVER_TAG and HINATA_APP_TAG to a specific version (e.g. 10.4.0). Every host then runs the same known build, and a rollback is a one-line tag change.

Where to go next