Skip to content

Installing on a server

This page is for administrators who install Celestian themselves. Application users do not need to read it.

Compose runs the following services:

Service Purpose
web The web application (Next.js) and the bridge (BFF) to the backend. HTTP on WEB_BIND:WEB_PORT (127.0.0.1:3100 by default).
api The Go backend. One binary with several roles: api, brokerauth, presence, rules, reports (all run together by default).
fluxmq The MQTT broker, with an auth callout to api (anonymous clients are refused).
postgresql Metadata (PostgreSQL 18).
timescaledb Time-series messages (TimescaleDB, a separate instance).
redis Cache, lockout, counters, duplicate markers.
gotenberg PDF rendering for reports (an internal network without outbound access). Optional.
caddy Optional (the caddy profile): a TLS reverse proxy if the server has none of its own.
data-init, db-init Prepare data folders, roles, and databases at startup.

The web application never exposes the backend publicly. The browser only talks to the web tier; the web tier talks to the backend.

  • Linux with Docker Engine and Compose v2. The architecture must match the images (linux/amd64 by default).
  • Outbound access to the registry where the images are.
  • A reverse proxy (nginx or another) that handles TLS on port 443 and forwards HTTP to 127.0.0.1:3100.
  • The MQTT ports (1883/1884, MQTTS 8883, or your choice) open when devices connect from outside. The application HTTP port does not need to be open to the public; only the proxy accesses it.
  • SMTP (STARTTLS 587 or implicit TLS 465) for e-mail verification, password reset, invitations, and notifications.
Terminal window
cd ~/celestian/deploy
./scripts/gen-secrets.sh # creates .env with random secrets

The important values (the full list is in Configuration):

Terminal window
# Images
API_IMAGE=<registry>/celestian-api:v1.0.0
WEB_IMAGE=<registry>/celestian-web:v1.0.0
# The public address (the URL users see through the proxy; no port, https)
PUBLIC_BASE_URL=https://celestian.example.com
# The web tier is exposed as local HTTP for the proxy
WEB_BIND=127.0.0.1
WEB_PORT=3100
# Persistent data (an absolute path is recommended)
DATA_DIR=/srv/celestian-data
# E-mail
SMTP_HOST=smtp.example.com # exactly as in the SMTP server certificate name
SMTP_PORT=587
SMTP_SECURITY=starttls
SMTP_USER=...
SMTP_PASSWORD=...
SMTP_FROM="Celestian <no-reply@example.com>"
# MQTT straight to the broker (not through the HTTP proxy)
MQTT_BIND=0.0.0.0
MQTT_PORT=1883
MQTT5_PORT=1884
BOOTSTRAP_BROKER_URL=mqtt://celestian.example.com:1883
# Only to create the first account; turn it off again afterwards
SIGNUP_ENABLED=true
  • A PUBLIC_BASE_URL with the https:// scheme makes the session cookie Secure, so sign-in only works over HTTPS. Testing through http://IP:3100 directly cannot sign in.
  • If an MQTT port clashes with another service, change MQTT_PORT/MQTT5_PORT and match the port in BOOTSTRAP_BROKER_URL so the code examples in the Credentials tab are correct.

An nginx example (TLS termination, forward to the web tier):

server {
listen 80;
server_name celestian.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name celestian.example.com;
ssl_certificate /etc/letsencrypt/live/celestian.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/celestian.example.com/privkey.pem;
client_max_body_size 10m; # CSV uploads, profile photos, firmware
# Configuration backup restore: files up to 64 MB
location /api/bff/backup/ {
client_max_body_size 70m;
proxy_pass http://127.0.0.1:3100;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_request_buffering off;
proxy_read_timeout 300s;
}
location / {
proxy_pass http://127.0.0.1:3100;
proxy_http_version 1.1;
proxy_set_header Host $host; # Next.js Server Actions match Origin against Host
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_buffering off; # Live (SSE) in Messages
proxy_read_timeout 3600s;
}
}

Key points: forward Host, X-Forwarded-Host, and X-Forwarded-Proto; turn off buffering so Live mode (SSE) works; raise the upload limit for backup restores. After changing: nginx -t && nginx -s reload.

  1. Terminal window
    cd ~/celestian/deploy
    docker compose pull
    docker compose up -d
    docker compose ps
  2. Check:
    Terminal window
    curl -I http://127.0.0.1:3100/login # must be 200
  3. Open https://celestian.example.com, sign up the first account (needs SIGNUP_ENABLED=true), then turn registration off (SIGNUP_ENABLED=false) and run docker compose up -d.
  • The first startup sometimes fails at db-init (a TimescaleDB race): run docker compose up -d once more.
  • A “cannot connect to the broker yet” warning at startup is normal.
  • Database migrations run automatically when api starts.
  1. Sign in and create an account (then turn registration off).
  2. Test e-mail: request a password reset from the sign-in page. If the e-mail does not arrive, see docker compose logs api | grep -i smtp.
  3. Create a Channel, a Device (save the key), and a connection.
  4. Publish over MQTT (client_id must start with the device ID under the prefix policy).
  5. Create a Channel Subscriber → Internal DB rule, then look in Messages and Dashboards.
Terminal window
docker compose run --rm --no-deps api smtp-test recipient@example.com

It prints the host, port, and mode, then sends one test e-mail and shows the actual error if it fails (DNS, timeout, certificate, login refused, sender not allowed). You can override the port temporarily: -e SMTP_PORT=587 -e SMTP_SECURITY=starttls.

Terminal window
# change API_IMAGE and WEB_IMAGE in .env to the new version, then
docker compose pull && docker compose up -d

Use explicit version tags, not latest, so a rollback is easy: restore the old tag and docker compose up -d. Migrations are not undone automatically, so make a backup before an update that contains new migrations.

Symptom Common cause
Sign-in succeeds and then you are immediately signed out, or forms do not respond PUBLIC_BASE_URL does not equal the opened URL, or the proxy does not forward Host and X-Forwarded-*.
Repeated redirects An HTTP request reaches Caddy (the caddy profile is still active) or the proxy points to the Caddy port; Cloudflare with Flexible SSL also triggers it (use Full).
Sign-up: “Server cannot be reached” SMTP is stuck. Test with smtp-test.
E-mail is not sent SMTP_HOST does not match the SMTP server certificate.
A device is refused by the broker client_id does not start with the device ID, or the device is not connected to the channel.
pull access denied The image repository is private: docker login on the server.
Slow e-mail delivery Usually a greeting delay or reverse DNS on the mail server. Registration, reset, and invitations are sent in the background.
  1. docker compose run --rm --no-deps api smtp-test <address> uses the same sending code as the application.
  2. If the TLS handshake hangs even though the port connects, compare clients (openssl s_client) and suspect the MTU (ping -M do -s 1472), a firewall that filters the ClientHello, or connection throttling on the mail server.
  3. Try another port (587 with SMTP_SECURITY=starttls).