Files
honey-be/DEPLOYMENT_GUIDE.md
Tihon 15498c8337
All checks were successful
Deploy to VPS / deploy (push) Successful in 52s
Initial setup, cleanup, VPS setup
2026-03-07 23:11:31 +02:00

21 KiB

VPS Deployment Guide for Lottery Application

This guide will help you deploy the Lottery application to a VPS (Ubuntu) using Docker, Docker Compose, and Nginx.

Prerequisites

  • Ubuntu VPS (tested on Ubuntu 20.04+)
  • Root or sudo access
  • Domain name pointing to your VPS IP (for HTTPS)
  • Basic knowledge of Linux commands

Architecture Overview

Internet
   ↓
Nginx (HTTPS, Port 443)
   ↓
   ├─→ Frontend (Static files from /opt/app/frontend/dist)
   ├─→ Backend API (/api/* → Docker container on port 8080)
   ├─→ WebSocket (/ws → Docker container)
   └─→ Avatars (/avatars/* → /opt/app/data/avatars)

Step 1: Initial VPS Setup

1.1 Update System

sudo apt update
sudo apt upgrade -y

1.2 Install Required Software

# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER

# Docker Compose v2+ is included with Docker (as a plugin)
# Verify it's installed:
docker compose version

# If not installed, install Docker Compose plugin:
# For Ubuntu/Debian:
sudo apt-get update
sudo apt-get install docker-compose-plugin

# Or if you need the standalone version (older method):
# sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
# sudo chmod +x /usr/local/bin/docker-compose

# Install Nginx
sudo apt install nginx -y

# Install Certbot for SSL certificates
sudo apt install certbot python3-certbot-nginx -y

# Log out and log back in for Docker group to take effect
exit

Step 2: Create Directory Structure

# Create main application directory
sudo mkdir -p /opt/app
sudo chown $USER:$USER /opt/app

# Create subdirectories
mkdir -p /opt/app/backend
mkdir -p /opt/app/frontend
mkdir -p /opt/app/nginx
mkdir -p /opt/app/data/avatars
mkdir -p /opt/app/mysql/data

# Set proper permissions
sudo chmod -R 755 /opt/app
sudo chown -R $USER:$USER /opt/app/data

Step 3: Deploy Backend

3.1 Copy Backend Files

From your local machine, copy the backend repository to the VPS:

# On your local machine, use scp or rsync
scp -r lottery-be/* user@your-vps-ip:/opt/app/backend/

# Or use git (recommended)
# On VPS:
cd /opt/app/backend
git clone <your-backend-repo-url> .

3.2 Plan Database Configuration

Important: MySQL runs as a Docker container (no separate MySQL installation needed). Before creating the secret file, you need to decide on your database credentials:

  1. Database Name: lottery_db (default, can be changed)
  2. Database Username: root (default, can be changed)
  3. Database Password: Choose a strong, secure password
  4. Database URL: jdbc:mysql://db:3306/lottery_db

Understanding the Database URL (SPRING_DATASOURCE_URL):

The URL format is: jdbc:mysql://<hostname>:<port>/<database-name>

For this deployment, use: jdbc:mysql://db:3306/lottery_db

Breaking it down:

  • jdbc:mysql:// - JDBC protocol for MySQL
  • db - This is the service name in docker-compose.prod.yml (acts as hostname in Docker network)
  • 3306 - Default MySQL port (internal to Docker network)
  • lottery_db - Database name (must match MYSQL_DATABASE in docker-compose)

Why db as hostname?

  • In Docker Compose, services communicate using their service names as hostnames
  • The MySQL service is named db in docker-compose.prod.yml (line 4: services: db:)
  • Both containers are on the same Docker network (lottery-network)
  • The backend container connects to MySQL using db:3306 (not localhost or the VPS IP)
  • This is an internal Docker network connection - MySQL is not exposed to the host

Quick Reference:

  • Correct: jdbc:mysql://db:3306/lottery_db (uses service name)
  • Wrong: jdbc:mysql://localhost:3306/lottery_db (won't work - localhost refers to the container itself)
  • Wrong: jdbc:mysql://127.0.0.1:3306/lottery_db (won't work - same reason)

Example credentials (use your own secure password!):

  • Database URL: jdbc:mysql://db:3306/lottery_db
  • Database Name: lottery_db
  • Username: root
  • Password: MySecurePassword123!

Note: These credentials will be used in:

  • The secret file (SPRING_DATASOURCE_URL, SPRING_DATASOURCE_USERNAME, SPRING_DATASOURCE_PASSWORD)
  • MySQL container environment variables (DB_PASSWORD, DB_ROOT_PASSWORD)

The MySQL container will be created automatically when you run docker-compose, and the database will be initialized with these credentials.

3.3 Create Secret Configuration File

The application uses a mounted secret file instead of environment variables for security. Create the secret file:

Option 1: Copy from template (if template file exists)

# Create the secrets directory (if it doesn't exist)
sudo mkdir -p /run/secrets

# Navigate to backend directory
cd /opt/app/backend

# Check if template file exists
ls -la honey-config.properties.template

# If it exists, copy it
sudo cp honey-config.properties.template /run/secrets/lottery-config.properties

Option 2: Create the file directly (if template wasn't copied)

If the template file doesn't exist in /opt/app/backend/, create the secret file directly:

# Create the secrets directory (if it doesn't exist)
sudo mkdir -p /run/secrets

# Create the secret file
sudo nano /run/secrets/lottery-config.properties

Then paste the following content (replace placeholder values):

# Lottery Application Configuration
# Replace all placeholder values with your actual configuration

# ============================================
# Database Configuration
# ============================================
# SPRING_DATASOURCE_URL format: jdbc:mysql://<hostname>:<port>/<database-name>
# 
# How to determine the URL:
# - Hostname: 'db' (this is the MySQL service name in docker-compose.prod.yml)
#   * In Docker Compose, services communicate using their service names
#   * The MySQL service is named 'db', so use 'db' as the hostname
#   * Both containers are on the same Docker network, so 'db' resolves to the MySQL container
# - Port: '3306' (default MySQL port, internal to Docker network)
# - Database name: 'lottery_db' (must match MYSQL_DATABASE in docker-compose.prod.yml)
#
# Example: jdbc:mysql://db:3306/lottery_db
#          └─┬─┘ └┬┘ └─┬──┘ └───┬────┘
#            │    │    │        └─ Database name
#            │    │    └─ Port (3306 is MySQL default)
#            │    └─ Service name in docker-compose (acts as hostname)
#            └─ JDBC protocol for MySQL
SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/lottery_db
SPRING_DATASOURCE_USERNAME=root
SPRING_DATASOURCE_PASSWORD=your_secure_database_password_here

# ============================================
# Telegram Bot Configuration
# ============================================
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
TELEGRAM_CHANNEL_CHECKER_BOT_TOKEN=your_channel_checker_bot_token_here
TELEGRAM_FOLLOW_TASK_CHANNEL_ID=@your_channel_name

# ============================================
# Frontend Configuration
# ============================================
FRONTEND_URL=https://yourdomain.com

# ============================================
# Avatar Storage Configuration
# ============================================
APP_AVATAR_STORAGE_PATH=/app/data/avatars
APP_AVATAR_PUBLIC_BASE_URL=
APP_AVATAR_MAX_SIZE_BYTES=2097152
APP_AVATAR_MAX_DIMENSION=512

# ============================================
# Session Configuration (Optional - defaults shown)
# ============================================
APP_SESSION_MAX_ACTIVE_PER_USER=5
APP_SESSION_CLEANUP_BATCH_SIZE=5000
APP_SESSION_CLEANUP_MAX_BATCHES=20

# ============================================
# GeoIP Configuration (Optional)
# ============================================
GEOIP_DB_PATH=

Edit the secret file with your actual values:

sudo nano /run/secrets/lottery-config.properties

Important: Replace all placeholder values with your actual configuration:

# Database Configuration
SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/lottery_db
SPRING_DATASOURCE_USERNAME=root
SPRING_DATASOURCE_PASSWORD=your_secure_database_password_here

# Telegram Bot Configuration
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
TELEGRAM_CHANNEL_CHECKER_BOT_TOKEN=your_channel_checker_bot_token_here
TELEGRAM_FOLLOW_TASK_CHANNEL_ID=@your_channel_name

# Frontend Configuration
FRONTEND_URL=https://yourdomain.com

# Avatar Storage Configuration
APP_AVATAR_STORAGE_PATH=/app/data/avatars
APP_AVATAR_PUBLIC_BASE_URL=

# Optional: Session Configuration (defaults shown)
APP_SESSION_MAX_ACTIVE_PER_USER=5
APP_SESSION_CLEANUP_BATCH_SIZE=5000
APP_SESSION_CLEANUP_MAX_BATCHES=20

# Optional: GeoIP Configuration
GEOIP_DB_PATH=

Set secure permissions:

# Make the file readable only by root and the docker group
sudo chmod 640 /run/secrets/lottery-config.properties
sudo chown root:docker /run/secrets/lottery-config.properties

Important Notes:

  • The database credentials you set here (SPRING_DATASOURCE_*) must match the MySQL container environment variables (see Step 3.4)
  • The MySQL container will be created automatically when you run docker-compose
  • The database lottery_db will be created automatically on first startup
  • Data will persist in a Docker volume (mysql_data)

3.4 Set MySQL Container Environment Variables

The MySQL container needs the database password as environment variables. These must match the credentials in your secret file.

Option 1: Read from secret file automatically (recommended - more secure and consistent)

Use the provided script that reads the password from the secret file:

cd /opt/app/backend

# Make sure the script is executable
chmod +x scripts/load-db-password.sh

# Source the script to load the password (this exports DB_PASSWORD and DB_ROOT_PASSWORD)
source scripts/load-db-password.sh

What this does:

  • Reads SPRING_DATASOURCE_PASSWORD from /run/secrets/lottery-config.properties
  • Exports DB_PASSWORD and DB_ROOT_PASSWORD with the same value
  • Ensures MySQL container credentials match the backend credentials automatically

Verify it worked:

# Check that the variables are set
echo "DB_PASSWORD is set: $([ -n "$DB_PASSWORD" ] && echo "yes" || echo "no")"

Option 2: Set environment variables manually (simpler but less secure)

If you prefer to set them manually (not recommended):

# Export the password (must match SPRING_DATASOURCE_PASSWORD from your secret file)
# Replace with the actual password you set in the secret file
export DB_PASSWORD=your_secure_database_password_here
export DB_ROOT_PASSWORD=your_secure_database_password_here

# Verify it's set
echo $DB_PASSWORD

Important Notes:

  • The DB_PASSWORD and DB_ROOT_PASSWORD must match SPRING_DATASOURCE_PASSWORD from your secret file
  • These environment variables are only used by the MySQL container
  • The backend application reads credentials from the secret file, not from environment variables
  • Option 1 is recommended because it ensures consistency and reduces the chance of mismatched passwords

3.5 Build and Start Backend

Before starting: Make sure you have:

  • Secret file created at /run/secrets/lottery-config.properties with database credentials
  • Environment variables DB_PASSWORD and DB_ROOT_PASSWORD set (use source scripts/load-db-password.sh from Step 3.4)
cd /opt/app/backend

# Make sure DB_PASSWORD and DB_ROOT_PASSWORD are set (if not already done in Step 3.4)
# If you haven't sourced the script yet, do it now:
source scripts/load-db-password.sh

# Build and start services
docker compose -f docker-compose.prod.yml up -d --build

# Check logs (press Ctrl+C to exit)
docker compose -f docker-compose.prod.yml logs -f

What happens when you start:

  1. MySQL container starts first (lottery-mysql)

    • Creates the database lottery_db automatically (if it doesn't exist)
    • Sets up the root user with your password from DB_PASSWORD
    • Waits until healthy before backend starts
  2. Backend container starts (lottery-backend)

    • Loads configuration from /run/secrets/lottery-config.properties
    • Connects to MySQL using credentials from secret file
    • Runs Flyway migrations to create all database tables
    • Starts the Spring Boot application

Wait for the database to be ready and migrations to complete. You should see:

  • lottery-mysql container running
  • lottery-backend container running
  • Log message: "📁 Loading configuration from mounted secret file: /run/secrets/lottery-config.properties"
  • Database migration messages (Flyway creating tables)
  • No errors in logs

Verify everything is working:

# Check that both containers are running
docker ps | grep lottery

# Check backend logs for secret file loading
docker compose -f docker-compose.prod.yml logs backend | grep "Loading configuration"

# Check backend logs for database connection
docker compose -f docker-compose.prod.yml logs backend | grep -i "database\|mysql\|flyway"

# Check for any errors
docker compose -f docker-compose.prod.yml logs backend | grep -i error

You should see:

  • 📁 Loading configuration from mounted secret file: /run/secrets/lottery-config.properties
  • Flyway migration messages showing tables being created
  • No connection errors

If you see connection errors:

  • Verify SPRING_DATASOURCE_PASSWORD in secret file matches DB_PASSWORD environment variable
  • Re-run the password loading script: source scripts/load-db-password.sh
  • Check that MySQL container is healthy: docker ps | grep mysql
  • Check MySQL logs: docker compose -f docker-compose.prod.yml logs db

Step 4: Build and Deploy Frontend

On your local machine:

cd lottery-fe

# Build for production (uses relative API URLs by default)
npm install
npm run build

# The dist/ folder will be created

4.2 Copy Frontend Build to VPS

# On your local machine
scp -r lottery-fe/dist/* user@your-vps-ip:/opt/app/frontend/dist/

# Or use rsync
rsync -avz lottery-fe/dist/ user@your-vps-ip:/opt/app/frontend/dist/

4.3 Alternative: Build on VPS

If you prefer to build on the VPS:

# Install Node.js on VPS
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

# Copy frontend source
scp -r lottery-fe/* user@your-vps-ip:/opt/app/frontend-source/

# Build
cd /opt/app/frontend-source
npm install
npm run build

# Copy dist to frontend directory
cp -r dist/* /opt/app/frontend/dist/

Step 5: Configure Nginx

5.1 Copy Nginx Configuration

# Copy the template
cp /opt/app/backend/nginx.conf.template /opt/app/nginx/nginx.conf

# Edit the configuration
nano /opt/app/nginx/nginx.conf

Update the following:

  1. Replace server_name _; with your domain name (e.g., server_name yourdomain.com;)
  2. Update SSL certificate paths if using Let's Encrypt (see Step 6)
  3. Verify paths match your directory structure
# Remove default Nginx config
sudo rm /etc/nginx/sites-enabled/default

# Create symlink to your config
sudo ln -s /opt/app/nginx/nginx.conf /etc/nginx/sites-available/lottery
sudo ln -s /etc/nginx/sites-available/lottery /etc/nginx/sites-enabled/

# Test Nginx configuration
sudo nginx -t

# If test passes, reload Nginx
sudo systemctl reload nginx

Step 6: Setup SSL Certificate (HTTPS)

6.1 Obtain SSL Certificate

# Stop Nginx temporarily
sudo systemctl stop nginx

# Obtain certificate (replace with your domain and email)
sudo certbot certonly --standalone -d yourdomain.com -d www.yourdomain.com --email your-email@example.com --agree-tos

# Start Nginx
sudo systemctl start nginx

6.2 Update Nginx Config with Certificate Paths

Edit /opt/app/nginx/nginx.conf and update:

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

6.3 Setup Auto-Renewal

# Test renewal
sudo certbot renew --dry-run

# Certbot will automatically renew certificates

Step 7: Configure Telegram Webhook

Update your Telegram bot webhook to point to your VPS:

# Replace with your bot token, domain, and the same webhook token you set in APP_TELEGRAM_WEBHOOK_TOKEN
curl -X POST "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/setWebhook" \
  -d "url=https://yourdomain.com/api/telegram/webhook/<YOUR_WEBHOOK_TOKEN>"

Verify webhook:

curl "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getWebhookInfo"

Step 8: Final Verification

8.1 Check Services

# Check Docker containers
docker ps

# Should show:
# - lottery-mysql
# - lottery-backend

# Check Nginx
sudo systemctl status nginx

8.2 Test Endpoints

# Test backend health
curl http://localhost:8080/actuator/health

# Test frontend (should return HTML)
curl https://yourdomain.com/

# Test API (should return JSON or error with auth)
curl https://yourdomain.com/api/health

8.3 Check Logs

# Backend logs
cd /opt/app/backend
docker-compose -f docker-compose.prod.yml logs -f

# Nginx logs
sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/nginx/error.log

8.4 Browser Testing

  1. Open https://yourdomain.com in a browser
  2. Test the Telegram Mini App
  3. Verify API calls work (check browser console)
  4. Test WebSocket connection (game updates)

Step 9: Maintenance Commands

9.1 Restart Services

# Restart backend
cd /opt/app/backend
docker compose -f docker-compose.prod.yml restart

# Restart Nginx
sudo systemctl restart nginx

9.2 Update Application

# Backend update
cd /opt/app/backend
git pull  # or copy new files
docker compose -f docker-compose.prod.yml up -d --build

# Frontend update
# Rebuild and copy dist/ folder

9.3 Backup Database

# Create backup
docker exec lottery-mysql mysqldump -u root -p${DB_PASSWORD} lottery_db > backup_$(date +%Y%m%d).sql

# Restore backup
docker exec -i lottery-mysql mysql -u root -p${DB_PASSWORD} lottery_db < backup_20240101.sql

9.4 View Logs

# Backend logs
cd /opt/app/backend
docker-compose -f docker-compose.prod.yml logs -f backend

# Database logs
docker-compose -f docker-compose.prod.yml logs -f db

# Nginx logs
sudo tail -f /var/log/nginx/error.log

Troubleshooting

Backend Not Starting

# Check logs
docker compose -f docker-compose.prod.yml logs backend

# Common issues:
# - Database not ready: wait for health check
# - Missing configuration: check secret file at /run/secrets/lottery-config.properties
# - Secret file not found: ensure file exists and is mounted correctly
# - Port conflict: ensure port 8080 is not exposed to host

Frontend Not Loading

# Check Nginx error log
sudo tail -f /var/log/nginx/error.log

# Verify files exist
ls -la /opt/app/frontend/dist/

# Check Nginx config
sudo nginx -t

Database Connection Issues

# Check database container
docker ps | grep mysql

# Check database logs
docker compose -f docker-compose.prod.yml logs db

# Test connection
docker exec -it lottery-mysql mysql -u root -p

SSL Certificate Issues

# Check certificate
sudo certbot certificates

# Renew certificate
sudo certbot renew

# Check Nginx SSL config
sudo nginx -t

WebSocket Not Working

# Check backend logs for WebSocket errors
docker compose -f docker-compose.prod.yml logs backend | grep -i websocket

# Verify Nginx WebSocket configuration
grep -A 10 "/ws" /opt/app/nginx/nginx.conf

Security Checklist

  • Strong database passwords set
  • Secret file has restricted permissions (chmod 640, owned by root:docker)
  • Secret file contains all required configuration values
  • SSL certificate installed and auto-renewal configured
  • Firewall configured (UFW recommended)
  • Backend port 8080 not exposed to host
  • MySQL port 3306 not exposed to host
  • Regular backups scheduled
  • Logs monitored for suspicious activity
# Install UFW
sudo apt install ufw -y

# Allow SSH (IMPORTANT - do this first!)
sudo ufw allow 22/tcp

# Allow HTTP and HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# Enable firewall
sudo ufw enable

# Check status
sudo ufw status

Directory Structure Summary

/opt/app/
├── backend/
│   ├── Dockerfile
│   ├── docker-compose.prod.yml
│   ├── lottery-config.properties.template
│   ├── pom.xml
│   └── src/
├── frontend/
│   └── dist/          (Vite production build)
├── nginx/
│   └── nginx.conf
├── data/
│   └── avatars/        (persistent uploads)
└── mysql/
    └── data/           (persistent DB storage)

/run/secrets/
└── lottery-config.properties  (mounted secret configuration file)

Support

If you encounter issues:

  1. Check logs first (backend, Nginx, Docker)
  2. Verify secret file exists at /run/secrets/lottery-config.properties and has correct values
  3. Verify secret file permissions (chmod 640, owned by root:docker)
  4. Check backend logs for "Loading configuration from mounted secret file" message
  5. Ensure all directories exist and have proper permissions
  6. Verify network connectivity between containers
  7. Check SSL certificate validity

Last Updated: 2026-01-24 Version: 1.0