Skip to Content
⚠️Active Development Notice: TimeTiles is under active development. Information may be placeholder content or not up-to-date.
Self-HostingDeployment

Deployment

Three ways to deploy TimeTiles to production. All include PostgreSQL, Nginx with SSL, and background workers.

Deployment Options

OptionBest forWhat you get
Docker ComposeProductionSeparate containers, horizontal scaling, standard Docker ops
All-in-OneDemos, small teamsSingle container with everything, simplest setup
Bootstrap ScriptFresh Ubuntu serversAutomated setup including security hardening
# 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 ssl

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-allinone

Bootstrap 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 bash

The 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.production

Required settings:

VariableDescription
DOMAIN_NAMEYour production domain
DB_PASSWORDDatabase password (20+ characters)
PAYLOAD_SECRETRandom secret (openssl rand -base64 32)
LETSENCRYPT_EMAILEmail for SSL certificate notifications

See Configuration for the full environment variable reference.

SSL/TLS

After DNS is pointing to your server and services are running:

timetiles ssl

Certificates auto-renew every 12 hours via the certbot container.

Custom Certificates

Place your certificate files in deployment/nginx/ssl/:

  • fullchain.pem — certificate + chain
  • privkey.pem — private key

Then restart nginx: timetiles restart nginx

CLI Reference

The timetiles CLI wraps Docker Compose for common operations:

CommandWhat it does
timetiles upStart all services
timetiles downStop all services
timetiles restartRestart services
timetiles statusCheck service health
timetiles logsView logs (add -f to follow)
timetiles updatePull latest images and redeploy
timetiles pullPull images without restarting
timetiles sslInitialize Let’s Encrypt certificates
timetiles backupCreate a backup (see Maintenance)
timetiles restoreRestore 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 container

File 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/health

Database migrations run automatically on container startup.

Troubleshooting

ProblemSolution
Build fails with OOMAdd 4 GB swap: sudo fallocate -l 4G /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile
SSL certificate failsVerify DNS resolves to your server: dig your-domain.com +short
App won’t startCheck logs: timetiles logs web
Database connection errorCheck postgres is running: timetiles status
Port already in useStop 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:

  1. Install Podman (rootless mode)
  2. Build the base container images
  3. Start the runner service
  4. Set SCRAPER_RUNNER_URL and SCRAPER_API_KEY in your .env.production
  5. Enable the enableScrapers feature flag in the admin dashboard

Next Steps

Last updated on