Skip to main content

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

1

Clean Previous Builds

cd backend
mvn clean
2

Run Tests

Ensure all tests pass before building:
mvn test
3

Build JAR

Create the production JAR file:
mvn package -DskipTests
This creates target/ems-backend-0.0.1-SNAPSHOT.jar
4

Verify JAR

Test the JAR locally:
java -jar target/ems-backend-0.0.1-SNAPSHOT.jar

Frontend Build

1

Set Production Environment

Create frontend/ems-frontend/.env.production:
VITE_API_BASE_URL=https://api.yourdomain.com/api
2

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.
3

Build Static Assets

npm run build
This creates an optimized dist/ folder with:
  • Minified JavaScript bundles
  • Optimized CSS
  • Compressed assets
  • Source maps (if configured)
4

Test Production Build

Preview the production build locally:
npm run preview

Deployment Strategies

Option 1: Traditional Server Deployment

Deploy to a VPS or dedicated server using systemd.

Backend Deployment

1

Upload JAR

Transfer the JAR to your server:
scp target/ems-backend-0.0.1-SNAPSHOT.jar user@server:/opt/ems/
2

Create Systemd Service

Create /etc/systemd/system/ems-backend.service:
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
3

Start Service

sudo systemctl daemon-reload
sudo systemctl enable ems-backend
sudo systemctl start ems-backend
sudo systemctl status ems-backend
4

View Logs

sudo journalctl -u ems-backend -f

Frontend Deployment with Nginx

1

Upload Build Files

scp -r dist/* user@server:/var/www/ems/
2

Configure Nginx

Create /etc/nginx/sites-available/ems:
ems.conf
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;
}
3

Enable Site

sudo ln -s /etc/nginx/sites-available/ems /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
4

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

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

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

docker-compose.yml
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

Option 3: Cloud Platform Deployment

AWS Deployment

Backend:
  1. Create Elastic Beanstalk application for Java
  2. Upload JAR file
  3. Configure environment variables in EB console
  4. Set up RDS MySQL instance
  5. Configure security groups
Frontend:
  1. Build static files: npm run build
  2. Upload to S3 bucket
  3. Enable static website hosting
  4. Configure CloudFront CDN
  5. Set up Route 53 for DNS

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:
  1. NEVER use spring.jpa.hibernate.ddl-auto=update in production
  2. Always use validate mode and manage schema changes explicitly
  3. 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:
1

Capture Current Schema

Export the development schema:
mysqldump -u app_user -p --no-data ems_db > schema.sql
2

Review and Clean

Review schema.sql and remove any development-only tables or columns.
3

Apply to Production

mysql -u prod_user -p ems_db < schema.sql
4

Set Validate Mode

In production application.properties:
spring.jpa.hibernate.ddl-auto=validate
5

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;
    }
}

Performance Optimization

Backend Optimizations

  • 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:
pom.xml
<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:
application.properties
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

1

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
2

Database Backup

Before any deployment:
mysqldump -u user -p ems_db > ems_db_$(date +%Y%m%d_%H%M%S).sql
3

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
4

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:
k6-test.js
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

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
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;
      }
    }
  }
}
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”)
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

Build docs developers (and LLMs) love