This guide covers a new production deployment of the official Nebulynk repository with Coolify. Use Coolify 4.1.0 or newer and keep Coolify itself up to date. Nebulynk uses Coolify's persistent magic environment variables for generated secrets and its service URL variables for public endpoints. See Coolify's Magic Environment Variables reference for the platform behavior used by this Compose file.
Read Secure Self-Hosting before exposing the instance. The general Docker architecture, backup requirements, and manual deployment path are documented in Self-Hosting.
Prepare four DNS names pointing to the Coolify server. The examples below use:
| Service | Public URL | Purpose |
|---|---|---|
frontend |
https://app.example.com |
Browser interface |
backend |
https://api.example.com |
API, authentication, and WebSockets |
livekit |
https://livekit.example.com |
LiveKit signalling; Nebulynk converts it to wss:// for clients |
garage |
https://files.example.com |
S3-compatible uploads, downloads, and recordings |
LiveKit media does not travel through the HTTP proxy. Allow inbound TCP 7881
and UDP 7882 in both Coolify and the server or provider firewall.
- In the target Coolify project and environment, select New Resource and Public Repository.
- Enter
https://github.com/sapientorius/Nebulynkas the repository. - Select Docker Compose as the build pack and set the Compose file to
/docker-compose.coolify.yml. - Coolify initially selects
main. Change the branch tostablebefore the first deployment. Never deploymainin production. Use an immutablevX.Y.Ztag instead only when the installation must remain pinned to one reviewed release. - Enable automatic deployments only when following the
stablechannel.
Do not start the first deployment until the branch, domains, and optional configuration below have been reviewed.
Enter a domain only for the following services. The port suffix selects the container port; Coolify still serves the public HTTPS endpoint on the normal HTTPS port.
| Coolify domain field | Value |
|---|---|
Domains for frontend |
https://app.example.com:8080 |
Domains for backend |
https://api.example.com:3030 |
Domains for livekit |
https://livekit.example.com:7880 |
Domains for garage |
https://files.example.com:3900 |
Leave the domain fields for postgres, redis, garage-volume-init, and
livekit-egress empty. They are internal services. An empty domain field does
not expose the service through the Coolify proxy.
The Compose stack derives all browser-facing configuration from these domain entries. Do not create or set any of the following variables for a standard Coolify deployment:
COOLIFY_URL_FRONTEND,COOLIFY_URL_BACKEND, orCOOLIFY_URL_LIVEKIT_WSS;STORAGE_S3_PUBLIC_ENDPOINTorLIVEKIT_PUBLIC_URL;VITE_API_URL,VITE_LIVEKIT_URL, orVITE_VAPID_PUBLIC_KEY;PASSKEY_RP_ID; orNEBULYNK_BUILD_SHA,NEBULYNK_BUILD_TIME, orNEBULYNK_UPDATE_PUBLIC_KEYS_JSON.
Coolify supplies SERVICE_URL_FRONTEND, SERVICE_FQDN_FRONTEND,
SERVICE_URL_BACKEND, SERVICE_URL_LIVEKIT, and SERVICE_URL_GARAGE from the
domain fields. Nebulynk uses SOURCE_COMMIT for the build SHA. The build time
is optional and remains unset, while official update-verification keys are
already embedded in release builds.
Changing a public domain requires saving the resource and performing a full rebuild so the frontend receives the new build-time URLs.
On a new resource, Coolify generates and persists these values. They appear in the environment-variable UI and can be edited before the first deployment.
| Generated variable | Used for |
|---|---|
SERVICE_PASSWORD_64_POSTGRES |
PostgreSQL password |
SERVICE_HEX_64_GARAGERPC |
Garage RPC secret |
SERVICE_USER_S3 |
Garage/S3 access key |
SERVICE_PASSWORD_64_S3 |
Garage/S3 secret key |
SERVICE_PASSWORD_64_JWT |
Browser-session signing |
SERVICE_PASSWORD_64_AI |
Encryption of stored AI-provider credentials |
SERVICE_USER_LIVEKIT |
LiveKit API key |
SERVICE_PASSWORD_64_LIVEKIT |
LiveKit API secret |
The standard variables POSTGRES_PASSWORD, GARAGE_RPC_SECRET,
STORAGE_S3_ACCESS_KEY, STORAGE_S3_SECRET_KEY, JWT_SECRET,
AI_SECRET_KEY, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET are optional
overrides. Existing non-empty values take precedence, which preserves current
installations during an update. Leave these overrides empty on a new resource
unless you intentionally supply your own credentials.
SERVICE_HEX_64_GARAGERPC must contain exactly 64 hexadecimal characters. To
create a replacement manually, openssl rand -hex 32 generates 32 random
bytes represented by exactly 64 hexadecimal characters.
Generated values persist across normal rebuilds and redeployments. Back them up securely with the PostgreSQL and Garage volumes. Do not change database, Garage, S3, JWT, AI-encryption, or LiveKit secrets after the first deployment without a service-specific rotation or migration procedure.
No additional environment variables are required for the base platform. Configure only the features you intend to use:
- Web Push: set
VAPID_PUBLIC_KEY,VAPID_PRIVATE_KEY, and optionallyVAPID_SUBJECT. The Compose stack reusesVAPID_PUBLIC_KEYfor the frontend build automatically. Changing it requires a full frontend rebuild. - Email: set
SMTP_HOST,SMTP_PORT,SMTP_SECURE,SMTP_IGNORE_TLS,SMTP_USER,SMTP_PASS,SMTP_FROM, and optionallySMTP_FROM_NAME. - GIF search: optionally set
KLIPY_API_KEYas an environment fallback. You can also add the key later under Admin → Platform Settings; the encrypted platform value takes precedence. - Two-factor encryption: optionally set a strong, stable
AUTH_2FA_SECRET_KEYto keep stored two-factor secrets separate from the JWT signing key. Existing installations can leave it unset and use the backwards-compatibleJWT_SECRETfallback. - Advanced limits, session settings, image pins, and logging values can keep their Compose defaults unless a reviewed deployment requirement says otherwise.
Do not put secrets in variables prefixed with VITE_; Vite values are embedded
in the browser image.
- Save the resource and confirm again that the source is
stableor the intended immutable release tag. - Deploy the complete Compose resource and inspect every service log.
garage-volume-initshould finish successfully and then remain exited. It prepares the two Garage volumes and is not a long-running service.- Confirm that the frontend loads without mixed-content warnings, login and passkey registration work, and the browser connects to the backend domain.
- Upload and download a file and verify that the signed URL uses the Garage domain.
- Start a test meeting and verify signalling, audio/video through TCP
7881or UDP7882, and recording output in Garage. - If configured, test SMTP delivery and Web Push registration.
Back up nebulynk_postgres_data, nebulynk_garage_meta, and
nebulynk_garage_data together. Preserve the generated environment values as
part of the recovery material. Redis data may be rebuilt.
Review release notes and verify backups before every update. A stable
resource can redeploy the latest reviewed commit; a tag-pinned resource must
be moved deliberately to the next tag. Never delete persistent volumes during
an update.
Existing explicit standard secrets continue to override Coolify-generated defaults. Older installations that still use MinIO-named variables must copy their current values before adopting the new Compose file:
| Legacy variable | Current variable |
|---|---|
MINIO_ROOT_USER or MINIO_ACCESS_KEY |
STORAGE_S3_ACCESS_KEY |
MINIO_ROOT_PASSWORD or MINIO_SECRET_KEY |
STORAGE_S3_SECRET_KEY |
MINIO_BUCKET |
STORAGE_S3_BUCKET |
MINIO_PUBLIC_ENDPOINT |
STORAGE_S3_PUBLIC_ENDPOINT |
Copy the existing values; do not generate replacements during this migration.
The bundled stack sets STORAGE_S3_ENDPOINT internally to
http://garage:3900; only map an old MINIO_ENDPOINT when retaining a custom
external S3-compatible service. Changing storage credentials without rotating
them in Garage can make existing objects inaccessible.
Run exactly one backend instance per shared application state. Stop the previous backend completely before starting its replacement; overlapping/rolling backend deployments are unsupported even with a desired replica count of one. Configure the orchestrator accordingly and allow a maintenance window.
The backend handles SIGTERM/SIGINT with a 60-second shutdown budget; container
stop grace is 75 seconds. Wait for GET /health/ready to return HTTP 200 before
routing traffic. Startup clears shared voice participants and stale presence;
existing LiveKit media does not imply seamless API-session recovery.
The Compose deployment adds one transcription-worker container using the backend
image. It has no public domain or port and starts after the backend becomes
healthy. It processes one recording at a time with a default limit of 1.5 GiB
memory and one CPU. Temporary audio stays inside the worker container and is
removed after processing or a restart. The default temporary recording limit is
2 GiB. Adjust TRANSCRIPTION_WORKER_MEMORY_LIMIT, TRANSCRIPTION_WORKER_CPUS,
or TRANSCRIPTION_MAX_TEMP_BYTES only after checking host capacity.
For the first upgrade, leave the transcription AI function disabled, back up
PostgreSQL and Garage, and deploy during the backend maintenance window described
above. Check that the backend and transcription-worker are healthy, then enable
transcription in the admin settings. Existing transcript artifacts in pending
or processing are migrated to the durable queue and processed oldest first.
Meetings that ended while transcription was disabled and never received a
transcript artifact are not queued retrospectively.
Watch the worker's logs for completed chunk indices and retry errors. A failed recording is retried at most twice after its first attempt, with five and then 30 minutes of delay. The API should remain healthy even if Docker reports an OOM for the worker. A permanently failed transcript can be restarted through the existing host or admin action after resolving the cause.
See runtime operations and isolated capacity verification for lifecycle ownership, recovery limits, exact rollout steps and reproducible local Docker tests. Redis rate limiting does not enable multiple API instances.