Deployment
Three ways to deploy TimeTiles to production. All include PostgreSQL, Nginx with SSL, and background workers.
Deployment Options
| Option | Best for | What you get |
|---|---|---|
| Docker Compose | Production | Separate containers, horizontal scaling, standard Docker ops |
| All-in-One | Demos, small teams | Single container with everything, simplest setup |
| Bootstrap Script | Fresh Ubuntu servers | Automated setup including security hardening |
Docker Compose (Recommended)
Quick Start
# 1. Clone the deployment files
git clone https://github.com/jfilter/timetiles.git
cd timetiles
# 2. Configure
cp deployment/.env.production.example deployment/.env.production
nano deployment/.env.production
# Set at minimum: DOMAIN_NAME, DB_PASSWORD, PAYLOAD_SECRET, LETSENCRYPT_EMAIL
# 3. Pull images and start
timetiles pull
timetiles up
# 4. Set up SSL (after DNS is pointing to your server)
timetiles sslHorizontal Web Scaling
If you run more than one web container or otherwise serve overlapping traffic from multiple Node.js processes, set:
RATE_LIMIT_BACKEND=pgThe default memory backend is only correct for single-web-instance deployments. In a multi-worker setup, leaving rate limits in memory would multiply the effective limit across workers.
All-in-One Container
Everything in a single container — PostgreSQL, Nginx, and the app — managed by supervisord. Good for quick demos or personal use.
docker run -d \
-p 80:80 -p 443:443 \
-v timetiles-data:/data \
-e PAYLOAD_SECRET=$(openssl rand -base64 32) \
-e DB_PASSWORD=$(openssl rand -base64 16) \
ghcr.io/jfilter/timetiles:latest-allinoneBootstrap Script (Ubuntu 24.04)
Fully automated setup on a fresh Ubuntu server. Installs Docker, configures firewall, deploys TimeTiles, sets up SSL, and enables security hardening (fail2ban, SSH hardening).
curl -sSL https://raw.githubusercontent.com/jfilter/timetiles/main/deployment/bootstrap/install.sh | sudo bashThe script prompts for your domain, email, and other settings interactively.
Configuration
Create your environment file from the template:
cp deployment/.env.production.example deployment/.env.productionRequired settings:
| Variable | Description |
|---|---|
DOMAIN_NAME | Your production domain |
DB_PASSWORD | Database password (20+ characters) |
PAYLOAD_SECRET | Random secret (openssl rand -base64 32) |
LETSENCRYPT_EMAIL | Email for SSL certificate notifications |
Shared-rate-limit setting for multi-web deployments:
| Variable | Description |
|---|---|
RATE_LIMIT_BACKEND | Set to pg when more than one web process/container serves overlapping traffic |
See Configuration for the full environment variable reference.
SSL/TLS
Let’s Encrypt (Recommended)
After DNS is pointing to your server and services are running:
timetiles sslCertificates auto-renew every 12 hours via the certbot container.
Custom Certificates
Place your certificate files in deployment/nginx/ssl/:
fullchain.pem— certificate + chainprivkey.pem— private key
Then restart nginx: timetiles restart nginx
CLI Reference
The timetiles CLI wraps Docker Compose for common operations:
| Command | What it does |
|---|---|
timetiles up | Start all services |
timetiles down | Stop all services |
timetiles restart | Restart services |
timetiles status | Check service health |
timetiles logs | View logs (add -f to follow) |
timetiles update | Pull latest images and redeploy |
timetiles pull | Pull images without restarting |
timetiles ssl | Initialize Let’s Encrypt certificates |
timetiles backup | Create a backup (see Maintenance) |
timetiles restore | Restore from backup |
Direct Docker Compose
For advanced usage:
alias dc="docker compose -f deployment/docker-compose.prod.yml --env-file deployment/.env.production"
dc ps # List containers
dc logs web # View web logs
dc exec web sh # Shell into web containerFile Structure
A bootstrapped host keeps the deployment files in a real git working tree at /opt/timetiles-src/ and exposes them as /opt/timetiles/ via a symlink. timetiles update runs git pull against that tree, so tracked files (compose, nginx, the CLI itself) stay in sync with the upstream branch. Operator state is .gitignored inside deployment/ and survives every pull.
/opt/timetiles-src/
├── .git/ # sparse-checkout: deployment/
└── deployment/
├── timetiles # CLI script (tracked)
├── docker-compose.prod.yml # Service orchestration (tracked)
├── nginx/ # Nginx config (tracked)
│ ├── nginx.conf
│ └── sites-enabled/
├── .env.production # Your configuration (.gitignored)
├── docker-compose.override.yml # Optional override (.gitignored)
└── backups/ # Backup files (.gitignored)
/opt/timetiles -> /opt/timetiles-src/deployment # compat symlinkA manual install just keeps the deployment/ directory in your repo checkout and there is no symlink.
Security
- Only ports 80 and 443 are exposed externally
- PostgreSQL is on an internal Docker network — not accessible from outside
- The Next.js app runs as a non-root user in a read-only container
- Containers have dropped capabilities and tmpfs
/tmp - Nginx enforces HTTPS and sets security headers
- Auth endpoints are rate-limited at the Nginx level (5 req/min per IP)
Health Checks
# CLI status check
timetiles status
# HTTP health endpoint
curl https://your-domain.com/api/health
# Returns: {"status":"ok","timestamp":"..."}Updating
# 1. Back up first
timetiles backup
# 2. Pull and redeploy
timetiles update
# 3. Verify
curl https://your-domain.com/api/healthDatabase migrations run automatically on container startup.
Troubleshooting
| Problem | Solution |
|---|---|
| Build fails with OOM | Add 4 GB swap: sudo fallocate -l 4G /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile |
| SSL certificate fails | Verify DNS resolves to your server: dig your-domain.com +short |
| App won’t start | Check logs: timetiles logs web |
| Database connection error | Check postgres is running: timetiles status |
| Port already in use | Stop existing services: timetiles down |
Optional: Scraper Runner
If you need scrapers (custom Python/Node.js scripts for data extraction), deploy the TimeScrape runner alongside TimeTiles. The runner is a separate stateless service that executes scripts in hardened Podman containers.
See Scraper Deployment for full setup instructions. In short:
- Install Podman (rootless mode)
- Build the base container images
- Start the runner service
- Set
SCRAPER_RUNNER_URLandSCRAPER_API_KEYin your.env.production - Enable the
enableScrapersfeature flag in the admin dashboard
Next Steps
- Configure environment variables, geocoding, and feature flags
- Set up backups and monitoring
- Customize the UI with themes and branding