A slim, one-command setup for the Dino backend stack. Instead of hand-editing
.env, nginx.conf and TLS certs (and keeping their domains in sync), you run
setup.sh once: it asks for a single base domain, auto-generates every
secret, and renders a consistent configuration ready for docker compose up -d.
# 1. Clone
git clone git@github.com:gnucoop/dino-install.git
cd dino-install
# 2. Generate a deployment into a folder of your choice, and answer the prompts
./setup.sh my-dino
# 3. Start the stack
cd my-dino
docker compose up -d # or: docker-compose up -d
# 4. Open the app in your browser at the Dino URL
# https://dino.<BASE_DOMAIN>
# (the exact URL + admin email/password are printed at the end of setup.sh)See Requirements first. For
selfsignedTLS you need mkcert, and the subdomains must resolve (setup.shoffers to add the/etc/hostsentries for you).
Services: nginx (TLS termination + reverse proxy), dinoapp (the Dino SPA,
served from the devgnucoop/dinoapp image), PostgreSQL (pgvector),
Hasura GraphQL, nhost Auth, MinIO (S3-compatible storage) and
nhost Storage. Two short-lived helpers run on first boot: createbucket
(creates the default MinIO bucket) and postinstall (seeds initial data once the
schema exists — see below).
flowchart TB
U(["👤 Browser"])
subgraph PROXY["Edge"]
N["🔒 nginx<br>TLS + reverse proxy<br>:80 / :443"]
end
subgraph APP["Application"]
D["dinoapp · SPA<br>devgnucoop/dinoapp<br>:8090"]
G["Hasura GraphQL<br>nhost/graphql-engine<br>:9000"]
A["Auth<br>nhost/auth<br>:9001 · /v1"]
S["Storage<br>nhost/storage<br>:9002"]
M["MinIO · S3<br>:9006 API / :32765 console"]
end
subgraph DATA["Data"]
P[("PostgreSQL<br>+ pgvector")]
end
subgraph BOOT["First-boot one-shots"]
CB["createbucket<br>minio/mc"]
PI["postinstall<br>psql · postinstall.sql"]
end
NA[["nhost_apps<br>metadata · migrations · emails"]]
IS[["init.sql · DB seed"]]
U -->|"HTTPS · *.BASE_DOMAIN"| N
N -->|"dino."| D
N -->|"hasura."| G
N -->|"auth."| A
N -->|"storage."| S
N -->|"minio. / minio-console."| M
G --> P
A --> P
A -.-> G
S --> P
S -.-> G
S -->|"S3"| M
NA -. "mount" .-> G
IS -. "mount" .-> P
CB -->|"creates default bucket"| M
PI -->|"seeds roles + admin user"| P
Request flow: the browser hits https://<service>.<BASE_DOMAIN>; nginx
terminates TLS and reverse-proxies each subdomain to the right service. Hasura,
Auth and Storage all talk to Postgres; Storage keeps objects in MinIO; Hasura
applies the nhost_apps migrations/metadata; Postgres is seeded from init.sql.
On first boot createbucket creates the default bucket and postinstall seeds
the roles + admin login, then both exit.
After Hasura and Auth have applied their migrations, the postinstall service
seeds initial data once (idempotent — it only runs when the tables are empty):
- the four roles:
admin,supervisor,collector,guest - an
Adminuser group (full permissions) - a web login user in
auth.userswhose email/password are theADMIN_EMAIL/ADMIN_PASSWORDfrom setup, display nameUSER_DISPLAY_NAME - that user's
admin+userrole mapping and a linkeduser_datarow - an anonymous
user_datarow
So after docker compose up -d you can log into the SPA at https://dino.<domain>
with the admin email + password shown at the end of setup.sh.
Pandino (AI backend), the chatbot proxy and the welcome site are intentionally excluded from this slim kit.
setup.sh checks all of these up front and aborts with a clear message if any
are missing:
| Tool | Purpose |
|---|---|
| Docker (daemon running) | runs the stack |
| Docker Compose | docker compose plugin or legacy docker-compose |
| openssl | generates secrets and self-signed certificates |
| git | clones nhost_apps |
| bash (3.2+) / sed | run and render the installer |
| Tool | When | Purpose |
|---|---|---|
| mkcert | required for selfsigned |
installs a local CA so browsers trust the certs. setup.sh aborts in selfsigned mode if it is missing — a plain openssl self-signed cert is untrusted and breaks cross-origin auth/GraphQL/WebSocket, so there is no fallback. |
| certbot | recommended for letsencrypt |
auto-issues Let's Encrypt certs. If absent, the script prints the exact command to run. |
- git access to the
nhost_appsrepo (Hasura metadata / migrations / emails).setup.shclones it into a realnhost_apps/folder — the installer never depends on any file outside itself. Default is SSH (git@github.com:gnucoop/nhost_apps.git, branchprod-1.21.0-permissions-m2); override withSETUP_NHOST_APPS_REPO/SETUP_NHOST_APPS_BRANCH. - Free host ports:
80,443(nginx),5432(postgres),8090(SPA),9000/9001/9002(hasura/auth/storage),9006/32765(MinIO API/console).setup.shverifies all of these are free and aborts (listing the offenders) if any are in use — bypass withSETUP_SKIP_PORT_CHECK=1. - Name resolution for the six subdomains (see the table below):
- local install →
setup.shprints the/etc/hostslines and can append them; - public install → real DNS A/AAAA records pointing at the host.
- local install →
- The SPA ships as the
devgnucoop/dinoappimage (no Node/Angular build needed here). If you build your own image, itsenvironment.tsmust definedataConfig.socketJwtExpiredCode(e.g.4403) — otherwise the app hangs on "Initializing data…". SetDINOAPP_VERSIONin.envto pick the tag.
init.sql (DB seed) and postinstall.sql (first-boot data) ship committed in
this folder and are copied into each deployment automatically.
./setup.sh # generate in place
./setup.sh ../my-dino # generate a ready-to-run deployment in ../my-dino
docker compose up -d # (run from the folder setup.sh wrote to)setup.sh [TARGET_DIR] writes .env, nginx.conf, TLS certs/, a copy of
docker-compose.yml, copies init.sql + postinstall.sql, and clones
nhost_apps/ into TARGET_DIR (default: this folder). This lets you keep the
installer as read-only source and
materialize each deployment into its own directory.
You'll be asked for a base domain (e.g. local.dinoapp.cloud or
dino.example.com), an admin email/password (blank = auto-generated), and a
TLS mode. All service hostnames are derived from the base domain:
| Service | Hostname |
|---|---|
| App (SPA) | dino.<BASE_DOMAIN> |
| Auth | auth.<BASE_DOMAIN> |
| Hasura | hasura.<BASE_DOMAIN> |
| Storage | storage.<BASE_DOMAIN> |
| MinIO API | minio.<BASE_DOMAIN> |
| MinIO console | minio-console.<BASE_DOMAIN> |
- selfsigned — generates a locally-trusted wildcard cert via mkcert
(required; no browser warnings). The script prints the
/etc/hostslines you need and can append them for you. - letsencrypt — runs
certbotif installed (needs public DNS + port 80), otherwise prints the exact command. - provided — bring your own
certs/fullchain.pem+certs/privkey.pem.
Every prompt has a SETUP_<NAME> override; set SETUP_NONINTERACTIVE=1 to skip
prompting entirely:
SETUP_NONINTERACTIVE=1 \
SETUP_BASE_DOMAIN=dino.example.com \
SETUP_ADMIN_EMAIL=admin@example.com \
SETUP_TLS_MODE=letsencrypt \
./setup.shBefore writing anything, setup.sh:
- Refuses to overwrite an existing deployment — if the target folder already
contains a
docker-compose.yml(or, for an in-place run, a generated.env), it aborts rather than regenerate secrets and break a running stack. Override withSETUP_FORCE=1. - Checks all required host ports are free and aborts listing any in use.
Override with
SETUP_SKIP_PORT_CHECK=1.
.env, nginx.conf, certs/ and a cloned nhost_apps/ — all git-ignored.
Re-running setup.sh backs up any existing .env to .env.backup.
Which folder? Everything above happens in this folder — the installer source. Everything below happens in your deployment folder: the
TARGET_DIRyou passed tosetup.sh(e.g.my-dino/), the one that holds the generated.env,nginx.conf,certs/,nhost_apps/,data/andminio/storage/. Do not re-runsetup.shto update — it would regenerate every secret and break the running stack (which is exactly why the existing-deployment guard aborts).
cd my-dino # your deployment folder, NOT the installer
# 0. Back up first
docker compose down
cp -a ../my-dino ../my-dino.backup-$(date +%F)
# 1. Pull DB updates (Hasura metadata + migrations)
git -C nhost_apps pull
# 2. Bump the image tags you want in .env
# DINOAPP_VERSION=... (and HASURA_VERSION, HASURA_AUTH_VERSION,
# HASURA_STORAGE_VERSION, POSTGRES_VERSION, MINIO_VERSION, NGINX_VERSION)
# 3. Fetch the new images and restart
docker compose pull
docker compose up -dAll state lives inside the folder — Postgres in ./data, MinIO objects in
./minio/storage, and your unrecoverable secrets in .env. Copying the whole
folder (with the stack stopped, so Postgres is consistent) is a complete backup
and the only rollback you get.
New Hasura migrations and metadata arrive through the nhost_apps/ clone:
git -C nhost_apps pullThe Hasura image is the cli-migrations variant, so it applies any new
migrations and reloads metadata automatically on startup — there is nothing to
run by hand. postinstall.sql is idempotent and will no-op on an existing
install, so your seeded roles and admin user are left alone.
If the clone was made shallow (
--depth 1) andgit pullcomplains, usegit -C nhost_apps fetch --depth 1 origin <branch> && git -C nhost_apps reset --hard FETCH_HEAD.
Edit the deployment's .env and set the new tag(s). DINOAPP_VERSION is the
usual one; the same applies to any other component you're moving:
| Variable | Component |
|---|---|
DINOAPP_VERSION |
Dino SPA (devgnucoop/dinoapp) |
HASURA_VERSION |
Hasura GraphQL engine |
HASURA_AUTH_VERSION |
nhost Auth |
HASURA_STORAGE_VERSION |
nhost Storage |
POSTGRES_VERSION |
Postgres / pgvector |
MINIO_VERSION / MC_VERSION |
MinIO + client |
NGINX_VERSION |
edge nginx |
Change tags only — leave the generated secrets, domains and ports untouched.
Bumping POSTGRES_VERSION across a major release is a data migration, not a
restart: dump first.
docker compose down
docker compose pull # required if a tag is 'latest' or already cached
docker compose up -d
docker compose logs -f graphql # watch the migrations applydocker compose pull matters: with a floating tag like latest, up -d reuses
the image already on disk and your "update" silently does nothing. Never pass
-v to down — the stack uses bind mounts, but the habit is what destroys data
elsewhere.