If you can open your Matrix homeserver but do not want users to rely on the public Element website, you can host Element Web as a static site on your own Nginx server. The files are served by Nginx; the Matrix homeserver remains a separate service that handles accounts, rooms, messages, and federation. This distinction matters: pointing the web client at Synapse does not install or configure Synapse.
This beginner walkthrough uses chat.example.org for the Element site and matrix.example.org for the homeserver. Replace both with your real hostnames. Commands assume a Debian- or Ubuntu-style Linux server; package paths and service names vary by distribution.
What you need before you begin
Have these items ready before changing server files:
- A domain or subdomain for the web client, with DNS pointing to the Nginx host.
- A working Matrix homeserver reachable at an HTTPS URL, such as
https://matrix.example.org.
- Nginx installed, with permission to edit its site configuration and reload the service.
- A valid TLS certificate for the web-client hostname. Users should log in over HTTPS because their browser sends credentials and session tokens to the site.
- A current stable Element Web release archive from the official project. Avoid deploying a development build to a public service unless you specifically intend to run development software.
Element Web is a browser application made of static files. The project’s current installation instructions recommend downloading a release, unpacking it on the server, pointing the web server at the resulting files, and configuring config.json. On Debian and Ubuntu, the project also documents an Element Web package that installs its web root under /usr/share/element-web and its configuration under /etc/element-web/config.json. The steps below use the release archive so you can choose a dedicated document root.
Review Element Web’s installation instructions and official releases before selecting a version. If you use the Debian package instead, point Nginx at the package’s documented web root and edit the package configuration file; do not copy the archive paths below unchanged.
1. Download and install the Element Web files
A document root is the directory Nginx serves when a visitor opens your site. Create a dedicated root and unpack the release there. Substitute the actual archive and directory names you downloaded; release versions change over time.
sudo install -d -o root -g www-data -m 0755 /var/www/element
tar -xzf element-vX.Y.Z.tar.gz
ls
The archive normally unpacks to a versioned directory. Copy the contents of that directory, including index.html, JavaScript bundles, fonts, images, and the sample configuration, into the Nginx document root. For example, if the extracted directory is named element-vX.Y.Z:
sudo cp -a element-vX.Y.Z/. /var/www/element/
sudo find /var/www/element -type d -exec chmod 755 {} \;
sudo find /var/www/element -type f -exec chmod 644 {} \;
ls -l /var/www/element/index.html /var/www/element/config.sample.json
Use the real versioned directory name in the copy command. Nginx needs read access to these files, not write access. Keep a copy of the current release and configuration before upgrades so you can restore them if a deployment fails.
2. Configure the client to use your homeserver
Element needs a homeserver address before it can start. Copy the sample file and set the default Matrix client API URL. In this example, Synapse is reachable at https://matrix.example.org:
cd /var/www/element
sudo cp config.sample.json config.json
sudoedit /var/www/element/config.json
Edit /var/www/element/config.json and make sure it contains a valid JSON object with a default_server_config entry, for example:
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://matrix.example.org"
}
}
}
This sets the server Element offers by default. It does not create a Synapse account or force every user to select this server. The current Element configuration guide documents default_server_config as the preferred way to supply homeserver connection information. The older default_hs_url option is deprecated. If you intend to restrict users to one server, Element documents a separate disable_custom_urls setting; decide whether that is appropriate for your community before enabling it.
You can also configure limited customization, such as a default theme or certain branding details, using documented Element options. This is not the same as a complete white-label replacement: the project documents selected customization settings rather than unrestricted rebranding. Check the configuration guide for options supported by the exact release you install; do not copy settings from a different version without checking them.
See Element Web’s current configuration reference. The develop documentation can change as the project evolves, so confirm any optional key against the release you deploy.
3. Add an Nginx site for the web client
Create a site configuration for your client hostname. This example assumes you already have a certificate for chat.example.org at the shown certificate paths. Certificate issuance is not included because the right method depends on your host, DNS provider, and existing TLS setup.
server {
listen 80;
server_name chat.example.org;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name chat.example.org;
ssl_certificate /etc/letsencrypt/live/chat.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/chat.example.org/privkey.pem;
root /var/www/element;
index index.html;
# Revalidate the client files after deployments.
add_header Cache-Control "no-cache" always;
# Element Web hosting security headers.
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Content-Security-Policy "frame-ancestors 'self'" always;
location / {
try_files $uri $uri/ =404;
}
}
Put the block in the appropriate site file for your distribution, enable that site if required, and check that the global Nginx configuration loads the standard mime.types file. Nginx uses MIME types to tell browsers whether a response is JavaScript, CSS, JSON, or another kind of file. The root must point to the directory that directly contains index.html.
The no-cache header above is a simple deployment-friendly baseline: browsers revalidate files after an update rather than continuing to use stale client code. Element’s hosting notes specifically require that /config.*.json, /i18n, /version, and /index.html not be cached, and recommend revalidation for the site root. If you later add longer-lived caching for large static assets, keep those paths fresh and verify the release’s asset naming before creating cache rules.
Element’s web README recommends anti-framing and content-type headers for self-hosted sites. If you already set a Content Security Policy (CSP) elsewhere, add frame-ancestors 'self' to that existing policy instead of sending a second CSP header. A restrictive CSP can block required application scripts, so do not copy a broad policy without testing it against the deployed client.
Read Element Web’s hosting and caching requirements and Nginx’s official core module documentation for the behavior of root, try_files, and static-file handling.
4. Validate the configuration and reload Nginx
Test the configuration before applying it. If Nginx reports an error, keep the current process running while you fix the reported file and line; do not reload a broken configuration.
sudo nginx -t
sudo systemctl reload nginx
curl -I https://chat.example.org/
curl -I https://chat.example.org/config.json
The test should report that the syntax is okay and the configuration test succeeded. The HTTPS requests should reach the intended host and return a successful response. Confirm the configuration file itself is valid JSON as well:
curl -fsS https://chat.example.org/config.json | python3 -m json.tool
Then open https://chat.example.org in a browser. You should see the Element login or welcome screen with your Matrix homeserver as the default. Try a test account, check browser developer tools for failed network requests, and confirm that login, room list loading, and message sending work. A page loading successfully only proves that Nginx served the web client; it does not prove that the homeserver is reachable or correctly configured.
5. Make homeserver discovery work when needed
If your Matrix IDs use a server name such as example.org while the Synapse API is at matrix.example.org, Matrix clients can use a client well-known response at https://example.org/.well-known/matrix/client to discover the API URL. This optional discovery endpoint belongs on the domain in the Matrix ID, not automatically on the Element web-client hostname. A minimal response looks like this:
{
"m.homeserver": {
"base_url": "https://matrix.example.org"
}
}
Serve it as JSON with the CORS headers required for browser-based clients. Synapse’s installation documentation gives an Nginx example and says its public_baseurl should match the URL clients use to reach Synapse. If your Element config directly sets the correct homeserver URL and your users enter that client-specific setting, discovery may not be needed for this basic setup.
Follow Synapse’s client well-known guidance and test the final URL with curl before expecting user-ID discovery to work. For security isolation, Synapse’s security guidance recommends hosting the homeserver on a different registered domain from sensitive web applications where practical; separate subdomains offer some protection, but a separate registered domain is the stronger recommendation.
Common problems to avoid
- Nginx shows a 403: check file and directory read permissions and confirm the Nginx worker can traverse each parent directory.
- The page is blank or returns 404 for JavaScript: make sure
root points to the extracted release directory contents, not one directory above or below them. Check the Nginx error and access logs.
- Element loads but cannot connect or log in: verify the
base_url in config.json, the homeserver’s HTTPS certificate, firewall access, and any Synapse-side CORS or reverse-proxy configuration. CORS is enforced by browsers; an Nginx static site cannot fix a homeserver origin policy by itself.
- Recent changes do not appear: check the cache headers for
/, /index.html, /config.json, /version, and /i18n, then hard-refresh the browser after confirming the deployed files were replaced.
- Login page works on HTTP only: finish TLS setup and redirect HTTP to HTTPS before inviting users to sign in.
Final deployment checklist
- The Element Web hostname resolves to the Nginx server and has a valid HTTPS certificate.
- The document root contains the release’s
index.html, assets, and a valid config.json.
- The JSON’s
m.homeserver.base_url points to the reachable HTTPS client API for your Matrix homeserver.
sudo nginx -t passes, and Nginx serves both the home page and config.json over HTTPS.
- The browser can sign in and make a real request to the homeserver without certificate, CORS, or missing-file errors.
- Your cache rules allow new client files and configuration to take effect after an upgrade.
Once these checks pass, the custom Element Web client is being served by Nginx and can communicate with the Matrix homeserver. Keep Element Web updated, retain a rollback copy of the previous release and configuration, and recheck the homeserver URL and browser network requests after each upgrade.
Official references