Deployment
Run timbl anywhere that runs Bun. This page covers the common deployment patterns: Docker, reverse proxies, cloud platforms, and VPS. Pick the one that matches your infrastructure.
Docker
Basic Setup
Create a Dockerfile:
FROM oven/bun:1
WORKDIR /app
COPY package.json bun.lock ./RUN bun install --production
COPY . .
ENV NODE_ENV=productionENV PORT=3000
EXPOSE 3000
CMD ["bun", "index.ts"]Build and run:
# Build imagedocker build -t timbl .
# Run with persistent volumesdocker run -p 3000:3000 \ -v $(pwd)/db:/app/db \ -v $(pwd)/uploads:/app/uploads \ timblDocker Compose
version: '3.8'
services: timbl: build: . ports: - "3000:3000" volumes: - ./db:/app/db - ./uploads:/app/uploads environment: - NODE_ENV=production - PORT=3000 - DATABASE_PATH=/app/db/cms.db - UPLOADS_DIR=/app/uploadsRun with compose:
docker-compose up -dReverse Proxy
For production, serve timbl behind a reverse proxy. This handles SSL termination and static file serving.
Caddy
example.com { reverse_proxy localhost:3000
# Serve uploads directly (faster, no timbl overhead) handle /uploads/* { root * /app/uploads file_server }}Nginx
server { listen 80; server_name example.com;
# Uploads - serve directly location /uploads/ { alias /app/uploads/; expires 30d; add_header Cache-Control "public, immutable"; }
# API and everything else - proxy to timbl location / { proxy_pass http://localhost:3000; proxy_http_version 1.1;
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;
# WebSocket support (if needed) proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }}Traefik
labels: - "traefik.enable=true" - "traefik.http.routers.timbl.rule=Host(`example.com`)" - "traefik.http.routers.timbl.tls.certresolver=letsencrypt" - "traefik.http.services.timbl.loadbalancer.server.port=3000"Environment-Specific Configuration
Development
PORT=3000NODE_ENV=developmentDATABASE_PATH=./db/cms.dbUPLOADS_DIR=./uploadsCORS_ORIGIN=*Production
PORT=3000NODE_ENV=productionDATABASE_PATH=/data/cms.dbUPLOADS_DIR=/data/uploadsUPLOADS_BASE_PATH=/uploadsCORS_ORIGIN=https://example.comSSL/TLS
Let’s Encrypt with Caddy
Caddy automatically provisions SSL certificates:
example.com { reverse_proxy localhost:3000}Let’s Encrypt with Certbot + Nginx
# Install certbotsudo apt install certbot python3-certbot-nginx
# Obtain certificatesudo certbot --nginx -d example.com
# Auto-renewal is set up automaticallyCloud Deployment
Railway
# Install Railway CLInpm install -g @railway/cli
# Login and deployrailway loginrailway initrailway upAdd volumes for persistence in Railway dashboard.
Fly.io
# Install flyctlcurl -L https://fly.io/install.sh | sh
# Launchfly launch
# Create volumes for persistencefly volumes create timbl_data --region sin --size 1Update fly.toml:
[mounts] source="timbl_data" destination="/app/data"VPS (DigitalOcean, Linode, etc.)
- Provision server with Ubuntu 22.04+
- Install Bun:
curl -fsSL https://bun.sh/install | bash - Clone your repo
- Run
bun install - Set up systemd service
- Configure Nginx/Caddy as reverse proxy
Systemd Service
Create /etc/systemd/system/timbl.service:
[Unit]Description=timbl CMSAfter=network.target
[Service]Type=simpleUser=www-dataWorkingDirectory=/app/timblExecStart=/root/.bun/bin/bun run startRestart=alwaysRestartSec=5Environment=NODE_ENV=productionEnvironment=PORT=3000Environment=DATABASE_PATH=/app/data/cms.dbEnvironment=UPLOADS_DIR=/app/data/uploads
[Install]WantedBy=multi-user.targetEnable and start:
sudo systemctl enable timblsudo systemctl start timblsudo systemctl status timblBackup Strategy
Database
SQLite is a single file - easy to backup:
# Daily backup cron job0 2 * * * cp /app/data/cms.db /backups/cms-$(date +\%Y\%m\%d).dbUploads
Sync to S3 or similar:
# Using rclonerclone sync /app/uploads s3:my-bucket/timbl-uploadsFull Backup Script
#!/bin/bashDATE=$(date +%Y%m%d_%H%M%S)BACKUP_DIR=/backups/$DATE
mkdir -p $BACKUP_DIR
# Databasecp /app/data/cms.db $BACKUP_DIR/
# Uploadstar -czf $BACKUP_DIR/uploads.tar.gz /app/uploads
# Configcp /app/timbl/content.config.ts $BACKUP_DIR/
# Optional: Upload to S3aws s3 sync $BACKUP_DIR s3://my-backup-bucket/timbl/$DATEHealth Checks
The runtime registers GET /api/health for process health and GET /api/ready
for dependency readiness. Point platform health checks at /api/health and
readiness checks at /api/ready.
Monitoring
Structured Logging
timbl uses structured logging. In production, forward logs to your monitoring system:
# Bun automatically outputs JSON in productionbun run start 2>&1 | jq -c '. | select(.level == "error")'Metrics Endpoint
Add via plugin:
registerRoute({ method: "GET", path: "/api/metrics", auth: "session", handler() { return { status: 200, body: { uptime: process.uptime(), memory: process.memoryUsage(), // add custom metrics } }; },});Security Checklist
- Use strong authentication (Better-Auth with secure passwords)
- Set
NODE_ENV=production - Restrict
CORS_ORIGINto your domain - Run behind HTTPS reverse proxy
- Regular database backups
- Upload directory outside web root (or serve via proxy)
- Keep dependencies updated
- Enable firewall (ufw/iptables)
- Use non-root user for timbl process
- Set up log monitoring
Troubleshooting
Database locked
SQLite doesn’t handle concurrent writes well. If you see “database is locked”:
- Ensure single timbl instance (no horizontal scaling with SQLite)
- Check for long-running transactions
- Consider WAL mode (Write-Ahead Logging)
Uploads not serving
Check UPLOADS_DIR and UPLOADS_BASE_PATH env vars. Ensure directory permissions.
CORS errors
Set CORS_ORIGIN to your actual domain, not * in production.
See also
- Storage and Uploads: how uploads are handled and served
- Custom Adapters: replacing the default database or storage backend
- HTTP API Reference: the endpoints your proxy forwards to