How to deploy a tldraw app on a VPS
tldraw is a React SDK for building collaborative infinite-canvas apps, not a server application with a universal Docker image. This guide creates a custom tldraw project, containerises it with nginx and Docker, and serves it behind a reverse proxy on a VPS.
Prerequisites
- Ubuntu 22.04 or 24.04 VPS with SSH access
- A domain name (e.g.
canvas.example.com) pointing to your server IP - Docker and Docker Compose installed on the VPS
- Node.js 20+ and npm on your local machine or CI
- Basic knowledge of React and the command line
Important disclaimers
- tldraw is an SDK, not a server application. You deploy the app you build with it.
- The self-hosted app serves a single-user editor by default. State is lost on page refresh unless you add persistence.
- Real-time collaboration requires the
@tldraw/syncbackend. The recommended deployment target is Cloudflare Workers, not a VPS. - Production use of tldraw requires a license key from tldraw.dev. Review the SDK license before publishing.
- The SDK does not follow semantic versioning. If you use
@tldraw/sync, thetldrawand@tldraw/syncpackage versions must match exactly.
Step 1: Connect and set up the server
ssh root@YOUR_SERVER_IP
apt update && apt upgrade -y
apt install -y curl gitInstall Node.js 20.x (LTS):
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt install -y nodejs
node --version # v20.x
npm --versionStep 2: Build a tldraw app
On your local machine or CI, scaffold a new React project with Vite:
npm create vite@latest tldraw-canvas -- --template react
cd tldraw-canvas
npm install tldrawOpen src/App.jsx and replace its content with the minimal tldraw editor:
import { Tldraw } from "tldraw";
import "tldraw/tldraw.css";
export default function App() {
return (
<div style={{ position: "fixed", inset: 0 }}>
<Tldraw />
</div>
);
}Test the app locally:
npm run devOpen http://localhost:5173 in your browser. You should see a blank canvas with the tldraw toolbar.
Build the static assets for production:
npm run buildThe output goes to the dist/ folder.
Step 3: Dockerize the app
Create a Dockerfile in the project root:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]Create an nginx.conf file for single-page-app routing:
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}Update the Dockerfile to use this nginx config:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]Step 4: Run with Docker Compose
Create a docker-compose.yml file:
services:
canvas:
build: .
restart: unless-stopped
ports:
- "127.0.0.1:3000:80"Build and start the container:
docker compose build
docker compose up -d
docker compose psThe app is now running on 127.0.0.1:3000 and is only reachable from the server itself.
Step 5: Configure the reverse proxy
Install Nginx on the VPS:
apt install -y nginxCreate an Nginx site configuration at /etc/nginx/sites-available/canvas:
server {
listen 80;
server_name canvas.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Enable the site:
ln -s /etc/nginx/sites-available/canvas /etc/nginx/sites-enabled/
nginx -t
systemctl reload nginxSet up HTTPS with Certbot:
apt install -y certbot python3-certbot-nginx
certbot --nginx -d canvas.example.comStep 6: First access
Open https://canvas.example.com in your browser. You should see the tldraw editor.
- Draw a shape and refresh the page. The drawing will be lost because there is no persistence backend.
- Open the browser's developer console. You should see no network errors.
This is a single-user editor. All state lives in the browser tab.
Maintenance
- Update tldraw: Run
npm install tldraw@latest, rebuild the Docker image (docker compose build), and restart the container (docker compose up -d). - Update npm packages: Periodically run
npm outdatedandnpm update, then rebuild. - Rebuild on server: Clone your repo on the VPS and run
docker compose build && docker compose up -dto deploy new versions. - Backups: Since this setup does not persist data, there is no database to back up. If you add persistence, back up the database and any uploaded assets.
- Monitor: Check
docker compose logsfor container errors and Nginx access/error logs at/var/log/nginx/.
Key notes
- No WebSocket upgrade is needed for the static editor. WebSocket sync is only required when you add
@tldraw/sync, which runs on Cloudflare Workers, not on this VPS. - This guide produces a single-user demo. Adding accounts, document storage, and real-time collaboration are separate concerns that require additional infrastructure.
- tldraw loads fonts and icons from its CDN by default. For air-gapped deployments, you can self-host these assets.
- The SDK does not follow semver. When upgrading, always check the tldraw changelog for breaking changes and ensure
tldrawand@tldraw/syncversions match if you use both.
Tools mentioned
tldraw
โA fast collaborative infinite canvas plus a React SDK for building custom canvas experiences.
The multiplayer web whiteboard is free; the SDK is free in development and offers a 100-day production trial or hobby license.