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:
- Database Name:
lottery_db(default, can be changed) - Database Username:
root(default, can be changed) - Database Password: Choose a strong, secure password
- 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 MySQLdb- This is the service name indocker-compose.prod.yml(acts as hostname in Docker network)3306- Default MySQL port (internal to Docker network)lottery_db- Database name (must matchMYSQL_DATABASEin docker-compose)
Why db as hostname?
- In Docker Compose, services communicate using their service names as hostnames
- The MySQL service is named
dbindocker-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(notlocalhostor 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_dbwill 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_PASSWORDfrom/run/secrets/lottery-config.properties - Exports
DB_PASSWORDandDB_ROOT_PASSWORDwith 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_PASSWORDandDB_ROOT_PASSWORDmust matchSPRING_DATASOURCE_PASSWORDfrom 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.propertieswith database credentials - ✅ Environment variables
DB_PASSWORDandDB_ROOT_PASSWORDset (usesource scripts/load-db-password.shfrom 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:
-
MySQL container starts first (
lottery-mysql)- Creates the database
lottery_dbautomatically (if it doesn't exist) - Sets up the root user with your password from
DB_PASSWORD - Waits until healthy before backend starts
- Creates the database
-
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
- Loads configuration from
Wait for the database to be ready and migrations to complete. You should see:
lottery-mysqlcontainer runninglottery-backendcontainer 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.propertiesFlyway migrationmessages showing tables being created- No connection errors
If you see connection errors:
- Verify
SPRING_DATASOURCE_PASSWORDin secret file matchesDB_PASSWORDenvironment 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:
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:
- Replace
server_name _;with your domain name (e.g.,server_name yourdomain.com;) - Update SSL certificate paths if using Let's Encrypt (see Step 6)
- Verify paths match your directory structure
5.2 Link Nginx Configuration
# 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
- Open
https://yourdomain.comin a browser - Test the Telegram Mini App
- Verify API calls work (check browser console)
- 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 byroot: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)
# 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:
- Check logs first (backend, Nginx, Docker)
- Verify secret file exists at
/run/secrets/lottery-config.propertiesand has correct values - Verify secret file permissions (
chmod 640, owned byroot:docker) - Check backend logs for "Loading configuration from mounted secret file" message
- Ensure all directories exist and have proper permissions
- Verify network connectivity between containers
- Check SSL certificate validity
Last Updated: 2026-01-24 Version: 1.0