Tenanto Demo Deployment - Hetzner Server
Server: debian-dev (138.199.199.163) Domain: tenanto.dev DNS Provider: Cloudflare
1. Cloudflare DNS Records
Add these DNS records in Cloudflare Dashboard for tenanto.dev:
| Type | Name | Content | Proxy | TTL |
|---|---|---|---|---|
| A | @ |
138.199.199.163 | Proxied (orange cloud) | Auto |
| A | * |
138.199.199.163 | Proxied (orange cloud) | Auto |
| A | admin |
138.199.199.163 | Proxied (orange cloud) | Auto |
| A | demo |
138.199.199.163 | Proxied (orange cloud) | Auto |
Explanation:
@= root domain (tenanto.dev) - landing page*= wildcard for tenant subdomains (*.tenanto.dev)admin= admin panel (admin.tenanto.dev)demo= demo tenant (demo.tenanto.dev)
2. Cloudflare SSL/TLS Settings
2.1 SSL Mode
Go to SSL/TLS → Overview → Set to Full (strict)
2.2 Origin Certificate
Go to SSL/TLS → Origin Server → Create Certificate
- Click Create Certificate
- Settings:
- Private key type: RSA (2048)
- Hostnames:
tenanto.dev*.tenanto.dev
- Certificate validity: 15 years (recommended)
- Click Create
- Copy the certificate (Origin Certificate)
- Copy the private key
- Save both files - you'll need them for the server
3. Server Preparation
3.1 Create Directory Structure
ssh devhetzner
su - claude
cd /home/claude/docker
# Create Tenanto directories
mkdir -p apps/tenanto
mkdir -p volumes/tenanto-postgres
mkdir -p volumes/tenanto-redis
mkdir -p certs/tenanto
3.2 Upload SSL Certificates
Save the Cloudflare Origin Certificate files:
/home/claude/docker/certs/tenanto/origin-cert.pem(certificate)/home/claude/docker/certs/tenanto/origin-key.pem(private key)
4. Docker Compose for Tenanto
Create /home/claude/docker/apps/tenanto/docker-compose.yml:
services:
tenanto-app:
build:
context: .
dockerfile: Dockerfile
container_name: tenanto-app
restart: unless-stopped
environment:
APP_NAME: "Tenanto Demo"
APP_ENV: production
APP_DEBUG: false
APP_URL: https://tenanto.dev
APP_KEY: ${TENANTO_APP_KEY}
DB_CONNECTION: pgsql
DB_HOST: tenanto-postgres
DB_PORT: 5432
DB_DATABASE: tenanto
DB_USERNAME: tenanto
DB_PASSWORD: ${TENANTO_DB_PASSWORD}
REDIS_HOST: tenanto-redis
REDIS_PASSWORD: ${TENANTO_REDIS_PASSWORD}
REDIS_PORT: 6379
CACHE_DRIVER: redis
QUEUE_CONNECTION: redis
SESSION_DRIVER: redis
MAIL_MAILER: log
# Demo mode
DEMO_MODE_ENABLED: true
DEMO_TENANT_MAX_AGE_HOURS: 24
volumes:
- ./:/var/www/html
- tenanto-storage:/var/www/html/storage/app
networks:
- docker_docker_network
depends_on:
tenanto-postgres:
condition: service_healthy
tenanto-redis:
condition: service_healthy
tenanto-postgres:
image: postgres:16-alpine
container_name: tenanto-postgres
restart: unless-stopped
environment:
POSTGRES_DB: tenanto
POSTGRES_USER: tenanto
POSTGRES_PASSWORD: ${TENANTO_DB_PASSWORD}
volumes:
- ../../volumes/tenanto-postgres:/var/lib/postgresql/data
networks:
- docker_docker_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U tenanto -d tenanto"]
interval: 10s
timeout: 5s
retries: 5
tenanto-redis:
image: redis:7-alpine
container_name: tenanto-redis
restart: unless-stopped
command: redis-server --requirepass ${TENANTO_REDIS_PASSWORD}
volumes:
- ../../volumes/tenanto-redis:/data
networks:
- docker_docker_network
healthcheck:
test: ["CMD", "redis-cli", "-a", "${TENANTO_REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
tenanto-queue:
build:
context: .
dockerfile: Dockerfile
container_name: tenanto-queue
restart: unless-stopped
command: php artisan queue:work --sleep=3 --tries=3
environment:
APP_ENV: production
DB_CONNECTION: pgsql
DB_HOST: tenanto-postgres
DB_PORT: 5432
DB_DATABASE: tenanto
DB_USERNAME: tenanto
DB_PASSWORD: ${TENANTO_DB_PASSWORD}
REDIS_HOST: tenanto-redis
REDIS_PASSWORD: ${TENANTO_REDIS_PASSWORD}
volumes:
- ./:/var/www/html
- tenanto-storage:/var/www/html/storage/app
networks:
- docker_docker_network
depends_on:
- tenanto-app
tenanto-scheduler:
build:
context: .
dockerfile: Dockerfile
container_name: tenanto-scheduler
restart: unless-stopped
command: sh -c "while true; do php artisan schedule:run --verbose --no-interaction & sleep 60; done"
environment:
APP_ENV: production
DB_CONNECTION: pgsql
DB_HOST: tenanto-postgres
DB_PORT: 5432
DB_DATABASE: tenanto
DB_USERNAME: tenanto
DB_PASSWORD: ${TENANTO_DB_PASSWORD}
REDIS_HOST: tenanto-redis
REDIS_PASSWORD: ${TENANTO_REDIS_PASSWORD}
volumes:
- ./:/var/www/html
networks:
- docker_docker_network
depends_on:
- tenanto-app
volumes:
tenanto-storage:
networks:
docker_docker_network:
external: true
5. Dockerfile for PHP 8.4
Create /home/claude/docker/apps/tenanto/Dockerfile:
FROM php:8.4-fpm-alpine
# Install system dependencies
RUN apk add --no-cache \
git \
curl \
libpng-dev \
libxml2-dev \
zip \
unzip \
postgresql-dev \
icu-dev \
linux-headers \
oniguruma-dev \
freetype-dev \
libjpeg-turbo-dev \
nodejs \
npm
# Install PHP extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) \
pdo \
pdo_pgsql \
pgsql \
mbstring \
exif \
pcntl \
bcmath \
gd \
intl \
opcache \
xml
# Install Redis extension
RUN apk add --no-cache --virtual .build-deps $PHPIZE_DEPS \
&& pecl install redis \
&& docker-php-ext-enable redis \
&& apk del .build-deps
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /var/www/html
# Copy application
COPY . .
# Install dependencies
RUN composer install --no-dev --optimize-autoloader --no-interaction
# Build frontend assets
RUN npm ci && npm run build && rm -rf node_modules
# Set permissions
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache
# PHP configuration
RUN echo "opcache.enable=1" >> /usr/local/etc/php/conf.d/opcache.ini \
&& echo "opcache.memory_consumption=128" >> /usr/local/etc/php/conf.d/opcache.ini \
&& echo "opcache.interned_strings_buffer=8" >> /usr/local/etc/php/conf.d/opcache.ini \
&& echo "opcache.max_accelerated_files=10000" >> /usr/local/etc/php/conf.d/opcache.ini \
&& echo "opcache.validate_timestamps=0" >> /usr/local/etc/php/conf.d/opcache.ini
EXPOSE 9000
CMD ["php-fpm"]
6. Nginx Configuration
Create /home/claude/docker/services/nginx/sites-enabled/tenanto.dev.conf:
# Tenanto Demo - Multi-tenant SaaS
# Handles: tenanto.dev, admin.tenanto.dev, *.tenanto.dev
# HTTP to HTTPS redirect
server {
listen 80;
server_name tenanto.dev *.tenanto.dev;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
# Main landing page (tenanto.dev)
server {
listen 443 ssl;
http2 on;
server_name tenanto.dev;
root /var/www/apps/tenanto/public;
index index.php;
# SSL Configuration (Cloudflare Origin Certificate)
ssl_certificate /etc/cloudflare/tenanto/origin-cert.pem;
ssl_certificate_key /etc/cloudflare/tenanto/origin-key.pem;
# Security Headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Logging
access_log /var/log/nginx/tenanto.dev-access.log;
error_log /var/log/nginx/tenanto.dev-error.log;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass tenanto-app:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
location ~ /\. {
deny all;
}
}
# Admin panel (admin.tenanto.dev)
server {
listen 443 ssl;
http2 on;
server_name admin.tenanto.dev;
root /var/www/apps/tenanto/public;
index index.php;
ssl_certificate /etc/cloudflare/tenanto/origin-cert.pem;
ssl_certificate_key /etc/cloudflare/tenanto/origin-key.pem;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-ancestors 'self';" always;
access_log /var/log/nginx/admin.tenanto.dev-access.log;
error_log /var/log/nginx/admin.tenanto.dev-error.log;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass tenanto-app:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
location ~ /\. {
deny all;
}
}
# Tenant subdomains (*.tenanto.dev)
server {
listen 443 ssl;
http2 on;
server_name ~^(?<subdomain>.+)\.tenanto\.dev$;
root /var/www/apps/tenanto/public;
index index.php;
ssl_certificate /etc/cloudflare/tenanto/origin-cert.pem;
ssl_certificate_key /etc/cloudflare/tenanto/origin-key.pem;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-ancestors 'self';" always;
access_log /var/log/nginx/tenant.tenanto.dev-access.log;
error_log /var/log/nginx/tenant.tenanto.dev-error.log;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass tenanto-app:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
# Static assets caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
location ~ /\. {
deny all;
}
}
7. Environment Variables
Add to /home/claude/docker/.env:
# Tenanto Demo
TENANTO_APP_KEY=base64:GENERATE_NEW_KEY_HERE
TENANTO_DB_PASSWORD=GENERATE_SECURE_PASSWORD
TENANTO_REDIS_PASSWORD=GENERATE_SECURE_PASSWORD
Generate secure values:
# Generate APP_KEY
php artisan key:generate --show
# Generate passwords
openssl rand -base64 32
8. Deployment Steps
Step 1: Configure Cloudflare
- Add DNS records (see section 1)
- Set SSL mode to Full (strict)
- Create Origin Certificate (see section 2.2)
Step 2: Prepare Server
ssh devhetzner
su - claude
cd /home/claude/docker
# Create directories
mkdir -p apps/tenanto
mkdir -p volumes/tenanto-postgres
mkdir -p volumes/tenanto-redis
mkdir -p certs/tenanto
# Upload Origin Certificate (from local machine)
# scp origin-cert.pem devhetzner:/home/claude/docker/certs/tenanto/
# scp origin-key.pem devhetzner:/home/claude/docker/certs/tenanto/
# Set permissions
chmod 600 certs/tenanto/*
Step 3: Deploy Application
cd /home/claude/docker/apps/tenanto
# Clone or copy the application
# Option A: From Git
git clone https://gitlab.com/mazacmartin/tenanto.dev.git .
# Option B: Upload ZIP
# scp tenanto.zip devhetzner:/home/claude/docker/apps/tenanto/
# unzip tenanto.zip
# Create .env
cp .env.example .env
# Edit .env with production values
# Start containers
docker compose up -d
# Run migrations
docker compose exec tenanto-app php artisan migrate --seed
# Generate demo tenant
docker compose exec tenanto-app php artisan demo:create
Step 4: Configure Nginx
# Copy nginx config
# Already created in step 6
# Reload nginx
docker exec nginx-proxy nginx -t
docker exec nginx-proxy nginx -s reload
Step 5: Verify
- https://tenanto.dev - Landing page
- https://admin.tenanto.dev/admin - System admin
- https://demo.tenanto.dev/app - Demo tenant
9. Maintenance Commands
# View logs
docker compose logs -f tenanto-app
# Clear caches
docker compose exec tenanto-app php artisan optimize:clear
# Run migrations
docker compose exec tenanto-app php artisan migrate
# Create demo tenant
docker compose exec tenanto-app php artisan demo:create
# Cleanup expired demos
docker compose exec tenanto-app php artisan demo:cleanup
10. Rollback Plan
If something goes wrong:
# Stop Tenanto containers
cd /home/claude/docker/apps/tenanto
docker compose down
# Remove nginx config
rm /home/claude/docker/services/nginx/sites-enabled/tenanto.dev.conf
docker exec nginx-proxy nginx -s reload
# Existing services remain unaffected
Summary
| Component | Location |
|---|---|
| Application | /home/claude/docker/apps/tenanto/ |
| PostgreSQL data | /home/claude/docker/volumes/tenanto-postgres/ |
| Redis data | /home/claude/docker/volumes/tenanto-redis/ |
| SSL Certificates | /home/claude/docker/certs/tenanto/ |
| Nginx config | /home/claude/docker/services/nginx/sites-enabled/tenanto.dev.conf |
| Logs | /home/claude/docker/logs/nginx/tenanto.dev-*.log |