diff --git a/workspaces/frontend/.dockerignore b/workspaces/frontend/.dockerignore new file mode 100644 index 000000000..763301fc0 --- /dev/null +++ b/workspaces/frontend/.dockerignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ \ No newline at end of file diff --git a/workspaces/frontend/Dockerfile b/workspaces/frontend/Dockerfile new file mode 100644 index 000000000..32a729c6a --- /dev/null +++ b/workspaces/frontend/Dockerfile @@ -0,0 +1,60 @@ +# ---------- Builder stage ---------- +FROM node:20-slim AS builder + +# Set working directory +WORKDIR /usr/src/app + +# Copy package files to the container +COPY package*.json ./ + +# Install the dependencies and build +RUN npm cache clean --force \ + && npm ci + +# Copy source code +COPY . . + +# Build the application +RUN npm run build:prod + + +# ---------- Production stage ---------- +FROM nginx:alpine + +USER root + +# Install envsubst (gettext package) +RUN apk add --no-cache gettext + +# Copy built assets from builder stage +COPY --from=builder /usr/src/app/dist /usr/share/nginx/html + +# Copy nginx config +COPY nginx.conf /etc/nginx/nginx.conf + +# Create directories and set permissions for non-root user +RUN mkdir -p /var/cache/nginx/client_temp \ + /var/cache/nginx/proxy_temp \ + /var/cache/nginx/fastcgi_temp \ + /var/cache/nginx/uwsgi_temp \ + /var/cache/nginx/scgi_temp \ + /var/run/nginx \ + /tmp/nginx && \ + # Change ownership of nginx directories to nginx user (UID 101) + chown -R 101:101 /var/cache/nginx \ + /var/run/nginx \ + /usr/share/nginx/html \ + /tmp/nginx \ + /etc/nginx + +# Switch to nginx user (UID 101) +USER 101:101 + +# Expose port +EXPOSE 8080 + +# Set environment variables +ENV PORT=8080 + +# Start the production server +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/workspaces/frontend/nginx.conf b/workspaces/frontend/nginx.conf new file mode 100644 index 000000000..60ade5887 --- /dev/null +++ b/workspaces/frontend/nginx.conf @@ -0,0 +1,66 @@ +worker_processes auto; + +error_log /dev/stderr warn; +pid /tmp/nginx.pid; + +events { + worker_connections 1024; +} + +http { + log_format main '$remote_addr - $remote_user [$time_local] - $http_x_api_version - "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /dev/stdout main; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Temporary file paths for non-root user + client_body_temp_path /var/cache/nginx/client_temp; + proxy_temp_path /var/cache/nginx/proxy_temp; + fastcgi_temp_path /var/cache/nginx/fastcgi_temp; + uwsgi_temp_path /var/cache/nginx/uwsgi_temp; + scgi_temp_path /var/cache/nginx/scgi_temp; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; + + # Gzip Compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/yaml application/xml application/xml+rss text/javascript image/svg+xml; + gzip_comp_level 5; + gzip_min_length 1000; + gzip_proxied any; + gzip_vary on; + gzip_disable "msie6"; + + server { + listen 8080; + + # Health check endpoint + location /health { + access_log off; + return 200 'healthy\n'; + } + + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ /index.html; + } + + # Static assets (cache enabled) + location ~* \.(css|js|gif|jpeg|jpg|png|ico|woff|woff2|ttf|otf|svg|eot)$ { + root /usr/share/nginx/html; + expires 30d; + add_header Cache-Control "public, no-transform"; + try_files $uri =404; + } + } +}