- Reorganize into apps/, packages/, docs/, e2e/, pocketbase/ directories - Add Dockerfiles for web, worker, and PocketBase services - Add docker-compose.yml for local orchestration - Add turbo.json for monorepo task management - Add Playwright e2e test infrastructure - Add PocketBase backend with migrations - Remove Vite/Next.js/ESLint/PostCSS config files - Update package.json with workspace dependencies - Add .env.example and .dockerignore
15 KiB
Deployment Guide
Deploy Project E with Docker Compose and Nginx Proxy Manager. Three containers run the application: web (Next.js), db (PocketBase), and worker (background jobs).
Table of Contents
- Prerequisites
- Quick Deploy
- Environment Configuration
- Nginx Proxy Manager Setup
- PocketBase Setup
- Backups
- Monitoring
- Scaling
- Troubleshooting
Prerequisites
- Docker 24.0 or later
- Docker Compose 2.20 or later
- Nginx Proxy Manager installed and running
- At least 1GB RAM and 10GB disk space
Quick Deploy
-
Clone the repository on your server
git clone <repository-url> cd ProjectE -
Create the environment file
cp .env.example .envEdit
.envand set the required variables:POCKETBASE_ADMIN_TOKEN=your-secure-random-token PUBLIC_URL=http://project-e.local COOKIE_SECURE=false ALLOWED_HOSTS=project-e.local,localhostGenerate a secure token:
openssl rand -hex 32 -
Build and start containers
docker compose up -d -
Verify all containers are running
docker compose psYou should see three containers with status
Up (healthy):project-e-web(internal only, no exposed ports)project-e-db(internal only, no exposed ports)project-e-workerrunning in the background
-
Configure Nginx Proxy Manager (see below)
-
Create your admin account
Once NPM is configured, access Project E through your domain. Create your first user account.
Environment Configuration
Required Variables
| Variable | Description |
|---|---|
POCKETBASE_ADMIN_TOKEN |
Admin token from PocketBase. Required for the worker and server-side API operations. |
Optional Variables
| Variable | Default | Description |
|---|---|---|
POCKETBASE_URL |
http://db:8090 |
PocketBase URL (internal Docker network) |
NODE_ENV |
production |
Node environment |
PUBLIC_URL |
http://localhost:3000 |
Public URL where users access Project E (used for generating absolute URLs) |
COOKIE_SECURE |
false |
Set to true if using HTTPS through NPM, false for HTTP-only LAN access |
ALLOWED_HOSTS |
localhost |
Comma-separated list of domains that can access the app |
Setting Variables
Create a .env file in the project root:
POCKETBASE_ADMIN_TOKEN=abc123def456...
POCKETBASE_URL=http://db:8090
PUBLIC_URL=http://project-e.local
COOKIE_SECURE=false
ALLOWED_HOSTS=project-e.local,localhost
Docker Compose reads this file automatically.
Nginx Proxy Manager Setup
Add Proxy Host
-
Open Nginx Proxy Manager admin interface
-
Go to Hosts → Proxy Hosts → Add Proxy Host
-
Configure the following:
Details Tab:
- Domain Names:
project-e.local(or your chosen domain) - Scheme:
http - Forward Hostname/IP:
project-e-web(the Docker container name) - Forward Port:
3000
SSL Tab:
- If using HTTPS: Select your SSL certificate
- If HTTP-only on LAN: Leave SSL disabled
Advanced Tab: Add these custom Nginx configuration lines:
# WebSocket support for realtime features proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; # Forward real client information 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; proxy_set_header Host $host; # SSE support for realtime endpoint proxy_buffering off; proxy_cache off; proxy_read_timeout 300s; # Increase timeout for long-running requests proxy_connect_timeout 300s; proxy_send_timeout 300s; - Domain Names:
-
Click Save
Testing the Connection
- From a device on your LAN, open a browser
- Navigate to
http://project-e.local(or your configured domain) - You should see the Project E login page
Optional: PocketBase Admin Access
If you need direct access to the PocketBase admin UI:
-
In NPM, add another Proxy Host:
- Domain Names:
pb.project-e.local - Scheme:
http - Forward Hostname/IP:
project-e-db - Forward Port:
8090
- Domain Names:
-
Or temporarily expose the port in
docker-compose.yml:db: ports: - "8090:8090"
PocketBase Setup
Initial Configuration
PocketBase runs as a standalone container. After starting it for the first time:
- Access the admin UI at
http://your-server:8090/_/ - Create your admin account
- Configure authentication settings under Settings > Auth
- Enable the auth methods you need (email/password is enabled by default)
Migrations
Database migrations are in pocketbase/pb_migrations/. They run automatically when the PocketBase container starts.
To add a new migration:
- Create a file in
pocketbase/pb_migrations/with the naming conventionYYYYMMDDHHMMSS_description.js - Restart the PocketBase container:
docker compose restart db
Data Directory
PocketBase stores all data (SQLite database, uploads, logs) in the /pb_data volume. This volume persists across container restarts.
Reverse Proxy
Put a reverse proxy in front of the application to handle SSL, compression, and routing.
Caddy (Recommended)
Caddy handles SSL automatically.
Create a Caddyfile:
your-domain.com {
reverse_proxy localhost:3000
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options "DENY"
X-Content-Type-Options "nosniff"
}
}
pb.your-domain.com {
reverse_proxy localhost:8090
}
Install and run Caddy:
# Install Caddy
sudo apt install caddy # Debian/Ubuntu
# or
brew install caddy # macOS
# Start Caddy
caddy start
Traefik
Create a traefik.yml:
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
certificatesResolvers:
letsencrypt:
acme:
email: your-email@example.com
storage: acme.json
httpChallenge:
entryPoint: web
providers:
docker:
exposedByDefault: false
Update docker-compose.yml to add Traefik labels:
services:
web:
labels:
- "traefik.enable=true"
- "traefik.http.routers.web.rule=Host(`your-domain.com`)"
- "traefik.http.routers.web.entrypoints=websecure"
- "traefik.http.routers.web.tls.certresolver=letsencrypt"
Nginx
Create /etc/nginx/sites-available/project-e:
server {
listen 80;
server_name your-domain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name your-domain.com;
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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;
proxy_cache_bypass $http_upgrade;
# SSE support for realtime endpoint
proxy_buffering off;
proxy_read_timeout 300s;
}
}
Enable the site and reload:
sudo ln -s /etc/nginx/sites-available/project-e /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
SSL/TLS
Let's Encrypt with Certbot
# Install certbot
sudo apt install certbot python3-certbot-nginx
# Get certificate
sudo certbot --nginx -d your-domain.com -d pb.your-domain.com
# Auto-renewal is configured automatically
Caddy
Caddy obtains and renews certificates automatically. No configuration needed beyond the domain name in the Caddyfile.
Internal Communication
The web and worker containers connect to PocketBase over the internal Docker network (http://db:8090). This traffic does not need SSL.
Only expose ports 3000 and 8090 to the reverse proxy, not directly to the internet.
Backups
PocketBase Database
The database is a single SQLite file at /pb_data/data.db.
Manual backup:
# Stop the container to ensure consistency
docker compose stop db
# Copy the database file
docker cp project-e-db:/pb_data/data.db ./backups/data-$(date +%Y%m%d).db
# Restart the container
docker compose start db
Automated backup script:
Create scripts/backup.sh:
#!/bin/bash
BACKUP_DIR="/path/to/backups"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p $BACKUP_DIR
# Use PocketBase's backup API (no downtime)
curl -X POST http://localhost:8090/api/backup \
-H "Authorization: Admin your-admin-token" \
-o "$BACKUP_DIR/backup-$DATE.zip"
# Keep only last 30 backups
find $BACKUP_DIR -name "backup-*.zip" -mtime +30 -delete
echo "Backup completed: backup-$DATE.zip"
Schedule with cron:
# Run daily at 2 AM
0 2 * * * /path/to/scripts/backup.sh
Upload Files
Uploaded files are stored in the project-e-web-uploads volume.
Backup uploads:
docker run --rm \
-v project-e-web-uploads:/source:ro \
-v $(pwd)/backups:/backup \
alpine \
tar czf /backup/uploads-$(date +%Y%m%d).tar.gz -C /source .
Restore
Restore database:
# Stop containers
docker compose stop db
# Remove old data
docker volume rm project-e-pb-data
# Copy backup into new volume
docker volume create project-e-pb-data
docker run --rm \
-v project-e-pb-data:/pb_data \
-v $(pwd)/backups:/backup \
alpine \
sh -c "cp /backup/data-YYYYMMDD.db /pb_data/data.db"
# Restart
docker compose start db
Restore uploads:
docker run --rm \
-v project-e-web-uploads:/target \
-v $(pwd)/backups:/backup \
alpine \
sh -c "cd /target && tar xzf /backup/uploads-YYYYMMDD.tar.gz"
Monitoring
Container Health
All containers include health checks. Check status:
docker compose ps
Application Health
The web container exposes a health endpoint:
curl http://localhost:3000/api/health
Response:
{
"status": "ok",
"timestamp": "2024-01-15T10:30:00.000Z",
"version": "0.1.0"
}
Logs
View logs from all containers:
# All containers
docker compose logs -f
# Specific container
docker compose logs -f web
docker compose logs -f db
docker compose logs -f worker
Resource Usage
Monitor container resource usage:
docker stats
Uptime Monitoring
Use an external service to monitor your deployment:
- UptimeRobot (free tier): HTTP monitoring with email/SMS alerts
- Healthchecks.io: Cron job monitoring
- Better Stack: Status pages and incident management
Set up a check for https://your-domain.com/api/health with a 60-second interval.
Scaling
Vertical Scaling
The application runs as a single instance of each container. To handle more load:
- Increase server resources: Add more CPU and RAM to your host
- Increase Node.js memory: Set
NODE_OPTIONS=--max-old-space-size=4096in the web container - Increase PocketBase limits: PocketBase handles thousands of concurrent connections on modest hardware
Horizontal Scaling
Horizontal scaling requires changes to the architecture:
Current limitations:
- MCP sessions are stored in memory (not shared between instances)
- SSE connections are tied to a specific container
- File uploads go to a local volume
To scale horizontally:
- Use a shared session store (Redis)
- Use a load balancer with sticky sessions for SSE
- Use object storage (S3) for file uploads
- Run multiple web containers behind a load balancer
For most personal and small-team use cases, a single instance handles the load. PocketBase with SQLite performs well up to hundreds of concurrent users.
Worker Scaling
The worker uses polling with exponential backoff. For high-throughput job processing:
- Run multiple worker containers (they coordinate through the database)
- Reduce the base poll interval (currently 5 seconds)
- Use a dedicated job queue (Bull, BullMQ) instead of database polling
Troubleshooting
Container Won't Start
Check logs:
docker compose logs web
docker compose logs db
docker compose logs worker
Common issues:
| Problem | Solution |
|---|---|
Cannot connect to PocketBase |
Ensure the db container is healthy. Check docker compose ps. |
Port 3000 already in use |
Change the port mapping in docker-compose.yml |
POCKETBASE_ADMIN_TOKEN not set |
Set the token in .env and restart |
Migration failed |
Check migration files in pocketbase/pb_migrations/ |
Web App Returns 500 Errors
- Check the web container logs:
docker compose logs web - Verify PocketBase is running:
docker compose exec db wget -qO- http://localhost:8090/api/health - Verify the admin token is correct:
docker compose exec web env | grep POCKETBASE
Realtime Events Not Arriving
- Check the SSE connection:
curl -N http://localhost:3000/api/realtime - Verify the reverse proxy is not buffering SSE responses
- For Nginx, ensure
proxy_buffering offis set - Check browser console for connection errors
Worker Not Processing Jobs
- Check worker logs:
docker compose logs worker - Verify the admin token is set correctly
- Check for pending jobs in PocketBase admin:
http://localhost:8090/_/#/collections/queue_jobs - Jobs retry automatically with exponential backoff (max 5 minutes between retries)
Database Corruption
If the SQLite database becomes corrupted:
-
Stop all containers:
docker compose down -
Restore from the most recent backup (see Backups)
-
If no backup exists, try SQLite's recovery:
sqlite3 data.db ".recover" | sqlite3 new-data.db -
Replace the corrupted file and restart
Out of Disk Space
PocketBase stores the database and uploads in the project-e-pb-data volume.
Check disk usage:
docker system df
docker volume inspect project-e-pb-data
Clean up:
# Remove unused images
docker image prune -a
# Remove unused volumes (WARNING: deletes all data)
docker volume prune
Expand volume:
Docker volumes use the host filesystem. If the host disk is full, expand it or move the volume to a larger disk.
SSL Certificate Errors
Caddy: Check the Caddy logs for ACME errors. Ensure port 80 is accessible from the internet for the HTTP challenge.
Let's Encrypt: Certificates renew automatically. Force renewal:
sudo certbot renew --force-renewal
Common errors:
Connection refused: Port 80 is blocked by firewallDNS problem: Domain does not resolve to this serverRate limit exceeded: Too many certificate requests. Wait and retry.
Performance Issues
- Slow page loads: Check if the server has enough RAM. Node.js needs at least 512MB.
- Slow API responses: Check PocketBase query performance in the admin panel
- SSE disconnects: Ensure the reverse proxy has appropriate timeout settings (300s+)
- Worker falling behind: Increase the poll frequency or add more worker instances