# 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 ```bash sudo apt update sudo apt upgrade -y ``` ### 1.2 Install Required Software ```bash # 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 ```bash # 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: ```bash # 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 . ``` ### 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://:/` **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)** ```bash # 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 lottery-config.properties.template # If it exists, copy it sudo cp lottery-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: ```bash # 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): ```properties # Lottery Application Configuration # Replace all placeholder values with your actual configuration # ============================================ # Database Configuration # ============================================ # SPRING_DATASOURCE_URL format: jdbc:mysql://:/ # # 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:** ```bash sudo nano /run/secrets/lottery-config.properties ``` **Important:** Replace all placeholder values with your actual configuration: ```properties # 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:** ```bash # 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: ```bash 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:** ```bash # 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): ```bash # 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) ```bash 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:** ```bash # 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 ### 4.1 Build Frontend Locally (Recommended) On your local machine: ```bash 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 ```bash # 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: ```bash # 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 ```bash # 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 ### 5.2 Link Nginx Configuration ```bash # 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 ```bash # 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: ```nginx ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; ``` ### 6.3 Setup Auto-Renewal ```bash # 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: ```bash # 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/setWebhook" \ -d "url=https://yourdomain.com/api/telegram/webhook/" ``` Verify webhook: ```bash curl "https://api.telegram.org/bot/getWebhookInfo" ``` ## Step 8: Final Verification ### 8.1 Check Services ```bash # Check Docker containers docker ps # Should show: # - lottery-mysql # - lottery-backend # Check Nginx sudo systemctl status nginx ``` ### 8.2 Test Endpoints ```bash # 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 ```bash # 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 ```bash # Restart backend cd /opt/app/backend docker compose -f docker-compose.prod.yml restart # Restart Nginx sudo systemctl restart nginx ``` ### 9.2 Update Application ```bash # 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 ```bash # 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 ```bash # 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 ```bash # 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 ```bash # 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 ```bash # 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 ```bash # Check certificate sudo certbot certificates # Renew certificate sudo certbot renew # Check Nginx SSL config sudo nginx -t ``` ### WebSocket Not Working ```bash # 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 ## Firewall Setup (Optional but Recommended) ```bash # 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