Comprehensive deployment instructions for jlc-search, a fast search engine for JLCPCB/LCSC electronic components.
git clone https://github.com/casimir-engineering/jlc-search.git
cd jlc-search
cp .env.example .env
# Edit .env with your domain, email, and passwords
docker compose build
docker compose up -d
docker compose run --rm ingest # first-time data load (~15 min)- OS: Ubuntu 22.04+ or Debian 12+ (other Linux distros work, but
deploy.shusesufw) - RAM: 4 GB minimum (PostgreSQL is tuned for 1.5 GB shared buffers)
- Disk: 50 GB free (database + raw data + images)
- Docker: Docker Engine 24+ with the Compose V2 plugin (
docker compose) - Domain: A domain name with a DNS A record pointing to the server's public IP
- Ports: 80 (HTTP/Let's Encrypt) and 443 (HTTPS) open in your firewall
- Bun (optional): Only needed if running ingestion or development outside Docker
Internet
|
:443/:80 Nginx Proxy Manager (npm)
| - SSL termination (Let's Encrypt)
| - HTTP -> HTTPS redirect
|
:8080 Frontend (nginx)
| - Serves React SPA
| - Proxies /api/* to backend
|
:3001 Backend (Bun + Hono)
| - Search API, image proxy, footprint/schematic SVGs
| - Connects to PostgreSQL
|
:5432 PostgreSQL 17
- Full-text search (tsvector + pg_trgm)
- 446k+ parts indexed
All services use network_mode: host, so they bind directly to the host network.
| Service | Image | Port | Purpose |
|---|---|---|---|
db |
postgres:17-alpine | 5432 | PostgreSQL with tuned memory settings |
backend |
Custom (Bun) | 3001 | API server |
frontend |
Custom (nginx) | 8080 | Static SPA + reverse proxy to backend |
npm |
jc21/nginx-proxy-manager | 80, 443, 81 | SSL termination and proxy |
ingest |
Custom (Bun) | none | On-demand data ingestion (tools profile) |
| Volume | Contents |
|---|---|
pg_data |
PostgreSQL database files |
img_cache |
Cached product images from LCSC CDN |
raw_data |
Downloaded raw data (jlcparts, JLCPCB API, datasheets) |
npm_data |
Nginx Proxy Manager configuration |
npm_letsencrypt |
Let's Encrypt certificates |
Copy .env.example to .env and configure each variable:
cp .env.example .env| Variable | Description | Example |
|---|---|---|
POSTGRES_PASSWORD |
PostgreSQL password for the jlc user |
a-strong-random-password |
DATABASE_URL |
Full PostgreSQL connection string | postgres://jlc:a-strong-random-password@localhost:5432/jlc |
The password in DATABASE_URL must match POSTGRES_PASSWORD.
| Variable | Description | Default |
|---|---|---|
PORT |
Backend HTTP port | 3001 |
ALLOWED_ORIGINS |
Comma-separated CORS origins | http://localhost:3000 |
For production, set ALLOWED_ORIGINS to your domain: https://your-domain.com.
| Variable | Description | Default |
|---|---|---|
JLCPARTS_BASE |
jlcparts mirror URL | https://yaqwsx.github.io/jlcparts |
INGEST_CONCURRENCY |
Number of parallel download workers | 4 |
| Variable | Description | Example |
|---|---|---|
DOMAIN |
Your public domain name | search.example.com |
NPM_ADMIN_EMAIL |
NPM admin login email | admin@example.com |
NPM_ADMIN_PASS |
NPM admin login password | a-strong-password |
LETSENCRYPT_EMAIL |
Email for Let's Encrypt certificate notifications | you@example.com |
| Variable | Description | Default |
|---|---|---|
VITE_API_BASE |
API base URL baked into the frontend build | "" (relative, uses nginx proxy) |
Normally leave VITE_API_BASE empty. The frontend nginx config proxies /api/* to the backend on port 3001.
git clone https://github.com/casimir-engineering/jlc-search.git
cd jlc-searchcp .env.example .envEdit .env and set at minimum:
POSTGRES_PASSWORD-- a strong random passwordDATABASE_URL-- update the password to matchALLOWED_ORIGINS-- your production domain (https://your-domain.com)DOMAIN-- your domain nameLETSENCRYPT_EMAIL-- for SSL certificate issuance
docker compose builddocker compose up -dWait for the database health check to pass (takes a few seconds):
docker compose logs db --follow # watch for "database system is ready to accept connections"The first ingestion downloads the jlcparts mirror and populates the database. This takes roughly 15 minutes depending on your internet connection.
docker compose run --rm ingestThis runs the default ingest script which downloads jlcparts data and processes it into PostgreSQL.
For a more comprehensive ingestion including JLCPCB stock data and LCSC enrichment:
# Download all sources (no DB needed)
make download
# Process all downloaded data into PostgreSQL
make processOpen http://your-server-ip:8080 in a browser. You should see the search interface. Try searching for a component like 100nF 0402.
The deploy script handles building, starting services, and opening firewall ports:
./deploy.shThen run the NPM auto-configuration script to create the proxy host and request an SSL certificate:
make configure-npmThis script will:
- Create an admin user in NPM (first run only)
- Create a proxy host forwarding your domain to
127.0.0.1:8080 - Request a Let's Encrypt certificate (if
LETSENCRYPT_EMAILis set in.env) - Enable Force SSL, HTTP/2, and HSTS
-
Access the NPM admin UI:
- Local server:
http://localhost:81 - Remote server: Set up an SSH tunnel first:
Then open
ssh -L 81:localhost:81 user@your-server
http://localhost:81
- Local server:
-
Log in with default credentials:
admin@example.com/changeme(change immediately) -
Add a Proxy Host:
- Domain Names: your domain (e.g.,
search.example.com) - Scheme: http
- Forward Hostname/IP:
127.0.0.1 - Forward Port:
8080 - Block Common Exploits: enabled
- Domain Names: your domain (e.g.,
-
In the SSL tab:
- Request a new Let's Encrypt certificate
- Force SSL: enabled
- HTTP/2 Support: enabled
- HSTS Enabled: enabled
- Enter your email for Let's Encrypt notifications
-
Save and verify:
https://your-domain.com
Port 81 (NPM admin panel) is intentionally not exposed to the internet. Always access it via SSH tunnel on remote servers.
jlc-search merges data from three sources:
| Source | What it provides | Download command | Process command |
|---|---|---|---|
| jlcparts mirror | 446k+ parts, attributes, categories | bun run ingest/src/download-jlcparts.ts |
bun run ingest/src/process-jlcparts.ts |
| JLCPCB API | JLCPCB stock levels, PCBA type info | bun run ingest/src/download-jlcpcb.ts |
bun run ingest/src/process-jlcpcb.ts |
| LCSC API | Enriched descriptions, pricing, stock | bun run ingest/src/download-lcsc.ts |
(merged during process-jlcpcb) |
# Default: downloads jlcparts and processes into DB
docker compose run --rm ingest
# Run a specific script inside the ingest container
docker compose run --rm ingest ingest/src/download-jlcparts.ts# Download all sources (network only, no DB)
make download
# Process all downloaded data (needs PostgreSQL running)
make process
# Or run both together using Docker
make ingestThe datasheet pipeline extracts searchable text from component datasheets:
# Full pipeline: export URLs -> download PDFs -> extract text -> process into DB
make datasheets
# Or run individual steps:
make export-datasheet-urls # Export datasheet URLs from DB
make download-datasheets # Download PDFs and extract text
make process-datasheets # Process extracted text into DBAll ingestion scripts support incremental updates:
- jlcparts: Tracks file hashes; only re-processes changed categories
- JLCPCB API: Uses a page manifest; resumes interrupted downloads
- LCSC enrichment: Appends to NDJSON file; deduplicates on process
- Datasheets: Tracks extraction in
datasheet_meta; skips already-processed files
All database writes use ON CONFLICT (lcsc) DO UPDATE, so any source can be re-run safely at any time.
cd /path/to/jlc-search
git pull
docker compose build
docker compose up -dThe database volume persists across rebuilds. No data is lost.
# Re-run ingestion to pick up new parts
docker compose run --rm ingest
# Or for a full refresh from all sources
make download && make process# Dump the database
docker compose exec db pg_dump -U jlc jlc > backup-$(date +%Y%m%d).sql
# Restore from backup
docker compose exec -T db psql -U jlc jlc < backup-20250101.sql# All services
docker compose logs -f
# Specific service
docker compose logs -f backend
docker compose logs -f db
docker compose logs -f npm# Restart everything
docker compose restart
# Restart a single service
docker compose restart backendThe main disk consumers are:
- PostgreSQL data volume (
pg_data): ~2-3 GB - Raw downloaded data (
raw_data): ~5-10 GB - Image cache (
img_cache): grows over time as users browse parts
Check Docker volume sizes:
docker system df -vCheck that no other process is using the required ports (5432, 3001, 8080, 80, 443, 81):
ss -tlnp | grep -E '(5432|3001|8080|80|443|81)'Verify the database is healthy:
docker compose exec db pg_isready -U jlcVerify DATABASE_URL in .env matches POSTGRES_PASSWORD:
# The password in this URL:
# DATABASE_URL=postgres://jlc:YOUR_PASSWORD@localhost:5432/jlc
# Must match:
# POSTGRES_PASSWORD=YOUR_PASSWORDdocker compose logs npm
docker compose restart npm- Verify DNS:
host your-domain.comshould return your server's IP - Verify port 80 is open:
curl -I http://your-domain.com - Check NPM logs:
docker compose logs npm
All Docker services use network_mode: host, which bypasses Docker's iptables rules. If you use UFW:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Do NOT expose port 81 (NPM admin) -- use SSH tunnel insteadThe database may be empty. Run ingestion:
docker compose run --rm ingestCheck part count:
docker compose exec db psql -U jlc -c "SELECT count(*) FROM parts;"For development without Docker (except PostgreSQL):
# Start PostgreSQL
make pg
# Run backend and frontend with hot reload
make devOr run them separately:
make dev-backend # Backend on port 3001
make dev-frontend # Frontend on port 3000 (proxies /api to backend)The frontend dev server (Vite) runs on port 3000 and proxies API requests to the backend on port 3001.