Delegation/split-domain deployment

Matrix allows clients and servers to discover a homeserver's "true" destination via .well-known delegation. This is especially useful if you would like to:

  • Serve Continuwuity on a subdomain while having only the base domain for your usernames
  • Use a port other than :8448 for server-to-server connections

This guide will show you how to have @user:example.com usernames while serving Continuwuity on https://matrix.example.com. It assumes you are using port 443 for both client-to-server connections and server-to-server federation.

Configuration

First, ensure you have set up A/AAAA records for matrix.example.com and example.com pointing to your IP.

Then, ensure that the server_name field matches your intended username suffix. If this is not the case, you MUST wipe the database directory and reinstall Continuwuity with your desired server_name.

Then, in the [global.well_known] section of your config file, add the following fields:

[global.well_known]

# defaults to port :443 if not specified
client = "https://matrix.example.com"

# port number MUST be specified
server = "matrix.example.com:443"

# (optional) customize your support contacts
# Defaults to members of the admin room if unset
#support_page =
#support_role = "m.role.admin"
#support_email =
#support_mxid = "@user:example.com"

Alternatively if you are using Docker, you can set the CONTINUWUITY_WELL_KNOWN environment variable as below:

services:
  continuwuity:
    ...
    environment:
      CONTINUWUITY_WELL_KNOWN: |
        {
        client=https://matrix.example.com,
        server=matrix.example.com:443
        }

      # You can also configure individual `.well-knowns` like this
      # CONTINUWUITY_WELL_KNOWN__CLIENT: https://matrix.example.com
      # CONTINUWUITY_WELL_KNOWN__SERVER: matrix.example.com:443

After doing the steps above, Continuwuity will serve these 3 JSON files:

  • /.well-known/matrix/client: for Client-Server discovery
  • /.well-known/matrix/server: for Server-Server (federation) discovery
  • /.well-known/matrix/support: admin contact details (strongly recommended to have)

To enable full discovery, you will need to reverse proxy these paths from the base domain back to Continuwuity.

Reverse proxying well-known files to Continuwuity

For Caddy
matrix.example.com:443 {
  reverse_proxy 127.0.0.1:8008
}

example.com:443 {
  reverse_proxy /.well-known/matrix* 127.0.0.1:8008
}
For Traefik (via Docker labels)
services:
  continuwuity:
    ...
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.continuwuity.rule=(Host(`matrix.example.com`) || (Host(`example.com`) && PathPrefix(`/.well-known/matrix`)))"
      - "traefik.http.routers.continuwuity.service=continuwuity"
      - "traefik.http.services.continuwuity.loadbalancer.server.port=8008"

For Docker users, consult the compose files in the Appendix section.

After applying these changes, restart Continuwuity and your reverse proxy.Visit these routes and check that the responses match the examples below:

https://example.com/.well-known/matrix/server
{ "m.server": "matrix.example.com:443" }
https://example.com/.well-known/matrix/client
{
  "m.homeserver": {
    "base_url": "https://matrix.example.com/"
  }
}

Serving well-known files manually

Instead of configuring [global.well_known] options and reverse proxying well-known URIs, you can serve these files directly as static JSON that match the ones above. This is useful if your base domain points to a different physical server, and reverse proxying isn't feasible.

Example Caddyfile for the base domain
https://example.com {

    respond /.well-known/matrix/server 200 {
        body `{"m.server":"matrix.example.com:443"}`
    }

    handle /.well-known/matrix/client {
    header Access-Control-Allow-Origin *
    respond <<JSON
    {
        "m.homeserver": {
            "base_url": "https://matrix.example.com/"
        }
    }
    JSON
    }
}

Remember to set the Access-Control-Allow-Origin: * header in your /.well-known/matrix/client path for web clients to work.

Troubleshooting

Check that other servers can connect to you. Here are some tools that can help identify federation issues:

Cannot log in with web clients

Make sure there is an Access-Control-Allow-Origin: * header in your /.well-known/matrix/client path. While Continuwuity serves this header by default, it may be dropped by reverse proxies or other middlewares.

Issues with alternative setups

As Matrix clients prioritize well-known URIs for their destination, this can lead to issues with alternative methods of accessing the server that doesn't use a publicly routeable IP and domain name. You will probably find yourself connecting to non-existent/undesired URLs in certain cases like:

  • Accessing to the server via localhost IPs (e.g. for testing purposes)
  • Accessing the server from behind a VPN, or from alternative networks (such as from an onionsite)

In these scenarios, further configurations would be needed. Refer to the Related Documentation section for resolution steps and see how they could apply to your use case.


Warning

The following methods are not recommended due to increased complexity with little benefits. If you have already set up .well-known delegation as above, you can safely skip this part.

The following methods uses SRV DNS records and only work with federation traffic. They are only included for completeness.

Using only SRV records

If you can't set up /.well-known/matrix/server on :443 for some reason, you can set up a SRV record (via your DNS provider) as below:

  • Service and name: _matrix-fed._tcp.example.com.
  • Priority: 10 (can be any number)
  • Weight: 10 (can be any number)
  • Port: 443
  • Target: matrix.example.com.

On the target's IP at port 443, you must configure a valid route and cert for your server name, example.com. Therefore, this method only works to redirect traffic into the right IP/port combo, and can not delegate your federation to a different domain.

Using SRV records + .well-known

You can also set up /.well-known/matrix/server with a delegated domain but no ports:

[global.well_known]
server = "matrix.example.com"

Then, set up a SRV record (via your DNS provider) to announce the port number as below:

  • Service and name: _matrix-fed._tcp.matrix.example.com.
  • Priority: 10 (can be any number)
  • Weight: 10 (can be any number)
  • Port: 443
  • Target: matrix.example.com.

On the target's IP at port 443, you'll need to provide a valid route and cert for matrix.example.com. It provides the same feature as pure .well-known delegation, albeit with more parts to handle.

Using SRV records as a fallback for .well-known delegation

Assume your delegation is as below:

[global.well_known]
server = "example.com:443"

If your Continuwuity instance becomes temporarily unreachable, other servers will not be able to find your /.well-known/matrix/server file, and defaults to using server_name:8448. This incorrect cache can persist for a long time, and would hinder re-federation when your server eventually comes back online.

If you want other servers to default to using port :443 even when it is offline, you could set up a SRV record (via your DNS provider) as follows:

  • Service and name: _matrix-fed._tcp.example.com.
  • Priority: 10 (can be any number)
  • Weight: 10 (can be any number)
  • Port: 443
  • Target: example.com.

On the target's IP at port 443, you'll need to provide a valid route and cert for example.com.


See the following Matrix Specs for full details on client/server resolution mechanisms:

Appendix

Docker Compose examples

The following Compose files are taken from Docker instructions and reconfigured to support split-domain delegation. Note the updated CONTINUWUITY_WELL_KNOWN variable and relevant changes in reverse proxy rules.

Caddy (using Caddyfile) - delegated.docker-compose.with-caddy.yml (view raw)
# Continuwuity - Using Caddy Docker Image

services:
    caddy:
        image: "docker.io/caddy:latest"
        ports:
            - 80:80
            - 443:443
        networks:
            - caddy
        volumes:
            - ./data:/data
        restart: unless-stopped
        configs:
            - source: Caddyfile
              target: /etc/caddy/Caddyfile

    homeserver:
        image: "forgejo.ellis.link/continuwuation/continuwuity:latest"
        restart: unless-stopped
        command: /sbin/conduwuit
        volumes:
            - db:/var/lib/continuwuity
            - ./continuwuity-resolv.conf:/etc/resolv.conf # use custom resolvers rather than Docker's
            #- ./continuwuity.toml:/etc/continuwuity.toml
        environment:
            CONTINUWUITY_SERVER_NAME: example.com
            CONTINUWUITY_DATABASE_PATH: /var/lib/continuwuity
            CONTINUWUITY_ADDRESS: 0.0.0.0
            CONTINUWUITY_PORT: 8008
            #CONTINUWUITY_CONFIG: '/etc/continuwuity.toml' # Uncomment if you mapped config toml above

            ## Serve .well-known files to tell others to reach Continuwuity on port :443
            CONTINUWUITY_WELL_KNOWN: |
                {
                client=https://matrix.example.com,
                server=matrix.example.com:443
                }

        networks:
            - caddy

networks:
    caddy:

volumes:
    db:

configs:
    Caddyfile:
        content: |
            https://matrix.example.com:443 {
                reverse_proxy http://homeserver:8008
            }
            https://example.com:443 {
                reverse_proxy /.well-known/matrix* http://homeserver:8008
            }
Caddy (using labels) - delegated.docker-compose.with-caddy-labels.yml (view raw)
# Continuwuity - With Caddy Labels

services:
    caddy:
    # This compose file uses caddy-docker-proxy as the reverse proxy for Continuwuity!
    # For more info, visit https://github.com/lucaslorentz/caddy-docker-proxy
        image: "docker.io/lucaslorentz/caddy-docker-proxy:ci-alpine"
        ports:
            - 80:80
            - 443:443
        environment:
            - CADDY_INGRESS_NETWORKS=caddy
        networks:
            - caddy
        volumes:
            - /var/run/docker.sock:/var/run/docker.sock
            - ./data:/data
        restart: unless-stopped
        labels:
            caddy: example.com
            caddy.reverse_proxy: /.well-known/matrix/* homeserver:8008

    homeserver:
        image: "forgejo.ellis.link/continuwuation/continuwuity:latest"
        restart: unless-stopped
        command: /sbin/conduwuit
        volumes:
            - db:/var/lib/continuwuity
            - ./continuwuity-resolv.conf:/etc/resolv.conf # use custom resolvers rather than Docker's
            #- ./continuwuity.toml:/etc/continuwuity.toml
        environment:
            CONTINUWUITY_SERVER_NAME: example.com # EDIT THIS
            CONTINUWUITY_DATABASE_PATH: /var/lib/continuwuity
            CONTINUWUITY_ADDRESS: 0.0.0.0
            CONTINUWUITY_PORT: 8008
            #CONTINUWUITY_CONFIG: '/etc/continuwuity.toml' # Uncomment if you mapped config toml above

            # Serve .well-known files to tell others to reach Continuwuity on port :443
            CONTINUWUITY_WELL_KNOWN: |
                {
                client=https://matrix.example.com,
                server=matrix.example.com:443
                }

        networks:
            - caddy
        labels:
            caddy: matrix.example.com
            caddy.reverse_proxy: "{{upstreams 8008}}"
volumes:
    db:

networks:
    caddy:
Traefik (for existing setup) - delegated.docker-compose.for-traefik.yml (view raw)
# Continuwuity - Behind Traefik Reverse Proxy

services:
  homeserver:
    image: "forgejo.ellis.link/continuwuation/continuwuity:latest"
    restart: unless-stopped
    command: /sbin/conduwuit
    volumes:
      - db:/var/lib/continuwuity
      - ./continuwuity-resolv.conf:/etc/resolv.conf # use custom resolvers rather than Docker's
      #- ./continuwuity.toml:/etc/continuwuity.toml
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.continuwuity.rule=(Host(`matrix.example.com`) || (Host(`example.com`) && PathPrefix(`/.well-known/matrix`)))"
      - "traefik.http.routers.continuwuity.entrypoints=websecure" # your HTTPS entry point
      - "traefik.http.routers.continuwuity.tls=true"
      - "traefik.http.routers.continuwuity.service=continuwuity"
      - "traefik.http.services.continuwuity.loadbalancer.server.port=8008"
      # possibly, depending on your config:
      # - "traefik.http.routers.continuwuity.tls.certresolver=letsencrypt"
    environment:
      CONTINUWUITY_SERVER_NAME: example.com # EDIT THIS
      CONTINUWUITY_DATABASE_PATH: /var/lib/continuwuity
      CONTINUWUITY_ADDRESS: 0.0.0.0
      CONTINUWUITY_PORT: 8008 # This must match with traefik's loadbalancer label
      #CONTINUWUITY_CONFIG: '/etc/continuwuity.toml' # Uncomment if you mapped config toml above

      # Serve .well-known files to tell others to reach Continuwuity on port :443
      CONTINUWUITY_WELL_KNOWN: |
        {
        client=https://matrix.example.com,
        server=matrix.example.com:443
        }

volumes:
  db:

networks:
  # This must match the network name that Traefik listens on
  proxy:
    external: true
Traefik included - delegated.docker-compose.with-traefik.yml (view raw)
# Continuwuity - With Traefik Reverse Proxy

services:
  homeserver:
    image: "forgejo.ellis.link/continuwuation/continuwuity:latest"
    restart: unless-stopped
    command: /sbin/conduwuit
    volumes:
      - db:/var/lib/continuwuity
      - ./continuwuity-resolv.conf:/etc/resolv.conf # use custom resolvers rather than Docker's
      #- ./continuwuity.toml:/etc/continuwuity.toml
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.continuwuity.rule=(Host(`matrix.example.com`) || (Host(`example.com`) && PathPrefix(`/.well-known/matrix`)))"
      - "traefik.http.routers.continuwuity.entrypoints=websecure"
      - "traefik.http.routers.continuwuity.tls.certresolver=letsencrypt"
      - "traefik.http.services.continuwuity.loadbalancer.server.port=8008"
    environment:
      CONTINUWUITY_SERVER_NAME: example.com # EDIT THIS
      CONTINUWUITY_DATABASE_PATH: /var/lib/continuwuity
      CONTINUWUITY_ADDRESS: 0.0.0.0
      CONTINUWUITY_PORT: 8008 # This must match with traefik's loadbalancer label
      #CONTINUWUITY_CONFIG: '/etc/continuwuity.toml' # Uncomment if you mapped config toml above

      # Serve .well-known files to tell others to reach Continuwuity on port :443
      CONTINUWUITY_WELL_KNOWN: |
        {
          client=https://matrix.example.com,
          server=matrix.example.com:443
        }

  traefik:
    image: "docker.io/traefik:latest"
    container_name: "traefik"
    restart: "unless-stopped"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "acme:/etc/traefik/acme"
    labels:
      - "traefik.enable=true"

      # middleware redirect
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
      # global redirect to https
      - "traefik.http.routers.redirs.rule=hostregexp(`{host:.+}`)"
      - "traefik.http.routers.redirs.entrypoints=web"
      - "traefik.http.routers.redirs.middlewares=redirect-to-https"

    environment:

      TRAEFIK_LOG_LEVEL: DEBUG
      TRAEFIK_ENTRYPOINTS_WEB: true
      TRAEFIK_ENTRYPOINTS_WEB_ADDRESS: ":80"
      TRAEFIK_ENTRYPOINTS_WEB_HTTP_REDIRECTIONS_ENTRYPOINT_TO: websecure

      TRAEFIK_ENTRYPOINTS_WEBSECURE: true
      TRAEFIK_ENTRYPOINTS_WEBSECURE_ADDRESS: ":443"
      TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_TLS_CERTRESOLVER: letsencrypt

      TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT: true
      # CHANGE THIS to desired email for ACME
      TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL: [email protected]
      TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_HTTPCHALLENGE: true
      TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_HTTPCHALLENGE_ENTRYPOINT: web
      TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_STORAGE: "/etc/traefik/acme/acme.json"

      # Since Traefik 3.6.3, paths with certain "encoded characters" are now blocked by default; we need a couple, or else things *will* break
      TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDSLASH: true
      TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDHASH: true

      TRAEFIK_PROVIDERS_DOCKER: true
      TRAEFIK_PROVIDERS_DOCKER_ENDPOINT: "unix:///var/run/docker.sock"
      TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT: false

volumes:
  db:
  acme:

networks:
  proxy: