A self-contained, production-oriented system that accepts .laz / .las LiDAR files, converts them into a web-optimized Potree 2 octree, and renders them in an interactive React point cloud viewer.
Everything runs with a single command:
docker compose up --buildNo manual installation of PDAL, PotreeConverter, PostgreSQL, or Redis is required on the host.
- Features
- Architecture
- Technology stack
- Prerequisites
- Quick start
- Step-by-step usage
- Docker services
- Project structure
- Processing pipeline
- API reference
- Configuration
- Local development
- Troubleshooting
- Security
| Area | Capabilities |
|---|---|
| Upload | Drag-and-drop, resumable chunked uploads (multi-GB), progress tracking, cancellation |
| Validation | Extension/MIME checks, LAS signature, PDAL integrity, point-count limits |
| Processing | Async Celery jobs, PDAL metadata extraction, PotreeConverter 2 octree generation |
| Viewer | Orbit/zoom/pan, elevation & classification coloring, clipping, measurement, FPS overlay |
| Ops | Health checks, structured logging, WebSocket job progress, OpenAPI docs |
flowchart TB
subgraph Browser
UI[React SPA]
end
subgraph Docker["Docker Compose"]
NGX[Nginx :8080]
API[FastAPI API]
WRK[Celery Worker]
PG[(PostgreSQL)]
RD[(Redis)]
FE[Frontend static]
VOL[(storage volume)]
end
UI --> NGX
NGX --> FE
NGX --> API
NGX -->|/tiles range requests| VOL
API --> PG
API --> RD
API --> VOL
WRK --> PG
WRK --> RD
WRK -->|PDAL + PotreeConverter| VOL
WRK -->|progress| RD
API -->|WebSocket| RD
Request flow (upload → view):
- User uploads
.lazvia the web UI (chunked or single-shot). - API validates the file and stores it under
/storage/uploads/. - A Celery task is queued in Redis.
- The worker runs PDAL (metadata, CRS, validation) then PotreeConverter 2 (octree).
- Output is written to
/storage/processed/{job_id}/potree/. - Nginx serves tiles with HTTP range requests (required for Potree 2).
- The React viewer loads
metadata.jsonvia@pnext/three-loader(Potree v2) with LOD streaming.
| Layer | Technologies |
|---|---|
| API | Python 3.12, FastAPI, SQLAlchemy, Alembic, Pydantic, SlowAPI |
| Worker | Celery, PDAL (conda-forge), PotreeConverter 2.1.2 |
| Database | PostgreSQL 16 |
| Queue | Redis 7 |
| Proxy | Nginx (upload limits, rate limiting, tile caching, range requests) |
| Frontend | React 18, TypeScript, Vite, Tailwind CSS, Zustand |
| 3D viewer | Three.js, React Three Fiber, @pnext/three-loader (Potree 2) |
| Containers | Docker Compose, multi-stage Dockerfiles |
Rendering format: Potree 2 octree (metadata.json, hierarchy.bin, octree.bin) — chosen for proven LOD, frustum culling, and streaming at 10M–500M+ points.
- Docker Desktop (or Docker Engine + Compose v2) — must be running
- 8 GB+ RAM recommended (worker + PDAL + PotreeConverter are memory-heavy)
- Disk space for LiDAR uploads and processed tiles (multi-GB per file is normal)
Apple Silicon (M1/M2/M3): The worker image uses platform: linux/amd64 because PotreeConverter only ships x86_64 Linux binaries. Docker runs this under emulation (slower but functional).
cd PointCloudConverter
cp .env.example .envEdit .env if needed (defaults work for local use). Change SECRET_KEY before any public deployment.
docker compose up --buildThe first build often takes 10–20 minutes (downloads PDAL via conda-forge, PotreeConverter binary, npm build).
Wait until logs show:
api—Application startup completeworker—celery@... readynginx— healthy
| URL | Description |
|---|---|
| http://localhost:8080 | Web UI (upload, jobs, viewer) |
| http://localhost:8080/api/docs | Swagger / OpenAPI |
| http://localhost:8080/health | Health check (API + Postgres + Redis) |
docker compose up --build -ddocker compose downRemove all data (database + uploads + processed clouds):
docker compose down -v- Open http://localhost:8080
- Drag and drop a
.lazor.lasfile onto the upload area (or click to browse). - Watch upload progress on the same page.
- When upload completes, the job is queued for processing (WebSocket updates).
Tips:
- Start with a smaller file (50–200 MB) to verify the pipeline before multi-GB uploads.
- Large files use 16 MB chunks by default (
CHUNK_SIZE_BYTESin.env).
- Go to Jobs in the navigation bar.
- Status flows:
uploading→queued→processing→completed(orfailed). - The list auto-refreshes every 5 seconds.
- On the Jobs page, click View on a
completedjob. - Use the sidebar controls:
- Point budget — max points rendered (lower if the browser is slow)
- Point size — visual size of points
- Color mode — Elevation, Classification, or RGB (if available)
- Elevation clipping — slice by height
- Measurement tool — click two points for distance
- Reset camera — reframe the cloud
Viewer tips:
- Use Elevation or Classification if the file has no RGB.
- If Chrome shows “Aw, Snap!”, lower the point budget to 500k, close other tabs, hard-refresh (Cmd+Shift+R), and ensure you rebuilt the frontend after the latest fixes.
On the Jobs page, click Delete to remove the job record and associated files from storage.
| Service | Image / build | Role | Host port |
|---|---|---|---|
| nginx | docker/nginx |
Reverse proxy, static UI, API, WebSocket, /tiles |
8080 → 80 |
| api | docker/api |
REST API, WebSocket, Alembic migrations | internal |
| worker | docker/worker |
Celery + PDAL + PotreeConverter | internal |
| frontend | docker/frontend |
Built React SPA (served via nginx) | internal |
| postgres | postgres:16-alpine |
Jobs, metadata, logs | internal |
| redis | redis:7-alpine |
Celery broker + progress pub/sub | internal |
Volumes:
| Volume | Purpose |
|---|---|
postgres_data |
Database persistence |
redis_data |
Redis AOF persistence |
storage_data |
Uploads, processed Potree tiles, metadata JSON |
Useful commands:
# Service status
docker compose ps
# Follow logs
docker compose logs -f api worker nginx
# Rebuild one service after code changes
docker compose build worker
docker compose up -d worker
# Rebuild frontend only
docker compose build frontend && docker compose up -d frontend nginxPointCloudConverter/
├── docker-compose.yml # Orchestrates all services
├── .env.example # Environment template
├── README.md
│
├── docker/
│ ├── api/ # API Dockerfile + entrypoint (migrations, uvicorn)
│ ├── worker/ # Worker Dockerfile (Ubuntu 24.04, PDAL, PotreeConverter)
│ ├── frontend/ # Frontend build + nginx SPA config
│ └── nginx/ # Reverse proxy config
│
├── backend/
│ ├── pyproject.toml
│ ├── alembic/ # Database migrations
│ └── app/
│ ├── main.py # FastAPI application
│ ├── config.py
│ ├── database.py
│ ├── models/ # Upload, metadata, processing logs
│ ├── schemas/ # Pydantic request/response models
│ ├── api/routes/ # upload, jobs, health
│ ├── services/ # storage, validation, upload, progress
│ ├── worker/ # Celery app + tasks
│ │ └── pipeline/ # pdal_metadata.py, potree_convert.py
│ └── websocket/ # Job progress WebSocket
│
├── frontend/
│ ├── package.json
│ ├── vite.config.ts
│ └── src/
│ ├── pages/ # UploadPage, JobsDashboard, ViewerPage
│ ├── components/viewer/ # Potree / R3F viewer
│ ├── api/client.ts # API client
│ └── store/ # Zustand state
│
└── storage/ # Bind-mounted via Docker volume at runtime
├── uploads/
├── processed/ # {job_id}/potree/metadata.json, octree.bin, ...
├── metadata/
└── logs/
| Step | Component | Action |
|---|---|---|
| 1 | API | Receive upload (chunked or single), store as {uuid}.laz |
| 2 | API | Validate extension, size, MIME |
| 3 | API | Enqueue Celery task process_pointcloud |
| 4 | Worker | Verify LAS LASF signature |
| 5 | Worker | PDAL — full file validation + metadata (bounds, EPSG, point count, elevation, classifications) |
| 6 | Worker | PotreeConverter 2 — build octree LOD |
| 7 | Worker | Save to /storage/processed/{job_id}/potree/ |
| 8 | Worker | Update PostgreSQL (status=completed, metadata row) |
| 9 | Worker | Publish progress via Redis → WebSocket |
| 10 | Nginx | Serve tiles at /tiles/{job_id}/potree/ with byte-range support |
| 11 | Browser | Stream LOD nodes via @pnext/three-loader |
Retries: Failed tasks retry up to 3 times with exponential backoff (CELERY_TASK_MAX_RETRIES).
Base URL: http://localhost:8080
| Method | Path | Description |
|---|---|---|
POST |
/api/upload/init |
Start resumable upload → returns job_id, chunk_size |
PUT |
/api/upload/{job_id}/chunk/{index} |
Upload one chunk (raw body) |
POST |
/api/upload/{job_id}/complete |
Finalize upload and enqueue processing |
DELETE |
/api/upload/{job_id} |
Cancel in-flight upload |
POST |
/api/upload |
Single-shot upload (multipart file) |
GET |
/api/job/{job_id} |
Get job status and progress |
GET |
/api/jobs |
List jobs (?page=1&page_size=20) |
GET |
/api/metadata/{job_id} |
Point cloud metadata (bounds, EPSG, counts, …) |
GET |
/api/pointcloud/{job_id} |
Viewer manifest (tile URLs) |
DELETE |
/api/job/{job_id} |
Delete job and files |
GET |
/health |
Liveness + Postgres + Redis checks |
WS |
/ws/jobs/{job_id} |
Real-time job progress (JSON messages) |
Interactive documentation: http://localhost:8080/api/docs
Example — check health:
curl http://localhost:8080/healthExample — list jobs:
curl http://localhost:8080/api/jobsCopy .env.example to .env:
cp .env.example .env| Variable | Default | Description |
|---|---|---|
SECRET_KEY |
(change me) | App secret — must change in production |
MAX_UPLOAD_BYTES |
10737418240 (10 GB) |
Maximum upload size |
CHUNK_SIZE_BYTES |
16777216 (16 MB) |
Resumable upload chunk size |
MAX_POINT_COUNT |
2000000000 |
Reject files exceeding this point count |
POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB |
pointcloud / … |
Database credentials |
POTREE_SPACING |
0.0 |
PotreeConverter spacing (0 = auto; increase to subsample for web) |
WORKER_CONCURRENCY |
1 |
Parallel Celery tasks per worker container |
CELERY_TASK_MAX_RETRIES |
3 |
Processing retry attempts |
RATE_LIMIT_PER_MINUTE |
120 |
API rate limit (also nginx limit_req) |
CORS_ORIGINS |
localhost variants | Allowed browser origins |
VITE_API_BASE_URL |
empty | Leave empty when using nginx on same host |
Docker is the recommended path. For native development:
Requires Python 3.12, PostgreSQL, Redis, PDAL, and PotreeConverter on PATH.
cd backend
pip install .
# Set DATABASE_URL, REDIS_URL, etc. in .env or environment
alembic upgrade head
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000Celery worker (separate terminal):
cd backend
celery -A app.worker.celery_app worker --loglevel=INFO --concurrency=1cd frontend
npm install
npm run devVite dev server proxies /api, /ws, and /tiles to http://localhost:8080 — run the full Docker stack (or nginx) for API and tiles.
# Terminal 1 — infrastructure + API
docker compose up postgres redis api worker nginx
# Terminal 2 — hot-reload UI
cd frontend && npm run devOpen http://localhost:5173 (Vite) or http://localhost:8080 (Docker nginx).
Docker is not running, or the web stack is stopped.
docker compose ps # nginx, api, frontend should be "Up"
docker compose up -d # start all servicesOnly starting the worker does not expose the UI:
docker compose up -d # not: docker compose up -d workerDatabase was left in a partial state from a failed first run.
docker compose down -v
docker compose up --buildRebuild the worker image (Ubuntu 24.04 + libtbb + PotreeConverter 2.1.2):
docker compose build worker
docker compose up -d workerCheck worker logs:
docker compose logs worker --tail 100Common causes: corrupt LAZ, unsupported point format, out-of-memory during conversion. Re-upload after fixing the file or lowering dataset size.
- Confirm job status is
completed. - Hard-refresh the browser (Cmd+Shift+R).
- Rebuild frontend:
docker compose build frontend && docker compose up -d frontend nginx - Verify tiles:
curl -I http://localhost:8080/tiles/{job_id}/potree/metadata.json
Usually GPU/memory pressure from the 3D viewer.
- Rebuild frontend (includes reload-loop fix).
- Quit Chrome completely and reopen.
- Lower Point budget to 500k in the viewer sidebar.
- Close other heavy tabs.
Expected — worker runs x86_64 under emulation. Processing still works; large files take longer.
- Upload validation (extension, MIME, LAS signature, PDAL probe, point-count ceiling)
- UUID-based storage paths (no user-controlled filenames on disk)
- Rate limiting (nginx + SlowAPI)
- Security headers (CORS,
X-Content-Type-Options, etc.) - Non-root container users (API); worker runs as UID 1000
- Auth-ready API structure (JWT can be added; not enabled in v1)
Before production: change SECRET_KEY, use strong Postgres passwords, restrict CORS_ORIGINS, put TLS in front of nginx, and consider virus scanning for uploads.
Portfolio / demonstration project. See repository for license terms if applicable.