Installing on a server
This page is for administrators who install Celestian themselves. Application users do not need to read it.
Architecture in brief
Section titled “Architecture in brief”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.
1. Server requirements
Section titled “1. Server requirements”- Linux with Docker Engine and Compose v2. The architecture must match the images (
linux/amd64by 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.
2. Prepare the folder and secrets
Section titled “2. Prepare the folder and secrets”cd ~/celestian/deploy./scripts/gen-secrets.sh # creates .env with random secrets3. Fill in deploy/.env
Section titled “3. Fill in deploy/.env”The important values (the full list is in Configuration):
# ImagesAPI_IMAGE=<registry>/celestian-api:v1.0.0WEB_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 proxyWEB_BIND=127.0.0.1WEB_PORT=3100
# Persistent data (an absolute path is recommended)DATA_DIR=/srv/celestian-data
# E-mailSMTP_HOST=smtp.example.com # exactly as in the SMTP server certificate nameSMTP_PORT=587SMTP_SECURITY=starttlsSMTP_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.0MQTT_PORT=1883MQTT5_PORT=1884BOOTSTRAP_BROKER_URL=mqtt://celestian.example.com:1883
# Only to create the first account; turn it off again afterwardsSIGNUP_ENABLED=true- A
PUBLIC_BASE_URLwith thehttps://scheme makes the session cookie Secure, so sign-in only works over HTTPS. Testing throughhttp://IP:3100directly cannot sign in. - If an MQTT port clashes with another service, change
MQTT_PORT/MQTT5_PORTand match the port inBOOTSTRAP_BROKER_URLso the code examples in the Credentials tab are correct.
4. Configure the reverse proxy
Section titled “4. Configure the reverse proxy”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.
5. Run
Section titled “5. Run”-
Terminal window cd ~/celestian/deploydocker compose pulldocker compose up -ddocker compose ps - Check:
Terminal window curl -I http://127.0.0.1:3100/login # must be 200 - Open
https://celestian.example.com, sign up the first account (needsSIGNUP_ENABLED=true), then turn registration off (SIGNUP_ENABLED=false) and rundocker compose up -d.
- The first startup sometimes fails at
db-init(a TimescaleDB race): rundocker compose up -donce more. - A “cannot connect to the broker yet” warning at startup is normal.
- Database migrations run automatically when
apistarts.
6. First test
Section titled “6. First test”- Sign in and create an account (then turn registration off).
- 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. - Create a Channel, a Device (save the key), and a connection.
- Publish over MQTT (
client_idmust start with the device ID under theprefixpolicy). - Create a Channel Subscriber → Internal DB rule, then look in Messages and Dashboards.
Testing SMTP on its own
Section titled “Testing SMTP on its own”docker compose run --rm --no-deps api smtp-test recipient@example.comIt 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.
Update and rollback
Section titled “Update and rollback”# change API_IMAGE and WEB_IMAGE in .env to the new version, thendocker compose pull && docker compose up -dUse 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.
Troubleshooting
Section titled “Troubleshooting”| 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. |
SMTP diagnosis
Section titled “SMTP diagnosis”docker compose run --rm --no-deps api smtp-test <address>uses the same sending code as the application.- 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. - Try another port (587 with
SMTP_SECURITY=starttls).