├── .env ├── .github └── workflows │ └── main.yaml ├── .gitignore ├── .huly.nginx ├── .template.huly.conf ├── .template.nginx.conf ├── LICENSE ├── MIGRATION.md ├── README.md ├── compose.yml ├── kube ├── QUICKSTART.md ├── README.md ├── account │ ├── account-deployment.yaml │ ├── account-ingress.yaml │ └── account-service.yaml ├── collaborator │ ├── collaborator-deployment.yaml │ ├── collaborator-ingress.yaml │ └── collaborator-service.yaml ├── config │ ├── config.yaml │ └── secret.yaml ├── elastic │ ├── elastic-deployment.yaml │ ├── elastic-persistentvolumeclaim.yaml │ └── elastic-service.yaml ├── front │ ├── front-deployment.yaml │ ├── front-ingress.yaml │ └── front-service.yaml ├── fulltext │ ├── fulltext-deployment.yaml │ └── fulltext-service.yaml ├── minio │ ├── files-persistentvolumeclaim.yaml │ ├── minio-deployment.yaml │ └── minio-service.yaml ├── mongodb │ ├── mongodb-deployment.yaml │ ├── mongodb-persistentvolumeclaim.yml │ └── mongodb-service.yaml ├── rekoni │ ├── rekoni-deployment.yaml │ ├── rekoni-ingress.yaml │ └── rekoni-service.yaml ├── stats │ ├── stats-deployment.yaml │ ├── stats-ingress.yaml │ └── stats-service.yaml ├── transactor │ ├── transactor-deployment.yaml │ ├── transactor-ingress.yaml │ └── transactor-service.yaml └── workspace │ └── workspace-deployment.yaml ├── nginx.sh ├── setup.sh └── traefik ├── README.md ├── setup.sh └── template-compose.yaml /.env: -------------------------------------------------------------------------------- 1 | huly.conf -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | pull_request: 9 | 10 | jobs: 11 | kind-selfhost: 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 20 14 | steps: 15 | - name: "Checkout repository" 16 | uses: actions/checkout@v4 17 | with: 18 | path: ${{ github.workspace }}/src/github.com/${{ github.repository }} 19 | 20 | - name: "Install kind" 21 | run: | 22 | [ $(uname -m) = x86_64 ] && curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.26.0/kind-linux-amd64 23 | # For ARM64 24 | [ $(uname -m) = aarch64 ] && curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.26.0/kind-linux-arm64 25 | chmod +x ./kind 26 | sudo mv ./kind /usr/local/bin/kind 27 | 28 | - name: "Setup k8s cluster" 29 | run: | 30 | cat < [!NOTE] 6 | > Huly is quite resource-heavy, so I recommend using a Droplet with 2 vCPUs and 4GB of RAM. Droplets with less RAM may 7 | > stop responding or fail. 8 | 9 | If you prefer Kubernetes deployment, there is a sample Kubernetes configuration under [kube](kube) directory. 10 | 11 | ## Installing `nginx` and `docker` 12 | 13 | First, update repositories cache: 14 | 15 | ```bash 16 | sudo apt update 17 | ``` 18 | 19 | Now, install `nginx`: 20 | 21 | ```bash 22 | sudo apt install nginx 23 | ``` 24 | 25 | Install docker using the [recommended method](https://docs.docker.com/engine/install/ubuntu/) from docker website. 26 | Afterwards perform [post-installation steps](https://docs.docker.com/engine/install/linux-postinstall/). Pay attention to 3rd step with `newgrp docker` command, it needed for correct execution in setup script. 27 | 28 | ## Clone the `huly-selfhost` repository and configure `nginx` 29 | 30 | Next, let's clone the `huly-selfhost` repository and configure Huly. 31 | 32 | ```bash 33 | git clone https://github.com/hcengineering/huly-selfhost.git 34 | cd huly-selfhost 35 | ./setup.sh 36 | ``` 37 | 38 | This will generate a [huly.conf](./huly.conf) file with your chosen values and create your nginx config. 39 | 40 | To add the generated configuration to your Nginx setup, run the following: 41 | 42 | ```bash 43 | sudo ln -s $(pwd)/nginx.conf /etc/nginx/sites-enabled/huly.conf 44 | ``` 45 | 46 | > [!NOTE] 47 | > If you change `HOST_ADDRESS`, `SECURE`, `HTTP_PORT` or `HTTP_BIND` be sure to update your [nginx.conf](./nginx.conf) 48 | > by running: 49 | > ```bash 50 | > ./nginx.sh 51 | > ``` 52 | >You can safely execute this script after adding your custom configurations like ssl. It will only overwrite the 53 | > necessary settings. 54 | 55 | Finally, let's reload `nginx` and start Huly with `docker compose`. 56 | 57 | ```bash 58 | sudo nginx -s reload 59 | sudo docker compose up -d 60 | ``` 61 | 62 | Now, launch your web browser and enjoy Huly! 63 | 64 | ## Generating Public and Private VAPID keys for front-end 65 | 66 | You'll need `Node.js` installed on your machine. Installing `npm` on Debian based distro: 67 | 68 | ``` 69 | sudo apt-get install npm 70 | ``` 71 | 72 | Install web-push using npm 73 | 74 | ```bash 75 | sudo npm install -g web-push 76 | ``` 77 | 78 | Generate VAPID Keys. Run the following command to generate a VAPID key pair: 79 | 80 | ``` 81 | web-push generate-vapid-keys 82 | ``` 83 | 84 | It will generate both keys that looks like this: 85 | 86 | ```bash 87 | ======================================= 88 | 89 | Public Key: 90 | sdfgsdgsdfgsdfggsdf 91 | 92 | Private Key: 93 | asdfsadfasdfsfd 94 | 95 | ======================================= 96 | ``` 97 | 98 | Keep these keys secure, as you will need them to set up your push notification service on the server. 99 | 100 | Add these keys into `compose.yaml` in section `services:ses:environment`: 101 | 102 | ```yaml 103 | - PUSH_PUBLIC_KEY=your public key 104 | - PUSH_PRIVATE_KEY=your private key 105 | ``` 106 | 107 | ## Mail Service 108 | 109 | The Mail Service is responsible for sending email notifications and confirmation emails during user login or signup processes. It can be configured to send emails through either an SMTP server or Amazon SES (Simple Email Service), but not both at the same time. 110 | 111 | ### General Configuration 112 | 113 | 1. Add the `mail` container to the `docker-compose.yaml` file. Specify the email address you will use to send emails as "SOURCE": 114 | 115 | ```yaml 116 | mail: 117 | image: hardcoreeng/mail:v0.6.501 118 | container_name: mail 119 | ports: 120 | - 8097:8097 121 | environment: 122 | - PORT=8097 123 | - SOURCE= 124 | restart: unless-stopped 125 | ``` 126 | 127 | 2. Add the mail container URL to the `transactor` and `account` containers: 128 | 129 | ```yaml 130 | account: 131 | ... 132 | environment: 133 | - MAIL_URL=http://mail:8097 134 | ... 135 | transactor: 136 | ... 137 | environment: 138 | - MAIL_URL=http://mail:8097 139 | ... 140 | ``` 141 | 142 | 3. In `Settings -> Notifications`, set up email notifications for the events you want to be notified about. Note that this is a user-specific setting, not company-wide; each user must set up their own notification preferences. 143 | 144 | ### SMTP Configuration 145 | 146 | To integrate with an external SMTP server, update the `docker-compose.yaml` file with the following environment variables: 147 | 148 | 1. Add SMTP configuration to the environment section: 149 | 150 | ```yaml 151 | mail: 152 | ... 153 | environment: 154 | ... 155 | - SMTP_HOST= 156 | - SMTP_PORT= 157 | - SMTP_USERNAME= 158 | - SMTP_PASSWORD= 159 | ``` 160 | 161 | 2. Replace `` and `` with your SMTP server's hostname and port. It's recommended to use a secure port, such as `587`. 162 | 163 | 3. Replace `` and `` with credentials for an account that can send emails via your SMTP server. If your service provider supports it, consider using an application API key as `` and a token as `` for enhanced security. 164 | 165 | ### Amazon SES Configuration 166 | 167 | 1. Set up Amazon Simple Email Service in AWS: [AWS SES Setup Guide](https://docs.aws.amazon.com/ses/latest/dg/setting-up.html) 168 | 169 | 2. Create a new IAM policy with the following permissions: 170 | 171 | ```json 172 | { 173 | "Version": "2012-10-17", 174 | "Statement": [ 175 | { 176 | "Effect": "Allow", 177 | "Action": [ 178 | "ses:SendEmail", 179 | "ses:SendRawEmail" 180 | ], 181 | "Resource": "*" 182 | } 183 | ] 184 | } 185 | ``` 186 | 187 | 3. Create a separate IAM user for SES API access, assigning the newly created policy to this user. 188 | 189 | 4. Configure SES environment variables in the `mail` container: 190 | 191 | ```yaml 192 | mail: 193 | ... 194 | environment: 195 | ... 196 | - SES_ACCESS_KEY= 197 | - SES_SECRET_KEY= 198 | - SES_REGION= 199 | ``` 200 | 201 | ### Notes 202 | 203 | 1. SMTP and SES configurations cannot be used simultaneously. 204 | 2. `SES_URL` is not supported in version v0.6.470 and later, please use `MAIL_URL` instead. 205 | 206 | 207 | ## Love Service (Audio & Video calls) 208 | 209 | Huly audio and video calls are created on top of LiveKit insfrastructure. In order to use Love service in your 210 | self-hosted Huly, perform the following steps: 211 | 212 | 1. Set up [LiveKit Cloud](https://cloud.livekit.io) account 213 | 2. Add `love` container to the docker-compose.yaml 214 | 215 | ```yaml 216 | love: 217 | image: hardcoreeng/love:v0.6.501 218 | container_name: love 219 | ports: 220 | - 8096:8096 221 | environment: 222 | - STORAGE_CONFIG=minio|minio?accessKey=minioadmin&secretKey=minioadmin 223 | - SECRET=secret 224 | - ACCOUNTS_URL=http://account:3000 225 | - DB_URL=mongodb://mongodb:27017 226 | - MONGO_URL=mongodb://mongodb:27017 227 | - STORAGE_PROVIDER_NAME=minio 228 | - PORT=8096 229 | - LIVEKIT_HOST= 230 | - LIVEKIT_API_KEY= 231 | - LIVEKIT_API_SECRET= 232 | restart: unless-stopped 233 | ``` 234 | 235 | 3. Configure `front` service: 236 | 237 | ```yaml 238 | front: 239 | ... 240 | environment: 241 | - LIVEKIT_WS= 242 | - LOVE_ENDPOINT=http://love:8096 243 | ... 244 | ``` 245 | 246 | ## AI Service 247 | 248 | Huly provides AI-powered chatbot that provides several services: 249 | 250 | - chat with AI 251 | - text message translations in the chat 252 | - live translations for virtual office voice and video chats 253 | 254 | 1. Set up OpenAI account 255 | 2. Add `aibot` container to the docker-compose.yaml 256 | 257 | ```yaml 258 | aibot: 259 | image: hardcoreeng/ai-bot:v0.6.501 260 | ports: 261 | - 4010:4010 262 | environment: 263 | - STORAGE_CONFIG=minio|minio?accessKey=minioadmin&secretKey=minioadmin 264 | - SERVER_SECRET=secret 265 | - ACCOUNTS_URL=http://account:3000 266 | - DB_URL=mongodb://mongodb:27017 267 | - MONGO_URL=mongodb://mongodb:27017 268 | - STATS_URL=http://stats:4900 269 | - FIRST_NAME=Bot 270 | - LAST_NAME=Huly AI 271 | - PASSWORD= 272 | - OPENAI_API_KEY= 273 | - OPENAI_BASE_URL= 274 | # optional if you use love service 275 | - LOVE_ENDPOINT=http://love:8096 276 | restart: unless-stopped 277 | ``` 278 | 279 | 3. Configure `front` service: 280 | 281 | ```yaml 282 | front: 283 | ... 284 | environment: 285 | # this should be available outside of the cluster 286 | - AI_URL=http://aibot:4010 287 | ... 288 | ``` 289 | 290 | 4. Configure `transactor` service: 291 | 292 | ```yaml 293 | transactor: 294 | ... 295 | environment: 296 | # this should be available inside of the cluster 297 | - AI_BOT_URL=http://aibot:4010 298 | ... 299 | ``` 300 | 301 | ## Configure OpenID Connect (OIDC) 302 | 303 | You can configure a Huly instance to authorize users (sign-in/sign-up) using an OpenID Connect identity provider (IdP). 304 | 305 | ### On the IdP side 306 | 1. Create a new OpenID application. 307 | * Use `{huly_account_svc}/auth/openid/callback` as the sign-in redirect URI. The `huly_account_svc` is the hostname for the account service of the deployment, which should be accessible externally from the client/browser side. In the provided example setup, the account service runs on port 3000. 308 | 309 | **URI Example:** 310 | - `http://huly.mydomain.com:3000/auth/openid/callback` 311 | 312 | 2. Configure user access to the application as needed. 313 | 314 | ### On the Huly side 315 | 316 | For the account service, set the following environment variables as provided by the IdP: 317 | 318 | * OPENID_CLIENT_ID 319 | * OPENID_CLIENT_SECRET 320 | * OPENID_ISSUER 321 | 322 | Ensure you have configured or add the following environment variable to the front service: 323 | 324 | * ACCOUNTS_URL (This should contain the URL of the account service, accessible from the client side.) 325 | 326 | You will need to expose your account service port (e.g. 3000) in your nginx.conf. 327 | 328 | Note: Once all the required environment variables are configured, you will see an additional button on the 329 | sign-in/sign-up pages. 330 | 331 | ## Configure GitHub OAuth 332 | 333 | You can also configure a Huly instance to use GitHub OAuth for user authorization (sign-in/sign-up). 334 | 335 | ### On the GitHub side 336 | 1. Create a new GitHub OAuth application. 337 | * Use `{huly_account_svc}/auth/github/callback` as the sign-in redirect URI. The `huly_account_svc` is the hostname for the account service of the deployment, which should be accessible externally from the client/browser side. In the provided example setup, the account service runs on port 3000. 338 | 339 | **URI Example:** 340 | - `http://huly.mydomain.com:3000/auth/github/callback` 341 | 342 | ### On the Huly side 343 | 344 | Specify the following environment variables for the account service: 345 | 346 | * `GITHUB_CLIENT_ID` 347 | * `GITHUB_CLIENT_SECRET` 348 | 349 | Ensure you have configured or add the following environment variable to the front service: 350 | 351 | * `ACCOUNTS_URL` (The URL of the account service, accessible from the client side.) 352 | 353 | You will need to expose your account service port (e.g. 3000) in your nginx.conf. 354 | 355 | Notes: 356 | 357 | * The `ISSUER` environment variable is not required for GitHub OAuth. 358 | * Once all the required environment variables are configured, you will see an additional button on the sign-in/sign-up 359 | pages. 360 | 361 | ## Disable Sign-Up 362 | 363 | You can disable public sign-ups for a deployment. When configured, sign-ups will only be permitted through an invite 364 | link to a specific workspace. 365 | 366 | To implement this, set the following environment variable for both the front and account services: 367 | 368 | ```yaml 369 | account: 370 | # ... 371 | environment: 372 | - DISABLE_SIGNUP=true 373 | # ... 374 | front: 375 | # ... 376 | environment: 377 | - DISABLE_SIGNUP=true 378 | # ... 379 | ``` 380 | 381 | _Note: When setting up a new deployment, either create the initial account before disabling sign-ups or use the 382 | development tool to create the first account._ 383 | -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | name: ${DOCKER_NAME} 2 | version: "3" 3 | services: 4 | nginx: 5 | image: "nginx:1.21.3" 6 | ports: 7 | - "${HTTP_BIND}:${HTTP_PORT}:80" 8 | volumes: 9 | - ./.huly.nginx:/etc/nginx/conf.d/default.conf 10 | restart: unless-stopped 11 | 12 | mongodb: 13 | image: "mongo:7-jammy" 14 | environment: 15 | - PUID=1000 16 | - PGID=1000 17 | volumes: 18 | - db:/data/db 19 | restart: unless-stopped 20 | 21 | minio: 22 | image: "minio/minio" 23 | command: server /data --address ":9000" --console-address ":9001" 24 | volumes: 25 | - files:/data 26 | restart: unless-stopped 27 | 28 | elastic: 29 | image: "elasticsearch:7.14.2" 30 | command: | 31 | /bin/sh -c "./bin/elasticsearch-plugin list | grep -q ingest-attachment || yes | ./bin/elasticsearch-plugin install --silent ingest-attachment; 32 | /usr/local/bin/docker-entrypoint.sh eswrapper" 33 | volumes: 34 | - elastic:/usr/share/elasticsearch/data 35 | environment: 36 | - ELASTICSEARCH_PORT_NUMBER=9200 37 | - BITNAMI_DEBUG=true 38 | - discovery.type=single-node 39 | - ES_JAVA_OPTS=-Xms1024m -Xmx1024m 40 | - http.cors.enabled=true 41 | - http.cors.allow-origin=http://localhost:8082 42 | healthcheck: 43 | interval: 20s 44 | retries: 10 45 | test: curl -s http://localhost:9200/_cluster/health | grep -vq '"status":"red"' 46 | restart: unless-stopped 47 | 48 | rekoni: 49 | image: hardcoreeng/rekoni-service:${HULY_VERSION} 50 | environment: 51 | - SECRET=${SECRET} 52 | deploy: 53 | resources: 54 | limits: 55 | memory: 500M 56 | restart: unless-stopped 57 | 58 | transactor: 59 | image: hardcoreeng/transactor:${HULY_VERSION} 60 | environment: 61 | - SERVER_PORT=3333 62 | - SERVER_SECRET=${SECRET} 63 | - SERVER_CURSOR_MAXTIMEMS=30000 64 | - DB_URL=mongodb://mongodb:27017 65 | - MONGO_URL=mongodb://mongodb:27017 66 | - STORAGE_CONFIG=minio|minio?accessKey=minioadmin&secretKey=minioadmin 67 | - FRONT_URL=http://localhost:8087 68 | - ACCOUNTS_URL=http://account:3000 69 | - FULLTEXT_URL=http://fulltext:4700 70 | - STATS_URL=http://stats:4900 71 | - LAST_NAME_FIRST=${LAST_NAME_FIRST:-true} 72 | restart: unless-stopped 73 | 74 | collaborator: 75 | image: hardcoreeng/collaborator:${HULY_VERSION} 76 | environment: 77 | - COLLABORATOR_PORT=3078 78 | - SECRET=${SECRET} 79 | - ACCOUNTS_URL=http://account:3000 80 | - DB_URL=mongodb://mongodb:27017 81 | - STATS_URL=http://stats:4900 82 | - STORAGE_CONFIG=minio|minio?accessKey=minioadmin&secretKey=minioadmin 83 | restart: unless-stopped 84 | 85 | account: 86 | image: hardcoreeng/account:${HULY_VERSION} 87 | environment: 88 | - SERVER_PORT=3000 89 | - SERVER_SECRET=${SECRET} 90 | - DB_URL=mongodb://mongodb:27017 91 | - MONGO_URL=mongodb://mongodb:27017 92 | - TRANSACTOR_URL=ws://transactor:3333;ws${SECURE:+s}://${HOST_ADDRESS}/_transactor 93 | - STORAGE_CONFIG=minio|minio?accessKey=minioadmin&secretKey=minioadmin 94 | - FRONT_URL=http://front:8080 95 | - STATS_URL=http://stats:4900 96 | - MODEL_ENABLED=* 97 | - ACCOUNTS_URL=http://localhost:3000 98 | - ACCOUNT_PORT=3000 99 | restart: unless-stopped 100 | 101 | workspace: 102 | image: hardcoreeng/workspace:${HULY_VERSION} 103 | environment: 104 | - SERVER_SECRET=${SECRET} 105 | - DB_URL=mongodb://mongodb:27017 106 | - MONGO_URL=mongodb://mongodb:27017 107 | - TRANSACTOR_URL=ws://transactor:3333;ws${SECURE:+s}://${HOST_ADDRESS}/_transactor 108 | - STORAGE_CONFIG=minio|minio?accessKey=minioadmin&secretKey=minioadmin 109 | - MODEL_ENABLED=* 110 | - ACCOUNTS_URL=http://account:3000 111 | - STATS_URL=http://stats:4900 112 | restart: unless-stopped 113 | 114 | front: 115 | image: hardcoreeng/front:${HULY_VERSION} 116 | environment: 117 | - SERVER_PORT=8080 118 | - SERVER_SECRET=${SECRET} 119 | - LOVE_ENDPOINT=http${SECURE:+s}://${HOST_ADDRESS}/_love 120 | - ACCOUNTS_URL=http${SECURE:+s}://${HOST_ADDRESS}/_accounts 121 | - REKONI_URL=http${SECURE:+s}://${HOST_ADDRESS}/_rekoni 122 | - CALENDAR_URL=http${SECURE:+s}://${HOST_ADDRESS}/_calendar 123 | - GMAIL_URL=http${SECURE:+s}://${HOST_ADDRESS}/_gmail 124 | - TELEGRAM_URL=http${SECURE:+s}://${HOST_ADDRESS}/_telegram 125 | - STATS_URL=http${SECURE:+s}://${HOST_ADDRESS}/_stats 126 | - UPLOAD_URL=/files 127 | - ELASTIC_URL=http://elastic:9200 128 | - COLLABORATOR_URL=ws${SECURE:+s}://${HOST_ADDRESS}/_collaborator 129 | - STORAGE_CONFIG=minio|minio?accessKey=minioadmin&secretKey=minioadmin 130 | - DB_URL=mongodb://mongodb:27017 131 | - MONGO_URL=mongodb://mongodb:27017 132 | - TITLE=${TITLE:-Huly Self Host} 133 | - DEFAULT_LANGUAGE=${DEFAULT_LANGUAGE:-en} 134 | - LAST_NAME_FIRST=${LAST_NAME_FIRST:-true} 135 | - DESKTOP_UPDATES_CHANNEL=selfhost 136 | restart: unless-stopped 137 | 138 | fulltext: 139 | image: hardcoreeng/fulltext:${HULY_VERSION} 140 | environment: 141 | - SERVER_SECRET=${SECRET} 142 | - DB_URL=mongodb://mongodb:27017 143 | - FULLTEXT_DB_URL=http://elastic:9200 144 | - ELASTIC_INDEX_NAME=huly_storage_index 145 | - STORAGE_CONFIG=minio|minio?accessKey=minioadmin&secretKey=minioadmin 146 | - REKONI_URL=http://rekoni:4004 147 | - ACCOUNTS_URL=http://account:3000 148 | - STATS_URL=http://stats:4900 149 | restart: unless-stopped 150 | 151 | stats: 152 | image: hardcoreeng/stats:${HULY_VERSION} 153 | environment: 154 | - PORT=4900 155 | - SERVER_SECRET=${SECRET} 156 | restart: unless-stopped 157 | volumes: 158 | db: 159 | elastic: 160 | files: -------------------------------------------------------------------------------- /kube/QUICKSTART.md: -------------------------------------------------------------------------------- 1 | # Quick Start with Kind 2 | > [!NOTE] 3 | > kind does not require kubectl, but you will not be able to perform some of the examples in our docs without it. To install kubectl see the upstream kubectl installation docs. 4 | 5 | ## Install 6 | 7 | **macOS:** 8 | ```bash 9 | # For Intel Macs 10 | [ $(uname -m) = x86_64 ] && curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.26.0/kind-darwin-amd64 11 | # For M1 / ARM Macs 12 | [ $(uname -m) = arm64 ] && curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.26.0/kind-darwin-arm64 13 | chmod +x ./kind 14 | mv ./kind /some-dir-in-your-PATH/kind 15 | ``` 16 | 17 | **Linux:** 18 | ```bash 19 | # For AMD64 / x86_64 20 | [ $(uname -m) = x86_64 ] && curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.26.0/kind-linux-amd64 21 | # For ARM64 22 | [ $(uname -m) = aarch64 ] && curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.26.0/kind-linux-arm64 23 | chmod +x ./kind 24 | sudo mv ./kind /usr/local/bin/kind 25 | ``` 26 | 27 | ## Setup cluster with port forwarding 28 | 29 | > [!NOTE] 30 | > On the host computer, `localhost:80` should be accessible. 31 | 32 | ```bash 33 | cat <> ./nginx.conf 67 | fi 68 | 69 | read -p "Do you want to run 'nginx -s reload' now to load your updated Huly config? (Y/n): " RUN_NGINX 70 | case "${RUN_NGINX:-Y}" in 71 | [Yy]* ) 72 | echo -e "\033[1;32mRunning 'nginx -s reload' now...\033[0m" 73 | sudo nginx -s reload 74 | ;; 75 | [Nn]* ) 76 | echo "You can run 'nginx -s reload' later to load your updated Huly config." 77 | ;; 78 | esac -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | HULY_VERSION="v0.6.501" 4 | DOCKER_NAME="huly" 5 | CONFIG_FILE="huly.conf" 6 | 7 | if [ -f "$CONFIG_FILE" ]; then 8 | source "$CONFIG_FILE" 9 | fi 10 | 11 | while true; do 12 | if [[ -n "$HOST_ADDRESS" ]]; then 13 | prompt_type="current" 14 | prompt_value="${HOST_ADDRESS}" 15 | else 16 | prompt_type="default" 17 | prompt_value="localhost" 18 | fi 19 | read -p "Enter the host address (domain name or IP) [${prompt_type}: ${prompt_value}]: " input 20 | _HOST_ADDRESS="${input:-${HOST_ADDRESS:-localhost}}" 21 | break 22 | done 23 | 24 | while true; do 25 | if [[ -n "$HTTP_PORT" ]]; then 26 | prompt_type="current" 27 | prompt_value="${HTTP_PORT}" 28 | else 29 | prompt_type="default" 30 | prompt_value="80" 31 | fi 32 | read -p "Enter the port for HTTP [${prompt_type}: ${prompt_value}]: " input 33 | _HTTP_PORT="${input:-${HTTP_PORT:-80}}" 34 | if [[ "$_HTTP_PORT" =~ ^[0-9]+$ && "$_HTTP_PORT" -ge 1 && "$_HTTP_PORT" -le 65535 ]]; then 35 | break 36 | else 37 | echo "Invalid port. Please enter a number between 1 and 65535." 38 | fi 39 | done 40 | 41 | echo "$_HOST_ADDRESS $HOST_ADDRESS $_HTTP_PORT $HTTP_PORT" 42 | 43 | if [[ "$_HOST_ADDRESS" == "localhost" || "$_HOST_ADDRESS" == "127.0.0.1" || "$_HOST_ADDRESS" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}:?$ ]]; then 44 | _HOST_ADDRESS="${_HOST_ADDRESS%:}:${_HTTP_PORT}" 45 | SECURE="" 46 | else 47 | while true; do 48 | if [[ -n "$SECURE" ]]; then 49 | prompt_type="current" 50 | prompt_value="Yes" 51 | else 52 | prompt_type="default" 53 | prompt_value="No" 54 | fi 55 | read -p "Will you serve Huly over SSL? (y/n) [${prompt_type}: ${prompt_value}]: " input 56 | case "${input}" in 57 | [Yy]* ) 58 | _SECURE="true"; break;; 59 | [Nn]* ) 60 | _SECURE=""; break;; 61 | "" ) 62 | _SECURE="${SECURE:+true}"; break;; 63 | * ) 64 | echo "Invalid input. Please enter Y or N.";; 65 | esac 66 | done 67 | fi 68 | 69 | SECRET=false 70 | if [ "$1" == "--secret" ]; then 71 | SECRET=true 72 | fi 73 | 74 | if [ ! -f .huly.secret ] || [ "$SECRET" == true ]; then 75 | openssl rand -hex 32 > .huly.secret 76 | echo "Secret generated and stored in .huly.secret" 77 | else 78 | echo -e "\033[33m.huly.secret already exists, not overwriting." 79 | echo "Run this script with --secret to generate a new secret." 80 | fi 81 | 82 | export HOST_ADDRESS=$_HOST_ADDRESS 83 | export SECURE=$_SECURE 84 | export HTTP_PORT=$_HTTP_PORT 85 | export HTTP_BIND=$HTTP_BIND 86 | export TITLE=${TITLE:-Huly} 87 | export DEFAULT_LANGUAGE=${DEFAULT_LANGUAGE:-en} 88 | export LAST_NAME_FIRST=${LAST_NAME_FIRST:-true} 89 | export HULY_SECRET=$(cat .huly.secret) 90 | 91 | envsubst < .template.huly.conf > $CONFIG_FILE 92 | 93 | echo -e "\n\033[1;34mConfiguration Summary:\033[0m" 94 | echo -e "Host Address: \033[1;32m$_HOST_ADDRESS\033[0m" 95 | echo -e "HTTP Port: \033[1;32m$_HTTP_PORT\033[0m" 96 | if [[ -n "$SECURE" ]]; then 97 | echo -e "SSL Enabled: \033[1;32mYes\033[0m" 98 | else 99 | echo -e "SSL Enabled: \033[1;31mNo\033[0m" 100 | fi 101 | 102 | read -p "Do you want to run 'docker compose up -d' now to start Huly? (Y/n): " RUN_DOCKER 103 | case "${RUN_DOCKER:-Y}" in 104 | [Yy]* ) 105 | echo -e "\033[1;32mRunning 'docker compose up -d' now...\033[0m" 106 | docker compose up -d 107 | ;; 108 | [Nn]* ) 109 | echo "You can run 'docker compose up -d' later to start Huly." 110 | ;; 111 | esac 112 | 113 | echo -e "\033[1;32mSetup is complete!\n Generating nginx.conf...\033[0m" 114 | ./nginx.sh 115 | -------------------------------------------------------------------------------- /traefik/README.md: -------------------------------------------------------------------------------- 1 | # Instructions to deploy Huly on a `self-hosted` server with SSL using `Traefik` 2 | 3 | ### Prerequisites 4 | 5 | - A domain name pointing to the server 6 | - A server with Docker and Docker Compose installed 7 | 8 | ### Steps 9 | 10 | 1. Clone the repository 11 | 12 | ```bash 13 | git clone https://github.com/hcengineering/huly-selfhost.git 14 | cd huly-selfhost/traefik 15 | ``` 16 | 17 | 2. Run setup.sh 18 | 19 | ```bash 20 | chmod +x setup.sh 21 | ./setup.sh 22 | ``` 23 | 24 | 3. Follow the instructions in the setup script to configure your domain name and email address 25 | 26 | ```bash 27 | $ ./setup.sh 28 | Enter the domain name: example.com 29 | Enter the email address: admin@example.com 30 | Setup is complete. Run 'docker compose up -d' to start the services. 31 | ``` 32 | 33 | 4. Modify the `docker-compose.yaml` file to customize any settings 34 | 35 | 5. Start the services 36 | 37 | ```bash 38 | docker compose up -d 39 | ``` 40 | 41 | 6. Access Huly at `https://example.com` 42 | -------------------------------------------------------------------------------- /traefik/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Ask for the domain name 4 | read -p "Enter the domain name: " DOMAIN_NAME 5 | if [ -z "$DOMAIN_NAME" ]; then 6 | echo "DOMAIN_NAME is required" 7 | exit 1 8 | fi 9 | 10 | # Ask for the email address 11 | read -p "Enter the email address: " LETSENCRYPT_EMAIL 12 | if [ -z "$LETSENCRYPT_EMAIL" ]; then 13 | echo "LETSENCRYPT_EMAIL address is required" 14 | exit 1 15 | fi 16 | 17 | export HULY_VERSION="v0.6.501" 18 | export HULY_SECRET="secret" 19 | export SERVER_ADDRESS=$DOMAIN_NAME 20 | export LETSENCRYPT_EMAIL=$LETSENCRYPT_EMAIL 21 | 22 | # replace the domain name and email address in the docker-compose file 23 | envsubst < template-compose.yaml > docker-compose.yaml 24 | 25 | echo -e "\033[1;32mSetup is complete. Run 'docker compose up -d' to start the services.\033[0m" 26 | -------------------------------------------------------------------------------- /traefik/template-compose.yaml: -------------------------------------------------------------------------------- 1 | x-common-env: &common-env 2 | SERVER_SECRET: ${HULY_SECRET} 3 | SECRET: ${HULY_SECRET} 4 | STORAGE_CONFIG: minio|minio?accessKey=minioadmin&secretKey=minioadmin 5 | MONGO_URL: mongodb://mongodb:27017 6 | DB_URL: mongodb://mongodb:27017 7 | ACCOUNTS_URL: http://account:3000 8 | STATS_URL: http://stats:4900 9 | 10 | services: 11 | traefik: 12 | restart: unless-stopped 13 | image: "traefik:v2.10" 14 | container_name: "traefik" 15 | ports: 16 | - "80:80" 17 | - "443:443" 18 | volumes: 19 | - "/var/run/docker.sock:/var/run/docker.sock:ro" 20 | - ./letsencrypt:/letsencrypt 21 | networks: 22 | - traefik-public 23 | command: 24 | - "--log.level=DEBUG" # set to INFO for production 25 | - "--api.insecure=false" 26 | - "--api.dashboard=true" 27 | - "--global.sendAnonymousUsage=false" 28 | - "--global.checkNewVersion=false" 29 | - "--providers.docker=true" 30 | - "--providers.docker.exposedbydefault=false" 31 | - "--providers.docker.network=traefik-public" 32 | - "--entrypoints.web.address=:80" 33 | - "--entrypoints.websecure.address=:443" 34 | - "--entrypoints.web.http.redirections.entryPoint.to=websecure" 35 | - "--entrypoints.web.http.redirections.entryPoint.scheme=https" 36 | - "--certificatesresolvers.myresolver.acme.email=${LETSENCRYPT_EMAIL}" 37 | - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json" 38 | - "--certificatesresolvers.myresolver.acme.tlschallenge=true" 39 | - "--certificatesresolvers.myresolver.acme.caserver=http://acme-staging-v02.api.letsencrypt.org/directory" # For testing, comment out for production 40 | labels: 41 | - "traefik.enable=true" 42 | - "traefik.http.routers.traefik.rule=Host(`${SERVER_ADDRESS}`) && (PathPrefix(`/api`) || PathPrefix(`/traefik`))" 43 | - "traefik.http.routers.traefik.service=api@internal" 44 | - "traefik.http.routers.traefik.entrypoints=websecure" 45 | # strip prefix for traefik dashboard 46 | - "traefik.http.routers.traefik.middlewares=strip-prefix-traefik" 47 | - "traefik.http.middlewares.strip-prefix-traefik.stripprefix.prefixes=/traefik" 48 | - "traefik.http.routers.traefik.tls=true" 49 | - "traefik.http.routers.traefik.tls.certresolver=myresolver" 50 | 51 | mongodb: 52 | image: mongo:7-jammy 53 | container_name: mongodb 54 | restart: unless-stopped 55 | environment: 56 | - PUID=1000 57 | - PGID=1000 58 | volumes: 59 | - db:/data/db 60 | networks: 61 | - internal-services 62 | 63 | minio: 64 | image: minio/minio 65 | restart: unless-stopped 66 | command: server /data --address ":9000" --console-address ":9001" 67 | volumes: 68 | - files:/data 69 | networks: 70 | - internal-services 71 | 72 | elastic: 73 | image: elasticsearch:7.14.2 74 | restart: unless-stopped 75 | command: | 76 | /bin/sh -c "./bin/elasticsearch-plugin list | grep -q ingest-attachment || yes | ./bin/elasticsearch-plugin install --silent ingest-attachment; 77 | /usr/local/bin/docker-entrypoint.sh eswrapper" 78 | volumes: 79 | - elastic:/usr/share/elasticsearch/data 80 | environment: 81 | - ELASTICSEARCH_PORT_NUMBER=9200 82 | - BITNAMI_DEBUG=true 83 | - discovery.type=single-node 84 | - ES_JAVA_OPTS=-Xms1024m -Xmx1024m 85 | - http.cors.enabled=true 86 | - http.cors.allow-origin=http://localhost:8082 87 | healthcheck: 88 | interval: 20s 89 | retries: 10 90 | test: curl -s http://localhost:9200/_cluster/health | grep -vq '"status":"red"' 91 | networks: 92 | - internal-services 93 | 94 | rekoni: 95 | image: hardcoreeng/rekoni-service:${HULY_VERSION} 96 | restart: unless-stopped 97 | environment: 98 | <<: *common-env 99 | deploy: 100 | resources: 101 | limits: 102 | memory: 500M 103 | networks: 104 | - internal-services 105 | - traefik-public 106 | labels: 107 | - "traefik.enable=true" 108 | - "traefik.http.routers.rekoni.entrypoints=websecure" 109 | - "traefik.http.services.rekoni.loadbalancer.server.port=4004" 110 | - "traefik.http.routers.rekoni.rule=Host(`${SERVER_ADDRESS}`) && PathPrefix(`/rekoni`)" 111 | - "traefik.http.routers.rekoni.middlewares=rekoni-stripprefix" 112 | - "traefik.http.middlewares.rekoni-stripprefix.stripprefix.prefixes=/rekoni" 113 | - "traefik.http.routers.rekoni.tls=true" 114 | - "traefik.http.routers.rekoni.tls.certresolver=myresolver" 115 | 116 | transactor: 117 | image: hardcoreeng/transactor:${HULY_VERSION} 118 | restart: unless-stopped 119 | environment: 120 | <<: *common-env 121 | SERVER_PORT: 3333 122 | SERVER_CURSOR_MAXTIMEMS: 30000 123 | FRONT_URL: http://localhost:8087 124 | FULLTEXT_URL: http://fulltext:4700 125 | LAST_NAME_FIRST: true 126 | networks: 127 | - internal-services 128 | - traefik-public 129 | labels: 130 | - "traefik.enable=true" 131 | # WebSocket route 132 | - "traefik.http.routers.transactor-ws.entrypoints=websecure" 133 | - "traefik.http.routers.transactor-ws.rule=Host(`${SERVER_ADDRESS}`) && PathPrefix(`/ws/transactor`)" 134 | - "traefik.http.routers.transactor-ws.tls=true" 135 | - "traefik.http.routers.transactor-ws.tls.certresolver=myresolver" 136 | - "traefik.http.services.transactor-ws.loadbalancer.server.port=3333" 137 | - "traefik.http.routers.transactor-ws.service=transactor-ws" 138 | 139 | # Strip WebSocket prefix 140 | - "traefik.http.routers.transactor-ws.middlewares=strip-transactor-ws-prefix" 141 | - "traefik.http.middlewares.strip-transactor-ws-prefix.stripprefix.prefixes=/ws/transactor" 142 | 143 | # HTTP route for non-WebSocket traffic 144 | - "traefik.http.routers.transactor.entrypoints=websecure" 145 | - "traefik.http.routers.transactor.rule=Host(`${SERVER_ADDRESS}`) && PathPrefix(`/transactor`)" 146 | - "traefik.http.routers.transactor.tls=true" 147 | - "traefik.http.routers.transactor.tls.certresolver=myresolver" 148 | - "traefik.http.services.transactor.loadbalancer.server.port=3333" 149 | - "traefik.http.routers.transactor.service=transactor" 150 | # Strip HTTP prefix 151 | - "traefik.http.routers.transactor.middlewares=strip-transactor-prefix" 152 | - "traefik.http.middlewares.strip-transactor-prefix.stripprefix.prefixes=/transactor" 153 | 154 | collaborator: 155 | image: hardcoreeng/collaborator:${HULY_VERSION} 156 | restart: unless-stopped 157 | environment: 158 | <<: *common-env 159 | COLLABORATOR_PORT: 3078 160 | networks: 161 | - internal-services 162 | - traefik-public 163 | labels: 164 | - "traefik.enable=true" 165 | # WebSocket route 166 | - "traefik.http.routers.collaborator-ws.entrypoints=websecure" 167 | - "traefik.http.routers.collaborator-ws.rule=Host(`${SERVER_ADDRESS}`) && PathPrefix(`/ws/collaborator`)" 168 | - "traefik.http.routers.collaborator-ws.tls=true" 169 | - "traefik.http.routers.collaborator-ws.tls.certresolver=myresolver" 170 | - "traefik.http.services.collaborator-ws.loadbalancer.server.port=3078" 171 | - "traefik.http.routers.collaborator-ws.service=collaborator-ws" 172 | 173 | # Strip WebSocket prefix 174 | - "traefik.http.routers.collaborator-ws.middlewares=strip-collaborator-ws-prefix" 175 | - "traefik.http.middlewares.strip-collaborator-ws-prefix.stripprefix.prefixes=/ws/collaborator" 176 | # HTTP route for non-WebSocket traffic 177 | - "traefik.http.routers.collaborator.entrypoints=websecure" 178 | - "traefik.http.routers.collaborator.rule=Host(`${SERVER_ADDRESS}`) && PathPrefix(`/collaborator`)" 179 | - "traefik.http.routers.collaborator.tls=true" 180 | - "traefik.http.routers.collaborator.tls.certresolver=myresolver" 181 | - "traefik.http.services.collaborator.loadbalancer.server.port=3078" 182 | 183 | # Strip HTTP prefix 184 | - "traefik.http.routers.collaborator.middlewares=strip-collaborator-prefix" 185 | - "traefik.http.middlewares.strip-collaborator-prefix.stripprefix.prefixes=/collaborator" 186 | 187 | account: 188 | image: hardcoreeng/account:${HULY_VERSION} 189 | restart: unless-stopped 190 | environment: 191 | <<: *common-env 192 | SERVER_PORT: 3000 193 | TRANSACTOR_URL: ws://transactor:3333;wss://${SERVER_ADDRESS}/ws/transactor 194 | FRONT_URL: http://front:8080 195 | MODEL_ENABLED: "*" 196 | ACCOUNT_PORT: 3000 197 | networks: 198 | - internal-services 199 | - traefik-public 200 | labels: 201 | - "traefik.enable=true" 202 | - "traefik.http.routers.account.entrypoints=websecure" 203 | - "traefik.http.services.account.loadbalancer.server.port=3000" 204 | - "traefik.http.routers.account.rule=Host(`${SERVER_ADDRESS}`) && PathPrefix(`/accounts`)" 205 | - "traefik.http.routers.account.middlewares=account-stripprefix" 206 | - "traefik.http.middlewares.account-stripprefix.stripprefix.prefixes=/accounts" 207 | - "traefik.http.routers.account.tls=true" 208 | - "traefik.http.routers.account.tls.certresolver=myresolver" 209 | 210 | workspace: 211 | image: hardcoreeng/workspace:${HULY_VERSION} 212 | restart: unless-stopped 213 | environment: 214 | <<: *common-env 215 | TRANSACTOR_URL: ws://transactor:3333;wss://${SERVER_ADDRESS}/ws/transactor 216 | MODEL_ENABLED: "*" 217 | networks: 218 | - internal-services 219 | 220 | front: 221 | image: hardcoreeng/front:${HULY_VERSION} 222 | restart: unless-stopped 223 | environment: 224 | <<: *common-env 225 | SERVER_PORT: 8080 226 | ACCOUNTS_URL: https://${SERVER_ADDRESS}/accounts 227 | REKONI_URL: https://${SERVER_ADDRESS}/rekoni 228 | CALENDAR_URL: https://${SERVER_ADDRESS}:8095 229 | GMAIL_URL: https://${SERVER_ADDRESS}:8088 230 | TELEGRAM_URL: https://${SERVER_ADDRESS}:8086 231 | STATS_URL: https://${SERVER_ADDRESS}/stats 232 | UPLOAD_URL: /files 233 | ELASTIC_URL: http://elastic:9200 234 | COLLABORATOR_URL: wss://${SERVER_ADDRESS}/ws/collaborator 235 | TITLE: Huly Self Host 236 | DEFAULT_LANGUAGE: en 237 | LAST_NAME_FIRST: true 238 | DESKTOP_UPDATES_CHANNEL: selfhost 239 | networks: 240 | - internal-services 241 | - traefik-public 242 | labels: 243 | - "traefik.enable=true" 244 | - "traefik.http.routers.front.entrypoints=websecure" 245 | - "traefik.http.services.front.loadbalancer.server.port=8080" 246 | - "traefik.http.routers.front.rule=Host(`${SERVER_ADDRESS}`)" 247 | - "traefik.http.routers.front.tls=true" 248 | - "traefik.http.routers.front.tls.certresolver=myresolver" 249 | 250 | fulltext: 251 | image: hardcoreeng/fulltext:${HULY_VERSION} 252 | restart: unless-stopped 253 | environment: 254 | <<: *common-env 255 | FULLTEXT_DB_URL: http://elastic:9200 256 | ELASTIC_INDEX_NAME: huly_storage_index 257 | REKONI_URL: http://rekoni:4004 258 | networks: 259 | - internal-services 260 | 261 | stats: 262 | image: hardcoreeng/stats:${HULY_VERSION} 263 | restart: unless-stopped 264 | environment: 265 | <<: *common-env 266 | PORT: 4900 267 | networks: 268 | - internal-services 269 | - traefik-public 270 | labels: 271 | - "traefik.enable=true" 272 | - "traefik.http.routers.stats.entrypoints=websecure" 273 | - "traefik.http.services.stats.loadbalancer.server.port=4900" 274 | - "traefik.http.routers.stats.rule=Host(`${SERVER_ADDRESS}`) && PathPrefix(`/stats`)" 275 | - "traefik.http.routers.stats.middlewares=stats-stripprefix" 276 | - "traefik.http.middlewares.stats-stripprefix.stripprefix.prefixes=/stats" 277 | - "traefik.http.routers.stats.tls=true" 278 | - "traefik.http.routers.stats.tls.certresolver=myresolver" 279 | 280 | networks: 281 | traefik-public: 282 | name: traefik-public 283 | internal-services: 284 | name: internal-services 285 | 286 | volumes: 287 | db: 288 | elastic: 289 | files: 290 | letsencrypt: 291 | --------------------------------------------------------------------------------