Analytics cookies only (Google Analytics, PostHog). No ad networks, no cross-site tracking. See our privacy policy.
Updated Oct 2026
verified on Ubuntu 26.04 · Oct 2026Self-host Excalidraw, the hand-drawn virtual whiteboard, on a VPS as a tiny static Nginx container behind Caddy HTTPS, with a digest-pinned image, a backup plan that fits an app with no server-side storage, and an honest note on what still calls excalidraw.com.
Excalidraw is a virtual whiteboard that draws everything with a hand-sketched look: boxes, arrows, freehand lines and text that read like a diagram on a napkin. It is written in TypeScript with React, licensed MIT, and rated 1 / 5 to deploy, which is as low as the scale goes.
The reason it is that easy is the way the official image is built: the web app is compiled once and served as plain files by Nginx. There is no database, no application server and no user accounts. You run one small container, and every visitor's browser does the actual work, keeping drawings in the browser's own storage. This guide puts that container on a VPS behind Caddy for HTTPS, pins the image so an upgrade is a decision rather than an accident, and is upfront about what "self-hosted" does and does not cover for this particular app. For picking a provider and tier, see Best VPS for self-hosting.
Excalidraw is about the lightest thing in the catalog. The catalog's own measurement of the container at idle is a few megabytes of RAM, and the image is about 137 MB on disk. In our verification run on an Ubuntu 26.04 server the Excalidraw container used about 4 MB of RAM and Caddy about 14 MB at idle, so the smallest tier any provider sells is enough. Because the server only hands out static files, load scales with how many people open the page, not with how many are drawing: all the drawing happens in their browsers.
There is nothing to size for storage either. No drawings are saved on the server, which is the main thing to understand before you rely on it. The Backups section covers what that means in practice.
Update the system and install the few tools this guide uses:
sudo apt-get update && sudo apt-get -y upgrade
sudo apt-get install -y curl ufw dnsutils
If you are logged in as root, create a non-root sudo user and continue as that user:
adduser deploy
usermod -aG sudo deploy
Then open only SSH and the two ports the reverse proxy needs:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
Excalidraw will be reachable only through the reverse proxy. That matters on a
Docker host, because a published container port goes around ufw entirely, so
the Compose file below publishes nothing for Excalidraw at all. Caddy is the
only public door.
Paid link — we earn a commission if you shop through it.
If Docker isn't on the server yet, the convenience script installs Docker Engine and the Compose plugin (the Docker and Compose on Ubuntu guide covers the details):
command -v docker >/dev/null || curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER"
sudo docker compose version
The group change applies on your next login; until then the commands below use
sudo docker.
The catalog's quick start runs docker run ... excalidraw/excalidraw:latest.
That is fine for a first look, but for a server you keep, latest is a moving
target: the upstream image publishes no version tags (apart from latest, its tags are
old build hashes and one stray branch name), so the only way to pin a release is by
digest. The Compose file below does that. Set your hostname first (this
guide uses draw.example.com; replace it with yours):
mkdir -p ~/excalidraw && cd ~/excalidraw
echo "DOMAIN=draw.example.com" > .env
cat > docker-compose.yml <<'EOF'
services:
excalidraw:
image: excalidraw/excalidraw@sha256:f7ee194addd607bf831d2af0f0a34463dd4225e426cf35199ef0b12a803398e9
restart: unless-stopped
caddy:
image: caddy:2
restart: always
ports: ["80:80", "443:443"]
environment:
DOMAIN: ${DOMAIN}
volumes: ["./Caddyfile:/etc/caddy/Caddyfile:ro", "caddy_data:/data"]
depends_on: [excalidraw]
volumes:
caddy_data:
EOF
cat > Caddyfile <<'EOF'
{$DOMAIN} {
encode zstd gzip
reverse_proxy excalidraw:80
}
EOF
Start it and wait for the container to report healthy. The image ships its own health check, and the loop is bounded so a broken install fails instead of hanging:
cd ~/excalidraw
sudo docker compose up -d
for i in $(seq 1 30); do
state=$(sudo docker inspect --format '{{.State.Health.Status}}' excalidraw-excalidraw-1 2>/dev/null)
[ "$state" = "healthy" ] && break
sleep 2
done
echo "excalidraw container health: $state"
sudo docker compose ps
Create an A record for your hostname (say draw.example.com) pointing at
the server's public IP, and confirm it resolves before Caddy tries to get a
certificate:
dig +short draw.example.com
Caddy is already running. It requests the certificate as soon as the name resolves and renews it on its own (see Automatic HTTPS with Caddy for how that works). Check that HTTPS answers, retrying while the certificate is issued:
. ~/excalidraw/.env
out=000
for i in $(seq 1 20); do
out=$(curl -s -o /dev/null -w '%{http_code} %{ssl_verify_result}' "https://$DOMAIN/") && [ "${out%% *}" = "200" ] && break
sleep 5
done
echo "HTTPS status and certificate check: $out"
[ "${out%% *}" = "200" ] || false
curl -s "https://$DOMAIN/" | grep -o '<title>[^<]*</title>'
A second number of 0 means the certificate verified, and the title line
confirms you are looking at the whiteboard and not Caddy's default page. Open
the address in a browser and you can draw immediately; there is no setup screen
and no login.
This is the one thing the one-line install snippet does not tell you. Self-hosting Excalidraw gets you the editor on your own domain. It does not get you the services the hosted site uses around the editor, because the official image is compiled with the project's public endpoints baked into the JavaScript. We checked the files inside the image rather than assuming:
cd ~/excalidraw
sudo docker compose exec -T excalidraw sh -c \
'grep -rhoE "https://(oss-collab|json|libraries)\.excalidraw\.com" /usr/share/nginx/html | sort | uniq -c'
In our run that listed oss-collab.excalidraw.com (live collaboration),
json.excalidraw.com (shareable links) and libraries.excalidraw.com (the
public shape library). In practice that means three things. Drawing, exporting
to PNG or SVG and saving .excalidraw files work entirely on your server and
in the browser. Shareable links and live-collaboration rooms would route
through the upstream hosted services rather than your box, so treat those
features as not self-hosted. And if keeping everything off third-party servers
is the reason you are here, run it for private solo drawing and skip the share
and collaborate buttons. A fully self-contained collaboration setup means
building the app yourself with your own collaboration server and storage
configured, which is a different and much bigger project than this guide.
Mixed-in third-party calls are also why the catalog describes the stock image
as having no sharing or collaboration when self-hosted.
The server holds nothing worth backing up beyond its configuration, because
Excalidraw keeps the canvas in the visitor's browser storage (local
storage and IndexedDB). That has two consequences. Clearing site data, using a
private window or switching browsers loses the drawing, and there is no copy on
your server to recover from. So the real backup is the file: use the menu's
Save to disk to download a .excalidraw file, which is plain JSON you can
put in version control or object storage, and tell your team to do the same for
anything that matters.
What the server does have is its setup, which you can archive in one command:
cd ~/excalidraw
( umask 077; tar czf ~/excalidraw-config-$(date +%F).tar.gz docker-compose.yml Caddyfile .env )
ls -lh ~/excalidraw-config-*.tar.gz
Keep that archive somewhere other than the server. Restoring is the reverse:
unpack it into an empty ~/excalidraw, run sudo docker compose up -d, and
Caddy fetches a fresh certificate on its own.
Because the image is pinned by digest, an upgrade is a deliberate edit. Pull the current image, read its digest, and put that in the Compose file:
cd ~/excalidraw
sudo docker pull excalidraw/excalidraw:latest
sudo docker image inspect excalidraw/excalidraw:latest --format '{{index .RepoDigests 0}}'
Replace the digest on the image: line with the one printed, then recreate the
container with sudo docker compose up -d. There is no data migration and no
database to back up first, so a bad release is rolled back by putting the old
digest back. Read the upstream
release notes before
jumping ahead, since a visual change to the editor is the kind of thing your
team will notice.
The site shows Caddy's default page or a 502. Run
sudo docker compose ps and sudo docker compose logs excalidraw. A 502 means
Caddy cannot reach the Excalidraw container, usually because it is still
starting or the service name in the Caddyfile no longer matches the one in
docker-compose.yml.
Caddy never gets a certificate. Confirm dig +short draw.example.com
returns the server's IP and that your provider's network-level firewall, not
just ufw, allows inbound 80 and 443. A blocked port 80 silently breaks the
ACME challenge.
My drawings disappeared. They were stored in that browser's local storage
and the data was cleared, or you opened the site under a different address.
Browser storage is per origin, so http:// and https:// or a changed
hostname each start with an empty canvas. Export to a .excalidraw file to
move work between them.
The share or collaborate buttons do nothing useful. That is the section above: those features depend on upstream hosted services, not on your server.
You're done when https://draw.example.com loads over a valid certificate,
you can draw and save a .excalidraw file, sudo docker compose ps shows both
containers up after a reboot (the restart policies bring them back), and the
configuration archive is stored off the server.
Last, the idle footprint, so you can compare your own box with ours:
sudo docker stats --no-stream --format '{{.Name}} {{.MemUsage}}'
From here, see the Excalidraw entry for catalog details, or Best VPS for self-hosting for the ranked server picks if you want to share the box with other tools.