Self-hosting with Azure Container Apps

Azure Container Apps is a serverless platform for running containers, with built-in load balancing, TLS termination and autoscaling. This article explains how to run the Weavy container image on Azure Container Apps, scaled out to two or more replicas, using the following Azure resources:

Resource Purpose
Azure Database for PostgreSQL flexible server Database shared by all replicas. Files, background jobs and logs are also stored here, so no storage account is needed.
Azure SignalR Service Realtime backplane that delivers realtime events to clients regardless of which replica they are connected to.
Container Apps environment and container app Runs the Weavy container.

The resources in this article are billed continuously, even when Weavy is idle: the PostgreSQL server, the SignalR Service unit and at least two container app replicas.

Prerequisites

  • An Azure subscription where you can create resources.
  • The Azure CLI with the Container Apps extension.

Sign in and make sure the extension and resource providers are installed:

az login
az extension add --name containerapp --upgrade
az provider register --namespace Microsoft.App
az provider register --namespace Microsoft.OperationalInsights

The commands below are written for bash. Start by defining some variables, choosing your own names, region and a strong database password:

RESOURCE_GROUP=weavy
LOCATION=swedencentral
POSTGRES_SERVER=my-weavy-db-server      # must be globally unique
POSTGRES_PASSWORD='<STRONG-PASSWORD>'
SIGNALR_NAME=my-weavy-signalr-service   # must be globally unique
ENVIRONMENT=weavy-env
APP_NAME=weavy
IMAGE=weavy/weavy:latest
WEAVY_LICENSE='YOUR-LICENSE-KEY'

az group create --name $RESOURCE_GROUP --location $LOCATION

We recommend that you use a specific version of the image, e.g. weavy/weavy:32.2.0, rather than latest. Then a replica that is restarted always runs the same version as the others, and you decide when to upgrade.

Create the database

Weavy requires PostgreSQL 18 or later. Create a flexible server and an empty database:

az postgres flexible-server create \
  --resource-group $RESOURCE_GROUP \
  --name $POSTGRES_SERVER \
  --location $LOCATION \
  --version 18 \
  --tier GeneralPurpose \
  --sku-name Standard_D2ds_v5 \
  --storage-size 32 \
  --admin-user weavy \
  --admin-password "$POSTGRES_PASSWORD" \
  --public-access 0.0.0.0

az postgres flexible-server db create \
  --resource-group $RESOURCE_GROUP \
  --server-name $POSTGRES_SERVER \
  --database-name weavy

--public-access 0.0.0.0 adds a firewall rule that allows connections from Azure services, which includes the container app. For stricter network isolation you can instead put the server and the Container Apps environment in the same virtual network, see Networking in Azure Container Apps.

We recommend the General Purpose tier. The Burstable tier runs out of CPU credits under sustained load from several replicas. Also keep in mind that each replica can open up to 100 database connections (the default connection pool size), and that the maximum number of connections depends on the size of the server. Standard_D2ds_v5 allows about 850. If you plan to scale out to many replicas, choose a larger server or add Max Pool Size=<number> to the connection string.

Weavy creates the database schema on first start. The connection string must use the following format (Weavy recognizes a PostgreSQL connection string by the host= keyword):

DB_CONNECTION="host=$POSTGRES_SERVER.postgres.database.azure.com;database=weavy;username=weavy;password=$POSTGRES_PASSWORD;ssl mode=require"

Create the SignalR Service

Create an Azure SignalR Service in Default service mode and get its connection string:

az signalr create \
  --resource-group $RESOURCE_GROUP \
  --name $SIGNALR_NAME \
  --location $LOCATION \
  --sku Standard_S1 \
  --unit-count 1 \
  --service-mode Default

SIGNALR_CONNECTION=$(az signalr key list --resource-group $RESOURCE_GROUP --name $SIGNALR_NAME --query primaryConnectionString --output tsv)

One Standard_S1 unit allows 1,000 concurrent connections. Increase --unit-count if you expect more connected users. The Free tier allows only 20 connections and should not be used in production.

With Azure SignalR Service the clients keep their realtime connection to the service rather than to a replica. Replicas can then scale in and out without dropping connections, and no sticky sessions are needed. You can also use Redis as backplane instead, see Scaling out, but then you must enable sticky sessions on the container app with az containerapp ingress sticky-sessions set --affinity sticky.

Create the container app

Create a Container Apps environment, which also creates a Log Analytics workspace for the container logs:

az containerapp env create \
  --resource-group $RESOURCE_GROUP \
  --name $ENVIRONMENT \
  --location $LOCATION

Then create the container app. The connection strings and the license key are stored as secrets and referenced from the environment variables:

az containerapp create \
  --resource-group $RESOURCE_GROUP \
  --name $APP_NAME \
  --environment $ENVIRONMENT \
  --image $IMAGE \
  --ingress external \
  --target-port 8080 \
  --cpu 1 \
  --memory 2Gi \
  --min-replicas 2 \
  --max-replicas 10 \
  --scale-rule-name http \
  --scale-rule-http-concurrency 100 \
  --secrets "db=$DB_CONNECTION" "signalr=$SIGNALR_CONNECTION" "license=$WEAVY_LICENSE" \
  --env-vars \
    ConnectionStrings__Weavy=secretref:db \
    ConnectionStrings__SignalR=secretref:signalr \
    Weavy__License=secretref:license

Some notes on these settings:

  • Container Apps terminates TLS and forwards requests as plain HTTP to port 8080, together with the X-Forwarded-Proto header that tells Weavy that the original request used HTTPS. The image listens for HTTP on port 8080 by default, so no certificate is needed in the container.
  • At least two replicas means that a restart or an upgrade never takes Weavy offline. The HTTP scale rule adds a replica for every 100 concurrent requests. The default of 10 requests is very aggressive for Weavy, where every replica also holds a pool of database connections.
  • 1 vCPU and 2 GiB of memory per replica is a good starting point. Less memory can be too little for generating document previews.
  • The license is optional. If you leave it out (remove license=... and Weavy__License=...), you can install it from the admin area after deploying.

Finally, set Weavy__ApplicationUrl to the public URL of the container app. Weavy uses it for absolute URLs when there is no incoming request to get them from, e.g. in background jobs and webhooks:

FQDN=$(az containerapp show --resource-group $RESOURCE_GROUP --name $APP_NAME --query properties.configuration.ingress.fqdn --output tsv)

az containerapp update \
  --resource-group $RESOURCE_GROUP \
  --name $APP_NAME \
  --set-env-vars Weavy__ApplicationUrl=https://$FQDN

If you add a custom domain, set Weavy__ApplicationUrl to that instead.

Configure health probes

Container Apps uses health probes to decide when a replica has started, when it should be restarted and when it can receive traffic. We recommend the following:

Probe Type Settings Why
Startup TCP, port 8080 Initial delay 5, period 5, timeout 3, failure threshold 60 On startup, Weavy connects to the database (and creates the schema on first start) before it starts listening. This allows up to 5 minutes for that.
Liveness HTTP GET /live, port 8080 Initial delay 5, period 10, timeout 5, failure threshold 3 /live returns 200 OK as long as the process is running. A timeout of 5 seconds keeps a busy replica from being restarted under load.
Readiness HTTP GET /ready, port 8080 Initial delay 5, period 10, timeout 5, failure threshold 3 /ready returns 503 Service Unavailable when a replica can't serve requests, e.g. when it can't reach the database or SignalR Service, so that traffic goes to the other replicas.

Configure the probes in the Azure portal: open the container app, go to Application > Containers, click Edit and deploy, select the weavy container and open the Health probes tab. If you deploy with YAML, ARM or Bicep, the probes look like this in the container definition:

probes:
  - type: Startup
    tcpSocket:
      port: 8080
    initialDelaySeconds: 5
    periodSeconds: 5
    timeoutSeconds: 3
    failureThreshold: 60
  - type: Liveness
    httpGet:
      path: /live
      port: 8080
    initialDelaySeconds: 5
    periodSeconds: 10
    timeoutSeconds: 5
    failureThreshold: 3
  - type: Readiness
    httpGet:
      path: /ready
      port: 8080
    initialDelaySeconds: 5
    periodSeconds: 10
    timeoutSeconds: 5
    failureThreshold: 3

Without a valid license, or when the database must be upgraded, /ready still returns 200 OK with the status degraded. The replicas stay in rotation so that you can reach the admin area, while all other requests get 503 Service Unavailable. See Health probes for details.

Browse to Weavy

Open https://<FQDN> in your browser. If everything worked out you should see the Weavy logo on your screen. You can also check:

  • https://<FQDN>/live, which returns 200 OK when the replicas are running.
  • https://<FQDN>/ready, which returns 200 OK with the status healthy when the database and SignalR Service can be reached and a valid license is installed. The status is degraded when there is no valid license yet.

Without a valid license Weavy answers requests with 503 Service Unavailable instead of showing the logo. The admin area stays available, so you can still create an admin account and install the license at /admin/license.

Create admin account

Next step is to create a local admin account for managing Weavy. Go to https://<FQDN>/admin/account and fill in the form to create an admin account and gain access to the /admin section where you can create API keys, view logs etc.

The admin area is reachable on the public URL of the container app. If you want to restrict access to it, you can add IP restrictions to the container app, or run a separate admin container app with internal ingress, see Split deployments.

Upgrade Weavy

To upgrade to a new version of Weavy, update the image of the container app:

az containerapp update \
  --resource-group $RESOURCE_GROUP \
  --name $APP_NAME \
  --image weavy/weavy:<VERSION>

Container Apps creates a new revision and shifts traffic to it once its replicas have started. If the new version requires a newer database schema, the replicas answer 503 Service Unavailable until the database has been upgraded from the admin area at /admin/database. To let Weavy upgrade the database automatically on startup instead, add the environment variable Weavy__AutomaticMigrations=true. See Database upgrades.

Troubleshooting

Once Weavy can connect to the database, the logs from all replicas are available in the admin area at Admin > Logs, where each entry shows which replica it came from. If Weavy does not start, e.g. because the connection string is invalid or the database can't be reached, look at the console output instead:

az containerapp logs show --resource-group $RESOURCE_GROUP --name $APP_NAME --follow

The console logs are also available in the Log Analytics workspace of the Container Apps environment, and the revisions and replicas (and their restarts) are shown in the Azure portal under Application > Revisions and replicas.

Common problems are:

  • The replicas keep restarting: The database can't be reached or the connection string is invalid. Check the firewall rules of the PostgreSQL server and that the connection string contains ssl mode=require.
  • 503 Service Unavailable: Browse to /ready to see the status. If it is degraded, there is usually no valid license, or the database must be upgraded from /admin/database. If it is unhealthy, check the logs.
  • Realtime events (e.g. new messages) only show up after a reload: Check that ConnectionStrings__SignalR is set and that the SignalR Service runs in Default service mode.

Clean up

To delete all resources created in this article, including the database and all data in it, delete the resource group:

az group delete --name $RESOURCE_GROUP