HotelHub HMSDocs

20 — Deployment Guide #

Step-by-step untuk deploy HMS ke production. Cover: VPS standar (DigitalOcean, Vultr, Linode, Hetzner, Niagahoster, Biznet), Docker (recommended), local on-prem.

Audience: technical owner / sysadmin yang siapkan instalasi standalone, atau vendor team yang siapkan tenant SaaS.


1. Sizing #

Hotel size Spec minimum Spec recommended
< 30 kamar 2 vCPU / 4 GB RAM / 60 GB SSD 4 vCPU / 8 GB / 80 GB
30-100 kamar 4 vCPU / 8 GB / 120 GB 8 vCPU / 16 GB / 200 GB
100-300 kamar 8 vCPU / 16 GB / 240 GB 8 vCPU / 32 GB / 400 GB + DB replica
> 300 kamar / chain 16 vCPU / 32 GB / 500 GB + DB cluster Dedicated infra, lihat enterprise architecture

Storage growth: estimate ~2 GB/tahun per 50 kamar (PII docs, photos, audit log).


2. Stack baseline #

Optional:


3. Path A — Bare-metal VPS install #

3.1 Server prep #

# update
sudo apt update && sudo apt upgrade -y

# essentials
sudo apt install -y curl wget git unzip software-properties-common ufw fail2ban

# firewall
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw enable

# fail2ban (default config OK)
sudo systemctl enable --now fail2ban

3.2 PHP 8.3 #

sudo add-apt-repository ppa:ondrej/php -y
sudo apt update
sudo apt install -y php8.3-fpm php8.3-cli php8.3-pgsql php8.3-redis \
  php8.3-mbstring php8.3-xml php8.3-curl php8.3-zip php8.3-bcmath \
  php8.3-gd php8.3-imagick php8.3-intl php8.3-soap

php.ini tweaks (per /etc/php/8.3/fpm/php.ini):

memory_limit = 512M
post_max_size = 64M
upload_max_filesize = 32M
max_execution_time = 120
opcache.enable = 1
opcache.memory_consumption = 256
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0

3.3 PostgreSQL #

sudo apt install -y postgresql-16
sudo -u postgres psql -c "CREATE USER hms WITH PASSWORD 'STRONG_PW';"
sudo -u postgres psql -c "CREATE DATABASE hotel_main OWNER hms;"

postgresql.conf (/etc/postgresql/16/main/postgresql.conf):

shared_buffers = 1GB           # 25% of RAM
effective_cache_size = 3GB     # 75% of RAM
work_mem = 16MB
maintenance_work_mem = 256MB
max_connections = 200

3.4 Redis #

sudo apt install -y redis-server
# bind 127.0.0.1, set requirepass di /etc/redis/redis.conf
sudo systemctl enable --now redis-server

3.5 Meilisearch #

curl -L https://install.meilisearch.com | sh
sudo mv meilisearch /usr/local/bin/
# create systemd service
sudo tee /etc/systemd/system/meilisearch.service <<EOF
[Unit]
Description=Meilisearch
[Service]
ExecStart=/usr/local/bin/meilisearch --master-key=YOUR_MASTER_KEY --db-path /var/lib/meilisearch
Restart=always
User=meilisearch
[Install]
WantedBy=multi-user.target
EOF
sudo useradd -r meilisearch
sudo mkdir -p /var/lib/meilisearch && sudo chown meilisearch /var/lib/meilisearch
sudo systemctl enable --now meilisearch

3.6 Nginx #

/etc/nginx/sites-available/hotel:

server {
  listen 80;
  server_name hotelmandala.com www.hotelmandala.com;
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl http2;
  server_name hotelmandala.com www.hotelmandala.com;
  root /var/www/hotel/public;
  index index.php;

  ssl_certificate /etc/letsencrypt/live/hotelmandala.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/hotelmandala.com/privkey.pem;

  client_max_body_size 64M;

  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;

  location / {
    try_files $uri $uri/ /index.php?$query_string;
  }

  location ~ \.php$ {
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
    include fastcgi_params;
  }

  location ~ /\.ht { deny all; }
  location ~ /\.env { deny all; }
}

sudo ln -s /etc/nginx/sites-available/hotel /etc/nginx/sites-enabled/

3.7 Let's Encrypt #

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d hotelmandala.com -d www.hotelmandala.com

3.8 App deploy #

sudo mkdir -p /var/www/hotel
sudo chown $USER:www-data /var/www/hotel
cd /var/www/hotel
git clone https://github.com/your-org/hotel.git .
composer install --no-dev --optimize-autoloader
cp .env.example .env
php artisan key:generate
# edit .env (DB, Redis, Mail, APP_URL, MEILISEARCH_KEY, etc.)
php artisan migrate --force
php artisan storage:link
npm ci && npm run build
sudo chown -R www-data:www-data storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cache

3.9 Supervisor (queue worker) #

/etc/supervisor/conf.d/hotel-worker.conf:

[program:hotel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/hotel/artisan queue:work redis --sleep=3 --tries=3 --timeout=120 --max-time=3600
autostart=true
autorestart=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/log/hotel-worker.log
stopwaitsecs=180
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start hotel-worker:*

3.10 Cron #

sudo crontab -u www-data -e
* * * * * cd /var/www/hotel && php artisan schedule:run >> /dev/null 2>&1

3.11 License setup #

cd /var/www/hotel
sudo -u www-data php artisan license:bootstrap
# kemudian akses https://hotelmandala.com/setup/wizard di browser

Repo include docker-compose.yml:

services:
  app:
    image: hotelhub/hms:1.0
    env_file: .env
    volumes:
      - ./storage:/var/www/storage
      - ./public/uploads:/var/www/public/uploads
    depends_on: [db, redis, meilisearch]
    networks: [back, front]

  nginx:
    image: nginx:1.27
    ports: ["80:80", "443:443"]
    volumes:
      - ./docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./docker/certs:/etc/nginx/certs:ro
      - ./public:/var/www/public:ro
    depends_on: [app]
    networks: [front]

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: hms
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: hotel_main
    volumes: [pgdata:/var/lib/postgresql/data]
    networks: [back]

  redis:
    image: redis:7
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes: [redisdata:/data]
    networks: [back]

  meilisearch:
    image: getmeili/meilisearch:v1.10
    environment:
      MEILI_MASTER_KEY: ${MEILI_KEY}
    volumes: [meilidata:/meili_data]
    networks: [back]

  worker:
    image: hotelhub/hms:1.0
    env_file: .env
    command: php artisan queue:work redis --sleep=3 --tries=3
    depends_on: [app, redis]
    networks: [back]

  scheduler:
    image: hotelhub/hms:1.0
    env_file: .env
    command: |
      sh -c 'while true; do php artisan schedule:run; sleep 60; done'
    depends_on: [app]
    networks: [back]

volumes:
  pgdata: {}
  redisdata: {}
  meilidata: {}

networks:
  back:
  front:

Deploy:

docker compose up -d
docker compose exec app php artisan migrate --force
docker compose exec app php artisan license:bootstrap
# kemudian https://your-domain/setup/wizard

5. Path C — Niagahoster / Biznet shared / managed #

Banyak owner hotel di Indonesia pilih Niagahoster Cloud VPS atau Biznet GIO untuk lokal latency.

Catatan:

Vendor opsional sediakan managed-host bundle:


6. Deployment checklist (pre-go-live) #


7. Backup strategy #

Database #

Cron daily 02:00:

pg_dump -Fc -U hms hotel_main | gzip | aws s3 cp - s3://hotel-backups/db/$(date +%Y%m%d).sql.gz \
  --sse AES256

Retention: 30 hari S3 standard, lifecycle policy → Glacier setelah 30 hari, delete setelah 365 hari.

Storage / uploads #

Cron daily 02:30: rsync /var/www/hotel/storage/app/ ke S3-compatible bucket (R2, B2, Wasabi).

Configuration #

Restore drill #

Quarterly: restore di staging server, verify integrity, document time-to-recover.


8. Monitoring #

Layer Tool
Server health Netdata / Glances / btop (manual SSH)
Application errors Sentry (BYOK key) atau self-hosted GlitchTip
Uptime UptimeRobot / BetterStack / Hetrix Tools
Logs storage/logs/*.log rotated daily, optional ship ke Loki/Logtail
Metrics Laravel Telescope (admin only, off in prod by default)
Slow queries PostgreSQL log_min_duration_statement = 1000
Queue health Horizon (kalau kita pilih horizon) atau cron alert kalau jobs failed > 50

9. Update procedure #

cd /var/www/hotel
sudo -u www-data php artisan down --secret=your-token  # maintenance mode
git pull origin main
composer install --no-dev --optimize-autoloader
npm ci && npm run build
sudo -u www-data php artisan migrate --force
sudo -u www-data php artisan config:cache
sudo -u www-data php artisan route:cache
sudo -u www-data php artisan view:cache
sudo -u www-data php artisan event:cache
sudo supervisorctl restart hotel-worker:*
sudo -u www-data php artisan up

Or via deploy script bin/deploy.sh (provided di repo).

Auto-update (P2): vendor push update notification, owner click "Update now" → run script.


10. Performance tuning checklist #

Lighthouse target booking engine: ≥90 mobile.


11. Network & ports #

Port Service Expose
80 HTTP (redirect) Public
443 HTTPS Public
22 SSH IP whitelist atau Tailscale only
5432 PostgreSQL localhost only
6379 Redis localhost only
7700 Meilisearch localhost only

Admin panel di subdomain admin.{domain} dengan IP whitelist (kalau aktifkan vendor remote management).


12. Troubleshooting cepat #

Gejala Cek dulu
500 error storage/logs/laravel.log, php-fpm log
Queue tidak jalan supervisorctl status, Redis aktif?
Cron tidak run crontab -l, php artisan schedule:list
Search blank Meilisearch service, re-index
Asset 404 npm run build, php artisan storage:link
Slow page OPcache enable? config:cache? DB index? slow query log
License banner php artisan license:diagnostic
OTA sync error storage/logs/channel-manager.log, integration test endpoint
Email tidak terkirim mail.php config, test via php artisan tinker Mail::raw(...)

13. Open questions #

  1. Bundle Docker image official ke registry public vs private? Default: public minor version, private snapshot per license customer.
  2. Auto-update channel stable vs beta — config flag UPDATE_CHANNEL=stable|beta?
  3. Multi-server cluster (LB + 2 app + DB replica) untuk hotel besar — provide as separate guide?
  4. Edge cache (Cloudflare worker) untuk pSEO halaman — Phase 2 kalau traffic tinggi.