Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions backend/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ services:
target: production
container_name: ctf-backend
restart: unless-stopped
ports:
- "${PORT:-3000}:3000"
# ports:
# - "${PORT:-3000}:3000"
volumes:
- ./src/utils/ca.pem:/app/dist/utils/key.pem:ro
env_file:
Expand All @@ -68,7 +68,18 @@ services:
condition: service_started
mongo:
condition: service_healthy

nginx:
image: nginx:alpine
container_name: ctf-nginx
restart: unless-stopped
ports:
- "3000:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
networks:
- ctf-network
depends_on:
- app

networks:
ctf-network:
Expand Down
2 changes: 1 addition & 1 deletion backend/dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ RUN npm ci --omit=dev && \
# Copy built files from builder stage
COPY --from=builder /app/dist ./dist


#Copy tls certificate
COPY src/utils/ca.pem src/utils/cert.pem ./dist/utils/

# Change ownership to non-root user
Expand Down
13 changes: 10 additions & 3 deletions backend/nginx.conf
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
events {
# Configuration for connection processing
worker_connections 1024;
worker_connections 1024;
}
http {
include mime.types;

upstream backendserver {
server 127.0.0.1:3000;
server ctf-backend:3000;
}

server {
listen 80; # Listen on standard web port
server_name localhost;
server_name _;

location / {
# Allow Cors Headers for Frontend
add_header 'Access-Control-Allow-Origin' 'http://localhost:5173' always;
# Allow specific methods
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE' always;

# 3. Allow specific headers (like Content-Type or Authorization)
add_header 'Access-Control-Allow-Headers' 'X-Requested-With,Content-Type,Authorization' always;
proxy_pass http://backendserver/;
# Important headers to keep the user's info intact
proxy_set_header Host $host;
Expand Down
69 changes: 69 additions & 0 deletions frontend/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Dependencies
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Build outputs
dist
build
.vite

# Environment files
.env
.env.*
!.env.example

# Git
.git
.gitignore
.gitattributes

# IDE
.vscode
.idea
*.swp
*.swo
*~

# OS
.DS_Store
Thumbs.db

# Testing
coverage
.nyc_output
*.test.ts
*.test.tsx
*.spec.ts
*.spec.tsx

# Documentation
README.md
docs
*.md

# CI/CD
.github
.gitlab-ci.yml
.travis.yml
.circleci

# Docker
Dockerfile
.dockerignore
docker-compose*.yml
compose.yaml

# Misc
scripts
*.log
.cache

# TypeScript
*.tsbuildinfo

# ESLint
.eslintcache

67 changes: 67 additions & 0 deletions frontend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# ============================================
# Build Stage
# ============================================
FROM node:20-alpine AS builder

# Set working directory
WORKDIR /app

# Install security updates and required packages
RUN apk update && \
apk upgrade && \
apk add --no-cache libc6-compat && \
rm -rf /var/cache/apk/*

# Copy package files
COPY package.json package-lock.json ./

# Install dependencies with clean cache
RUN npm ci --only=production=false && \
npm cache clean --force

# Copy source code and configuration files
COPY . .

# Build the application
RUN npm run build

# ============================================
# Production Stage
# ============================================
FROM nginx:1.27-alpine AS production

# Install security updates
RUN apk update && \
apk upgrade && \
apk add --no-cache curl && \
rm -rf /var/cache/apk/*

# Remove default nginx website
RUN rm -rf /usr/share/nginx/html/*

# Copy built files from builder stage
COPY --from=builder /app/dist /usr/share/nginx/html

# Copy custom nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf

# Create necessary directories and set permissions
# Nginx runs as root initially to bind to port 80, then drops to nginx user
RUN mkdir -p /var/cache/nginx /var/log/nginx /etc/nginx/conf.d && \
chown -R nginx:nginx /usr/share/nginx/html && \
chown -R nginx:nginx /var/cache/nginx && \
chown -R nginx:nginx /var/log/nginx && \
chmod -R 755 /usr/share/nginx/html && \
chmod -R 755 /var/cache/nginx && \
chmod -R 755 /var/log/nginx

# Expose port 80
EXPOSE 80

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost/ || exit 1

# Start nginx
CMD ["nginx", "-g", "daemon off;"]

76 changes: 76 additions & 0 deletions frontend/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
server {
listen 0.0.0.0:80;
server_name _;
root /usr/share/nginx/html;
index index.html;

# 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 "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

# Content Security Policy
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'
'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src
'self' data: https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self'
http://localhost:3000; frame-ancestors 'self';" always;

# Hide nginx version
server_tokens off;

# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml;

# Cache static assets
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# API proxy to backend
location /api/ {
proxy_pass http://127.0.0.1:3000/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;
}

# Handle React Router (SPA routing)
location / {
try_files $uri $uri/ /index.html;
}

# Deny access to hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}

# Deny access to backup files
location ~ ~$ {
deny all;
access_log off;
log_not_found off;
}

# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}

3 changes: 1 addition & 2 deletions frontend/src/services/api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import axios from 'axios';

const API_BASE_URL =
import.meta.env.VITE_API_URL || 'http://localhost:3000/api';
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:80/api';

const api = axios.create({
baseURL: API_BASE_URL,
Expand Down