Run a Server on Coolify
Deploy one Zugoid Server in Coolify from the published image, claim the Owner, add addresses and update by redeploying or pinning a version.
The Coolify template runs one standalone Zugoid Server: Zugoid on a single celld node with private RustFS storage. Coolify's proxy owns the domain and TLS. The template uses the published ghcr.io/inchidev/zugoid image. It is public, so Coolify pulls it without a registry login and nothing is built on your machine.
It is not a published Coolify catalog entry yet. Copy the Compose file below.
Install
- In Coolify, choose New Resource → Docker Compose and paste the Compose file.
- Set the domain of the
zugoidservice, on port8080. SetADMIN_USERNAMEto the first Owner's username; it defaults toowner. LeaveZUGOID_VERSIONunset to runlatest. - Deploy. The first start takes a few minutes while the database migrations are applied.
- Wait until
https://<your-domain>/api/setup/healthanswers with HTTP 200. - Sign in with
ADMIN_USERNAMEand the password Coolify generated:SERVICE_PASSWORD_ADMINin the service's environment variables. Then change the password in your account settings.
The first start creates the Owner from these values. Later changes to SERVICE_PASSWORD_ADMIN have no effect. To choose the password yourself, set SERVICE_PASSWORD_ADMIN (at least 12 characters) before the first deploy.
If the Server does not come up, read the logs of the zugoid container in Coolify; see also Troubleshooting.
Coolify generates these values for the template:
| Variable | Used as |
|---|---|
SERVICE_URL_ZUGOID_8080, SERVICE_FQDN_ZUGOID_8080 |
The Server's public origin (PUBLIC_ORIGIN). |
SERVICE_PASSWORD_64_AUTHSECRET |
AUTH_SECRET. Signs sessions and encrypts stored credentials. |
SERVICE_PASSWORD_ADMIN |
ADMIN_PASSWORD. The first Owner's initial password. |
SERVICE_PASSWORD_64_SETUPTOKEN |
SETUP_TOKEN. Only used if the Owner is claimed on /setup instead. |
SERVICE_USER_RUSTFS, SERVICE_PASSWORD_64_RUSTFS |
Keys for the private storage bucket. |
Important
Keep these values private and never regenerate them. A new
AUTH_SECRETsigns out everyone and makes stored credentials unreadable.
APP_NAME sets the initial name (default Zugoid). You can rename the Server later in Settings.
Compose file
This is self-host/coolify/docker-compose.yaml as of this release. Use Copy and paste it into Coolify unchanged.
# documentation: https://github.com/inchidev/zugoid/blob/main/self-host/coolify/README.md
# slogan: Self-hosted team chat on a single VPS.
# category: collaboration
# tags: chat,team,celld,rustfs,self-host,zugoid
# logo: svgs/zugoid.png
# port: 8080
services:
# Existing installations used a root-owned volume. Prepare it before the
# unprivileged node starts, including after a cold restore.
celld-perms:
image: alpine:3.24
user: "0:0"
restart: "no"
network_mode: none
read_only: true
cap_drop: [ALL]
cap_add: [CHOWN, DAC_OVERRIDE, FOWNER]
security_opt: [no-new-privileges:true]
volumes:
- celld-data:/data
command: ["chown", "-R", "10001:10001", "/data"]
rustfs-perms:
image: alpine:3.24
user: "0:0"
restart: "no"
volumes:
- rustfs-data:/data
command:
- sh
- -c
- chown -R 10001:10001 /data
rustfs:
image: rustfs/rustfs:1.0.4
restart: always
depends_on:
rustfs-perms:
condition: service_completed_successfully
environment:
- RUSTFS_VOLUMES=/data
- RUSTFS_ADDRESS=0.0.0.0:9000
- RUSTFS_CONSOLE_ENABLE=false
- RUSTFS_ACCESS_KEY=$SERVICE_USER_RUSTFS
- RUSTFS_SECRET_KEY=$SERVICE_PASSWORD_64_RUSTFS
- RUSTFS_OBS_LOGGER_LEVEL=error
volumes:
- rustfs-data:/data
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:9000/health"]
interval: 10s
timeout: 5s
retries: 12
start_period: 20s
bucket-init:
image: amazon/aws-cli:2.39.3
restart: "no"
depends_on:
rustfs:
condition: service_healthy
environment:
- AWS_ACCESS_KEY_ID=$SERVICE_USER_RUSTFS
- AWS_SECRET_ACCESS_KEY=$SERVICE_PASSWORD_64_RUSTFS
- AWS_DEFAULT_REGION=${AWS_REGION:-us-east-1}
- AWS_EC2_METADATA_DISABLED=true
entrypoint: ["/bin/sh", "-c"]
command:
- |
set -eu
aws configure set default.s3.addressing_style path
i=0
while [ "$$i" -lt 30 ]; do
if aws s3api head-bucket --bucket cells --endpoint-url http://rustfs:9000 >/dev/null 2>&1; then
exit 0
fi
if aws s3api create-bucket --bucket cells --endpoint-url http://rustfs:9000 --region us-east-1; then
exit 0
fi
i=$$((i + 1))
sleep 2
done
echo "could not create s3://cells" >&2
exit 1
# Updates are this image tag: latest follows stable releases, or pin a
# version such as 0.0.2 and change it to update. Never set an older one.
zugoid:
image: ghcr.io/inchidev/zugoid:${ZUGOID_VERSION:-latest}
# A celld node that loses its lease exits; Docker starts it again.
restart: always
user: "10001:10001"
read_only: true
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
tmpfs:
- /tmp:size=256m,mode=1777
stop_grace_period: 50s
depends_on:
celld-perms:
condition: service_completed_successfully
bucket-init:
condition: service_completed_successfully
rustfs:
condition: service_healthy
environment:
- SERVICE_URL_ZUGOID_8080
- SERVICE_FQDN_ZUGOID_8080
- PUBLIC_ORIGIN=$SERVICE_URL_ZUGOID_8080
- ZUGOID_ORIGINS=${ZUGOID_ORIGINS:-}
- AUTH_SECRET=$SERVICE_PASSWORD_64_AUTHSECRET
- SETUP_TOKEN=$SERVICE_PASSWORD_64_SETUPTOKEN
- ADMIN_USERNAME=${ADMIN_USERNAME:-owner}
# Coolify generates the first Owner's password; the first start creates the Owner.
- ADMIN_PASSWORD=$SERVICE_PASSWORD_ADMIN
- APP_NAME=${APP_NAME:-Zugoid}
- AWS_ACCESS_KEY_ID=$SERVICE_USER_RUSTFS
- AWS_SECRET_ACCESS_KEY=$SERVICE_PASSWORD_64_RUSTFS
- AWS_REGION=${AWS_REGION:-us-east-1}
- AWS_EC2_METADATA_DISABLED=true
- S3_ENDPOINT=http://rustfs:9000
- CELLD_BUCKET=s3://cells
- CELLD_WATCH=/var/lib/zugoid/celld
- CELLD_DURABILITY=bucket
- CELLD_READY_FLEET_GATE_MS=0
- CLOUDFLARE_ACCOUNT_ID=${CLOUDFLARE_ACCOUNT_ID:-}
- CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN:-}
- AI_BASE_URL=${AI_BASE_URL:-}
- AI_API_KEY=${AI_API_KEY:-}
- AI_MODEL=${AI_MODEL:-}
- MAIL_GATEWAY_URL=${MAIL_GATEWAY_URL:-}
- MAIL_GATEWAY_TOKEN=${MAIL_GATEWAY_TOKEN:-}
- SMTP_GATEWAY_URL=${SMTP_GATEWAY_URL:-}
- SMTP_GATEWAY_TOKEN=${SMTP_GATEWAY_TOKEN:-}
- ZUGOID_EMAIL_DOMAINS=${ZUGOID_EMAIL_DOMAINS:-[]}
- MAIL_APP_HOSTNAME=${MAIL_APP_HOSTNAME:-}
- MAIL_DEFAULT_LOCAL_PART=${MAIL_DEFAULT_LOCAL_PART:-}
- INBOUND_EMAIL_SECRET=${INBOUND_EMAIL_SECRET:-}
- BROWSER_CDP_URL=${BROWSER_CDP_URL:-}
volumes:
- celld-data:/var/lib/zugoid/celld
healthcheck:
test: ["CMD", "/usr/local/bin/zugoid-healthcheck"]
interval: 15s
timeout: 5s
retries: 12
start_period: 240s
volumes:
rustfs-data:
celld-data:
More addresses
To serve more domains, add them in Coolify and list every origin in ZUGOID_ORIGINS, comma-separated. The Server trusts only listed origins. RustFS has no public port.
Updates
The image is built for linux/amd64 and linux/arm64. Only stable releases are published as images, with three tags: the exact version (for example 0.3.5), its minor line (0.3) and latest.
Updates are controlled in Coolify, not in Zugoid:
- Follow stable releases. Leave
ZUGOID_VERSIONunset to uselatest. Redeploy the service to pull the newest stable release. - Pin a version. Set
ZUGOID_VERSIONto an exact version. Change it and redeploy to update.
On start, the new version applies its database migrations and skips ones already applied. Migrations only go forward: an older image refuses to start on data a newer one has migrated. If that happens, set a version at least as new as the one in the error.
In Zugoid, Server settings → Deployment → Updates shows "Updates are managed outside Zugoid" and tells you when a newer stable release exists.
Backups
Backups are optional but recommended before an update.
- Server settings → Deployment → Backups downloads the database and files, or sends them to an S3 destination.
- For a full copy, stop the service and archive the
rustfs-dataandcelld-datavolumes. A copy taken while the service runs may be inconsistent.
Keep backups outside these volumes. See Files and backups.
Optional providers
Cloudflare, AI and mail credentials are optional. The template accepts CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN, AI_BASE_URL, AI_API_KEY, AI_MODEL, MAIL_GATEWAY_URL, MAIL_GATEWAY_TOKEN, SMTP_GATEWAY_URL, SMTP_GATEWAY_TOKEN, ZUGOID_EMAIL_DOMAINS, MAIL_APP_HOSTNAME, MAIL_DEFAULT_LOCAL_PART, INBOUND_EMAIL_SECRET and BROWSER_CDP_URL. See Optional providers for what each one does.