Innovate | Connect | Transform
ContactService statusUSD $

GICT knowledge base

How to Self-Host Excalidraw on Your VPS (With Real-Time Collaboration)

Published

Self Hosted

Miro is a whiteboard tool. Excalidraw is also a whiteboard tool. The key difference is that Excalidraw is open source, drawings look hand-drawn by default, and you can run it on your own server for free. No per-seat fees. No team plans. No enterprise negotiations. Your whiteboard, your server, your rules.

Excalidraw at excalidraw.com stores drawings in your browser by default. If you self-host it, you get the same clean interface with real-time collaboration. Multiple people draw on the same board simultaneously, with live cursors, in-canvas updates, and end-to-end encryption between participants.

Here is how to deploy Excalidraw with collaboration on Ubuntu 24.04, using Docker and Caddy for automatic SSL.

What You Need Before Starting

  • Ubuntu 24.04 VPS with at least 1GB RAM
  • A registered domain with two subdomains pointed at your VPS IP
  • Docker and Docker Compose installed on the server

You need two subdomains because Excalidraw separates its web interface from its collaboration server. One subdomain serves the whiteboard app. The other handles real-time WebSocket connections between collaborators.

Point these two A records at your VPS IP before you start:

  • draw.yourdomain.com – the whiteboard app itself
  • collab.yourdomain.com – the real-time collaboration server

Replace yourdomain.com with your actual domain throughout the steps below. Both DNS records must be active before you run the installation steps.

Note on testing without a domain: You can use nip.io for testing without registering a domain. Any subdomain of X-X-X-X.nip.io resolves to the IP X.X.X.X. However, nip.io does not support SSL certificates from Let’s Encrypt, and Excalidraw’s collaboration features require HTTPS to work (the browser WebCrypto API requires a secure context). For full collaboration support, use a real registered domain.

Step 1: Install Docker

If Docker is not installed on your VPS yet, install it from the official Docker repository. Run each line separately:

sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --batch --yes --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER

Log out and back in for the docker group to take effect, or run newgrp docker to activate it in your current session. Verify the installation:

docker --version
docker compose version

You should see Docker version 20 or higher and Docker Compose version 2.

Step 2: Create the Project Directory

Create a folder to hold your Excalidraw configuration:

mkdir -p ~/excalidraw && cd ~/excalidraw
mkdir -p caddy_data caddy_config

The two Caddy directories store SSL certificates and configuration. They persist across restarts so Caddy does not need to request new certificates every time the container starts.

Step 3: Write the Docker Compose File

Create ~/excalidraw/docker-compose.yml with this content:

services:

  excalidraw:
    image: excalidraw/excalidraw:latest
    container_name: excalidraw
    restart: unless-stopped
    expose:
      - "80"
    environment:
      - NODE_ENV=production
      - VITE_APP_WS_SERVER_URL=https://collab.yourdomain.com
    entrypoint: /bin/sh
    command:
      - -c
      - |
        echo "Patching hardcoded collab URL with: $$VITE_APP_WS_SERVER_URL"
        find /usr/share/nginx/html/assets -type f -name "*.js" \
          -exec sed -i "s|https://oss-collab\.excalidraw\.com|$$VITE_APP_WS_SERVER_URL|g" {} +
        echo "Starting nginx..."
        nginx -g 'daemon off;'
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:80/ >/dev/null 2>&1 || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s
    security_opt:
      - no-new-privileges:true
    networks:
      - excalidraw_net

  excalidraw-room:
    image: excalidraw/excalidraw-room:latest
    container_name: excalidraw-room
    restart: unless-stopped
    expose:
      - "80"
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:80/ >/dev/null 2>&1 || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s
    security_opt:
      - no-new-privileges:true
    networks:
      - excalidraw_net

  caddy:
    image: caddy:2-alpine
    container_name: excalidraw-caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./caddy_data:/data
      - ./caddy_config:/config
    networks:
      - excalidraw_net
    depends_on:
      - excalidraw
      - excalidraw-room

networks:
  excalidraw_net:
    driver: bridge

Replace yourdomain.com in the VITE_APP_WS_SERVER_URL line with your actual domain.

Step 4: Write the Caddyfile

Create ~/excalidraw/Caddyfile with this content:

draw.yourdomain.com {
    reverse_proxy excalidraw:80
}

collab.yourdomain.com {
    reverse_proxy excalidraw-room:80
}

Replace yourdomain.com with your actual domain. Caddy reads this file at startup and automatically provisions Let’s Encrypt SSL certificates for both subdomains. No extra configuration is needed.

Step 5: Start the Stack

Pull the images and start all three containers:

cd ~/excalidraw
docker compose pull
docker compose up -d

Give the containers a moment to start, then check the Excalidraw logs:

docker compose logs excalidraw | head -20

Look for the line that says Patching hardcoded collab URL with: https://collab.yourdomain.com. This confirms the sed command replaced the hardcoded collaboration URL in the JavaScript bundle. If you do not see this line, the entrypoint did not run correctly and collaboration will not work.

Check the Caddy logs:

docker compose logs caddy | tail -20

Look for lines showing SSL certificate acquisition. Caddy obtains certificates automatically from Let’s Encrypt. If you see errors about certificate issuance, the most common cause is that your DNS A records have not propagated yet. Verify DNS is working from your local machine:

dig +short draw.yourdomain.com
dig +short collab.yourdomain.com

Both commands should return your VPS IP address. If they do not, wait for DNS to propagate and try again.

The Hardcoded URL Gotcha

Excalidraw’s Docker image bakes the collaboration server URL into the JavaScript at build time. Setting VITE_APP_WS_SERVER_URL as an environment variable does not work because the value is already embedded in the compiled files.

The entrypoint in the compose file works around this by running a sed command that replaces the hardcoded https://oss-collab.excalidraw.com string in every JavaScript file with your collab subdomain URL, at container startup. This means every time the container starts, it patches the bundle with your URL before nginx starts serving it.

The double dollar signs ($$VITE_APP_WS_SERVER_URL) are required. Docker Compose interpolates $VAR before passing the command to the shell. Using $$ escapes the first dollar sign so the shell inside the container sees a single $ and reads the actual environment variable value.

Test the Whiteboard

Open your browser and navigate to https://draw.yourdomain.com. You should see the Excalidraw whiteboard interface. Click anywhere on the canvas to start drawing.

To test collaboration, open the same URL in a second browser tab or an incognito window. Click Live collaboration in the left sidebar. Share the generated link with another person or open it in another tab. You should see two cursors moving independently on the canvas with changes appearing in real time for both users.

If collaboration does not connect, open the browser developer console (F12) and look for WebSocket errors. The most common issue is that the sed patch did not run. Check docker compose logs excalidraw for the patch confirmation line. If it is missing, restart the container:

docker compose restart excalidraw
docker compose logs excalidraw | grep patch

Make sure both draw.yourdomain.com and collab.yourdomain.com resolve correctly from your browser machine. If either domain shows a certificate error, Caddy’s certificates have not fully propagated yet.

Enable Basic Authentication (Optional)

If you want to password-protect your whiteboard so only invited people can access it, add basic authentication to the Caddyfile:

draw.yourdomain.com {
    basic_auth {
        yourusername JDJhJDE0JE9IMXI3Lk15cGRBdkQuTkw4RGF5V2RLUmQuT3cybEVDL3hDWHBhVXhVcE1MZVRoRjkuZW1GZGQ1
    }
    reverse_proxy excalidraw:80
}

Generate your own password hash using this command on any machine with Docker:

docker run -it caddy:2-alpine caddy hash-password

Enter your desired password when prompted. Copy the hash it outputs and paste it into the Caddyfile in place of the example hash. Restart Caddy:

docker compose restart caddy

Note: Basic authentication protects the whiteboard app only. The collaboration server at the collab subdomain has no built-in authentication. Anyone with a collaboration link can join a drawing session. This is fine for small teams and trusted groups.

Keep Excalidraw Updated

Update to the latest version with one command:

cd ~/excalidraw
docker compose pull
docker compose up -d

The sed entrypoint reapplies automatically on every startup, so updating the image does not break the collaboration URL patch.

There is no server-side database to back up. Drawings are stored in each browser’s local storage and in the collaboration server’s memory during active sessions. Excalidraw supports exporting drawings as .excalidraw JSON files, PNG, or SVG images directly from the app.

Need a server to run this on? InterServer VPS plans start at $6/month with the resources needed to run Excalidraw and its collaboration server. Full root access, unmetered bandwidth, and Ubuntu 24.04 ready to go.

Related guides

Account services are unavailable until the production backend is configured.