Overview
This guide covers deploying the Extracurricular Management System to production environments. EMS consists of a Spring Boot backend and a React frontend that can be deployed independently or together.
Pre-Deployment Checklist
All tests passing locally
Production configuration files prepared (no dev credentials)
Database backup and migration strategy in place
SSL/TLS certificates obtained for HTTPS
Monitoring and logging configured
Secrets stored in secure secrets manager
Database schema set to validate mode (not update)
Resource limits and scaling policies defined
Building for Production
Backend Build
Run Tests
Ensure all tests pass before building:
Build JAR
Create the production JAR file: This creates target/ems-backend-0.0.1-SNAPSHOT.jar
Verify JAR
Test the JAR locally: java -jar target/ems-backend-0.0.1-SNAPSHOT.jar
Frontend Build
Set Production Environment
Create frontend/ems-frontend/.env.production: VITE_API_BASE_URL = https://api.yourdomain.com/api
Install Dependencies
cd frontend/ems-frontend
npm ci --production=false
npm ci (clean install) is preferred over npm install in CI/CD pipelines for reproducible builds.
Build Static Assets
This creates an optimized dist/ folder with:
Minified JavaScript bundles
Optimized CSS
Compressed assets
Source maps (if configured)
Test Production Build
Preview the production build locally:
Deployment Strategies
Option 1: Traditional Server Deployment
Deploy to a VPS or dedicated server using systemd.
Backend Deployment
Upload JAR
Transfer the JAR to your server: scp target/ems-backend-0.0.1-SNAPSHOT.jar user@server:/opt/ems/
Create Systemd Service
Create /etc/systemd/system/ems-backend.service: [Unit]
Description =EMS Backend Service
After =network.target mysql.service
[Service]
Type =simple
User =ems
WorkingDirectory =/opt/ems
ExecStart =/usr/bin/java -jar /opt/ems/ems-backend-0.0.1-SNAPSHOT.jar
# Environment variables
Environment = "DB_URL=jdbc:mysql://localhost:3306/ems_db?useSSL=true"
Environment = "DB_USERNAME=ems_prod"
EnvironmentFile =/opt/ems/secrets.env
# Resource limits
MemoryLimit =1G
CPUQuota =50%
# Restart policy
Restart =on-failure
RestartSec =10s
# Logging
StandardOutput =journal
StandardError =journal
SyslogIdentifier =ems-backend
[Install]
WantedBy =multi-user.target
Create /opt/ems/secrets.env with sensitive values: DB_PASSWORD = your_secure_password
JWT_SECRET = your_jwt_secret_32_chars_min
SMTP_PASSWORD = your_smtp_password
Secure the secrets file: chmod 600 /opt/ems/secrets.env
chown ems:ems /opt/ems/secrets.env
Start Service
sudo systemctl daemon-reload
sudo systemctl enable ems-backend
sudo systemctl start ems-backend
sudo systemctl status ems-backend
View Logs
sudo journalctl -u ems-backend -f
Frontend Deployment with Nginx
Upload Build Files
scp -r dist/ * user@server:/var/www/ems/
Configure Nginx
Create /etc/nginx/sites-available/ems: server {
listen 80 ;
listen [::]:80;
server_name yourdomain.com www.yourdomain.com;
# Redirect HTTP to HTTPS
return 301 https://$ server_name $ request_uri ;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
# SSL Configuration
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on ;
# Frontend static files
root /var/www/ems;
index index.html;
# Gzip compression
gzip on ;
gzip_vary on ;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# SPA routing - serve index.html for all routes
location / {
try_files $ uri $ uri / /index.html;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable" ;
}
# Proxy API requests to backend
location /api/ {
proxy_pass http://localhost:8080/api/;
proxy_http_version 1.1 ;
proxy_set_header Upgrade $ http_upgrade ;
proxy_set_header Connection 'upgrade' ;
proxy_set_header Host $ host ;
proxy_set_header X-Real-IP $ remote_addr ;
proxy_set_header X-Forwarded-For $ proxy_add_x_forwarded_for ;
proxy_set_header X-Forwarded-Proto $ scheme ;
proxy_cache_bypass $ http_upgrade ;
# Timeouts
proxy_connect_timeout 60s ;
proxy_send_timeout 60s ;
proxy_read_timeout 60s ;
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" 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:;" always;
}
Enable Site
sudo ln -s /etc/nginx/sites-available/ems /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Obtain SSL Certificate
Use Let’s Encrypt for free SSL certificates: sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
Option 2: Docker Deployment
Containerize both applications for consistent deployments.
Backend Dockerfile
# Build stage
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests
# Runtime stage
FROM eclipse-temurin:21-jre-jammy
WORKDIR /app
# Create non-root user
RUN groupadd -r ems && useradd -r -g ems ems
# Copy JAR from build stage
COPY --from=build /app/target/ems-backend-0.0.1-SNAPSHOT.jar app.jar
# Change ownership
RUN chown -R ems:ems /app
USER ems
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
EXPOSE 8080
ENTRYPOINT [ "java" , "-jar" , "app.jar" ]
Frontend Dockerfile
# Build stage
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --production=false
COPY . .
ARG VITE_API_BASE_URL
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
RUN npm run build
# Runtime stage with Nginx
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
# Custom Nginx config for SPA routing
RUN echo 'server { \
listen 80; \
location / { \
root /usr/share/nginx/html; \
index index.html; \
try_files $uri $uri/ /index.html; \
} \
}' > /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD [ "nginx" , "-g" , "daemon off;" ]
Docker Compose
version : '3.8'
services :
db :
image : mysql:8.0
restart : always
environment :
MYSQL_ROOT_PASSWORD : ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE : ems_db
MYSQL_USER : app_user
MYSQL_PASSWORD : ${DB_PASSWORD}
volumes :
- mysql_data:/var/lib/mysql
ports :
- "3306:3306"
healthcheck :
test : [ "CMD" , "mysqladmin" , "ping" , "-h" , "localhost" ]
interval : 10s
timeout : 5s
retries : 5
backend :
build :
context : ./backend
dockerfile : Dockerfile
restart : always
depends_on :
db :
condition : service_healthy
environment :
- DB_URL=jdbc:mysql://db:3306/ems_db?useSSL=false
- DB_USERNAME=app_user
- DB_PASSWORD=${DB_PASSWORD}
- JWT_SECRET=${JWT_SECRET}
- SMTP_HOST=${SMTP_HOST}
- SMTP_PORT=${SMTP_PORT}
- SMTP_USERNAME=${SMTP_USERNAME}
- SMTP_PASSWORD=${SMTP_PASSWORD}
ports :
- "8080:8080"
healthcheck :
test : [ "CMD" , "curl" , "-f" , "http://localhost:8080/actuator/health" ]
interval : 30s
timeout : 10s
retries : 3
start_period : 60s
frontend :
build :
context : ./frontend/ems-frontend
dockerfile : Dockerfile
args :
VITE_API_BASE_URL : http://localhost:8080/api
restart : always
depends_on :
- backend
ports :
- "80:80"
volumes :
mysql_data :
Deploy with Docker Compose
# Create .env file with secrets
cat > .env << EOF
MYSQL_ROOT_PASSWORD=secure_root_password
DB_PASSWORD=secure_db_password
JWT_SECRET=$( openssl rand -hex 32 )
SMTP_HOST=smtp.office365.com
SMTP_PORT=587
[email protected]
SMTP_PASSWORD=smtp_password
EOF
# Build and start all services
docker-compose up -d --build
# View logs
docker-compose logs -f
# Check status
docker-compose ps
AWS Deployment
Elastic Beanstalk
ECS (Docker)
Backend :
Create Elastic Beanstalk application for Java
Upload JAR file
Configure environment variables in EB console
Set up RDS MySQL instance
Configure security groups
Frontend :
Build static files: npm run build
Upload to S3 bucket
Enable static website hosting
Configure CloudFront CDN
Set up Route 53 for DNS
Push Docker images to ECR
Create ECS cluster
Define task definitions for backend/frontend
Configure Application Load Balancer
Set up RDS MySQL instance
Use AWS Secrets Manager for credentials
Configure auto-scaling policies
Azure Deployment
Backend :az webapp create --name ems-backend --runtime "JAVA:21-java21" \
--resource-group ems-rg
az webapp config appsettings set --name ems-backend \
--settings DB_URL="..." JWT_SECRET="..."
az webapp deployment source config-zip --name ems-backend \
--src target/ems-backend-0.0.1-SNAPSHOT.jar
Frontend :az storage account create --name emsfrontend
az storage blob service-properties update --account-name emsfrontend \
--static-website --index-document index.html
az storage blob upload-batch -s dist -d '$web' --account-name emsfrontend
Production Configuration
Backend Production Settings
application-production.properties
# Server
server.port =8080
server.compression.enabled =true
server.http2.enabled =true
# Database - use environment variables
spring.datasource.url =${DB_URL}
spring.datasource.username =${DB_USERNAME}
spring.datasource.password =${DB_PASSWORD}
# CRITICAL: Never use 'update' in production
spring.jpa.hibernate.ddl-auto =validate
# Connection pooling
spring.datasource.hikari.maximum-pool-size =20
spring.datasource.hikari.minimum-idle =5
spring.datasource.hikari.connection-timeout =30000
spring.datasource.hikari.idle-timeout =600000
spring.datasource.hikari.max-lifetime =1800000
# JWT
secretJwtString =${JWT_SECRET}
expirationInt =86400000
# Email
spring.mail.host =${SMTP_HOST}
spring.mail.port =${SMTP_PORT}
spring.mail.username =${SMTP_USERNAME}
spring.mail.password =${SMTP_PASSWORD}
spring.mail.properties.mail.smtp.auth =true
spring.mail.properties.mail.smtp.starttls.enable =true
# Logging
logging.level.root =INFO
logging.level.com.ems.backend =INFO
logging.level.org.springframework.web =WARN
logging.level.org.hibernate =WARN
# Actuator (monitoring)
management.endpoints.web.exposure.include =health,info,metrics
management.endpoint.health.show-details =when-authorized
# File upload limits
spring.servlet.multipart.max-file-size =10MB
spring.servlet.multipart.max-request-size =10MB
Critical Production Settings :
NEVER use spring.jpa.hibernate.ddl-auto=update in production
Always use validate mode and manage schema changes explicitly
The @Version annotation on entities (e.g., Event.java:83) provides optimistic locking to prevent data corruption during concurrent updates
Database Migration Strategy
Since EMS uses ddl-auto=update in development, switching to production requires a migration strategy:
Capture Current Schema
Export the development schema: mysqldump -u app_user -p --no-data ems_db > schema.sql
Review and Clean
Review schema.sql and remove any development-only tables or columns.
Apply to Production
mysql -u prod_user -p ems_db < schema.sql
Set Validate Mode
In production application.properties: spring.jpa.hibernate.ddl-auto =validate
Future Migrations
For schema changes, consider using:
Flyway : Version-controlled SQL migrations
Liquibase : Database-agnostic migrations
Add to pom.xml: < dependency >
< groupId > org.flywaydb </ groupId >
< artifactId > flyway-core </ artifactId >
</ dependency >
< dependency >
< groupId > org.flywaydb </ groupId >
< artifactId > flyway-mysql </ artifactId >
</ dependency >
Security Hardening
Database Security
Use SSL/TLS for database connections
Restrict database user permissions
Enable binary logging for point-in-time recovery
Regular automated backups
Network isolation (private subnet)
Application Security
Rotate JWT secrets regularly
Use strong, unique passwords
Enable HTTPS only (redirect HTTP)
Configure CORS appropriately
Keep dependencies updated
Infrastructure Security
Firewall rules (only ports 80, 443, 22)
SSH key-based authentication
Regular OS security updates
Intrusion detection system (IDS)
DDoS protection
Monitoring & Logging
Centralized logging (ELK, CloudWatch)
Application performance monitoring
Security event alerting
Resource utilization metrics
Audit trail (see AuditLog entity)
Spring Security Configuration
Ensure your production security configuration is strict:
SecurityConfig.java (example snippet)
@ Configuration
@ EnableWebSecurity
public class SecurityConfig {
@ Bean
public SecurityFilterChain filterChain ( HttpSecurity http ) throws Exception {
http
. csrf (csrf -> csrf . disable ()) // Using JWT instead
. cors (cors -> cors . configurationSource ( corsConfigurationSource ()))
. sessionManagement (session ->
session . sessionCreationPolicy ( SessionCreationPolicy . STATELESS )
)
. authorizeHttpRequests (auth -> auth
. requestMatchers ( "/api/auth/**" ). permitAll ()
. requestMatchers ( "/actuator/health" ). permitAll ()
. anyRequest (). authenticated ()
)
. addFilterBefore (jwtAuthFilter, UsernamePasswordAuthenticationFilter . class );
return http . build ();
}
@ Bean
public CorsConfigurationSource corsConfigurationSource () {
CorsConfiguration configuration = new CorsConfiguration ();
configuration . setAllowedOrigins ( List . of (
"https://yourdomain.com" ,
"https://www.yourdomain.com"
));
configuration . setAllowedMethods ( List . of ( "GET" , "POST" , "PUT" , "DELETE" , "PATCH" ));
configuration . setAllowedHeaders ( List . of ( "*" ));
configuration . setAllowCredentials ( true );
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource ();
source . registerCorsConfiguration ( "/api/**" , configuration);
return source;
}
}
Backend Optimizations
Database Query Optimization
Use indexes on frequently queried columns (eventID, userID, etc.)
Implement pagination for list endpoints
Use @EntityGraph to prevent N+1 queries
Enable query caching for read-heavy operations
Monitor slow query log
Add Redis for caching: < dependency >
< groupId > org.springframework.boot </ groupId >
< artifactId > spring-boot-starter-data-redis </ artifactId >
</ dependency >
spring.cache.type =redis
spring.redis.host =localhost
spring.redis.port =6379
Cache read-heavy endpoints: @ Cacheable ( value = "events" , key = "#eventID" )
public EventDTO getEvent ( Long eventID) { ... }
Configure HikariCP (included in Spring Boot): spring.datasource.hikari.maximum-pool-size =20
spring.datasource.hikari.minimum-idle =5
spring.datasource.hikari.connection-timeout =30000
Set optimal JVM flags: java -jar \
-Xms512m -Xmx1g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
app.jar
Frontend Optimizations
Code Splitting : Vite automatically splits code by route
Asset Optimization : Images compressed and served via CDN
Caching : Set long cache times for versioned assets
Compression : Enable Gzip/Brotli in Nginx/CDN
Lazy Loading : Load components on demand
Monitoring & Observability
Spring Boot Actuator
Monitor application health and metrics:
management.endpoints.web.exposure.include =health,info,metrics,prometheus
management.endpoint.health.show-details =when-authorized
management.metrics.export.prometheus.enabled =true
Access metrics:
Health: GET /actuator/health
Metrics: GET /actuator/metrics
Prometheus: GET /actuator/prometheus
Logging
Configure structured logging:
logging.pattern.console =%d{yyyy-MM-dd HH:mm:ss} - %msg%n
logging.pattern.file =%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
logging.file.name =/var/log/ems/application.log
logging.file.max-size =10MB
logging.file.max-history =30
Monitoring Stack
Recommended monitoring tools:
Prometheus Metrics collection and time-series database
Grafana Visualization and dashboards
ELK Stack Elasticsearch, Logstash, Kibana for log aggregation
Rollback Strategy
Keep Previous Version
Always keep the previous working deployment: mv ems-backend-0.0.1-SNAPSHOT.jar ems-backend-0.0.1-SNAPSHOT.jar.backup
Database Backup
Before any deployment: mysqldump -u user -p ems_db > ems_db_ $( date +%Y%m%d_%H%M%S ) .sql
Blue-Green Deployment
Run new version alongside old, switch traffic when validated:
Backend: Run on different port, update load balancer
Frontend: Deploy to new S3/CDN path, update DNS
Rollback Procedure
If issues arise: # Stop new version
sudo systemctl stop ems-backend
# Restore previous version
cp ems-backend-0.0.1-SNAPSHOT.jar.backup ems-backend-0.0.1-SNAPSHOT.jar
# Restart
sudo systemctl start ems-backend
Testing in Production
Always test in a staging environment that mirrors production before deploying to production.
Smoke Tests
After deployment, verify critical functionality:
# Health check
curl https://api.yourdomain.com/actuator/health
# Authentication
curl -X POST https://api.yourdomain.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected] ","password":"password"}'
# Public endpoint
curl https://api.yourdomain.com/api/events
Load Testing
Use tools like Apache JMeter or k6 to simulate traffic:
import http from 'k6/http' ;
import { check } from 'k6' ;
export let options = {
vus: 100 , // 100 virtual users
duration: '5m' ,
};
export default function () {
let res = http . get ( 'https://api.yourdomain.com/api/events' );
check ( res , {
'status is 200' : ( r ) => r . status === 200 ,
'response time < 500ms' : ( r ) => r . timings . duration < 500 ,
});
}
Run: k6 run k6-test.js
Continuous Deployment
GitHub Actions Example
.github/workflows/deploy.yml
name : Deploy to Production
on :
push :
branches : [ main ]
jobs :
deploy-backend :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v3
- name : Set up Java 21
uses : actions/setup-java@v3
with :
java-version : '21'
distribution : 'temurin'
- name : Build with Maven
run : |
cd backend
mvn clean package -DskipTests
- name : Deploy to Server
uses : appleboy/scp-action@master
with :
host : ${{ secrets.SERVER_HOST }}
username : ${{ secrets.SERVER_USER }}
key : ${{ secrets.SSH_KEY }}
source : "backend/target/ems-backend-0.0.1-SNAPSHOT.jar"
target : "/opt/ems/"
- name : Restart Service
uses : appleboy/ssh-action@master
with :
host : ${{ secrets.SERVER_HOST }}
username : ${{ secrets.SERVER_USER }}
key : ${{ secrets.SSH_KEY }}
script : |
sudo systemctl restart ems-backend
sleep 10
sudo systemctl status ems-backend
deploy-frontend :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v3
- name : Set up Node.js
uses : actions/setup-node@v3
with :
node-version : '20'
- name : Build Frontend
run : |
cd frontend/ems-frontend
npm ci
npm run build
env :
VITE_API_BASE_URL : https://api.yourdomain.com/api
- name : Deploy to Server
uses : appleboy/scp-action@master
with :
host : ${{ secrets.SERVER_HOST }}
username : ${{ secrets.SERVER_USER }}
key : ${{ secrets.SSH_KEY }}
source : "frontend/ems-frontend/dist/*"
target : "/var/www/ems/"
Post-Deployment
Verify all services are running
Test critical user flows (login, event creation, registration)
Monitor error rates and response times
Check database connection pool utilization
Verify email notifications are sending
Test JWT token generation and validation
Confirm SSL certificate is valid
Review application logs for errors
Support & Troubleshooting
Common Production Issues
High Database Connection Count
Symptoms : Connection pool exhausted errorsSolutions :
Increase spring.datasource.hikari.maximum-pool-size
Check for connection leaks (unclosed connections)
Implement connection timeout monitoring
Scale database vertically or use read replicas
OptimisticLockException During Events
Symptoms : Concurrent registration failuresCause : Multiple users registering simultaneouslySolution : This is expected behavior from the @Version field on Event entity (Event.java:83). The application implements optimistic locking to prevent race conditions. Implement retry logic in the frontend:async function registerForEvent ( eventID ) {
let retries = 3 ;
while ( retries > 0 ) {
try {
await api . post ( `/events/ ${ eventID } /register` );
return ;
} catch ( error ) {
if ( error . response ?. status === 409 && retries > 1 ) {
retries -- ;
await sleep ( 100 );
} else {
throw error ;
}
}
}
}
JWT Token Validation Fails
Symptoms : Users repeatedly logged outSolutions :
Verify secreteJwtString matches across all backend instances
Check token expiration time is appropriate
Ensure server clocks are synchronized (NTP)
Check for secreteJwtString typo (not “secretJwtString”)
Email Notifications Not Sending
Symptoms : Registration confirmations not receivedSolutions :
Check SMTP credentials are correct
Verify firewall allows outbound port 587
Check email service rate limits
Review application logs for SMTP errors
Test with a different SMTP provider
Next Steps
Setup Guide Set up local development environment
Configuration Detailed configuration reference
Architecture Understand the system architecture
API Reference Explore the REST API