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 sslAll-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 |
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
After setup, deployment files are in /opt/timetiles/ (bootstrap) or ./deployment/ (manual):
deployment/
├── timetiles # CLI script
├── docker-compose.prod.yml # Service orchestration
├── .env.production # Your configuration
├── nginx/
│ ├── nginx.conf # Main nginx config
│ └── sites-enabled/ # Site-specific configs
└── backups/ # Backup files (restic)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