diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c860191 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +node_modules +.git +.gitignore +.vscode +.idea +*.md +!README.md +.env +.env.* +dist +.astro diff --git a/.gitea/workflows/ci-cd.yaml b/.gitea/workflows/ci-cd.yaml new file mode 100644 index 0000000..c0a5282 --- /dev/null +++ b/.gitea/workflows/ci-cd.yaml @@ -0,0 +1,78 @@ +name: CI/CD Pipeline + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + # Build job - verify Astro build works + build: + name: Build Astro Site + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build site + run: npm run build + env: + NODE_ENV: production + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + retention-days: 1 + + # Deploy job - only on main branch push after build succeeds + deploy: + name: Deploy to Portainer + runs-on: ubuntu-latest + needs: [build] + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Gitea registry + uses: docker/login-action@v3 + with: + registry: gitlab.impresion3d.pro + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: | + gitlab.impresion3d.pro/root/davidaragon-portfolio:latest + gitlab.impresion3d.pro/root/davidaragon-portfolio:${{ github.sha }} + cache-from: type=registry,ref=gitlab.impresion3d.pro/root/davidaragon-portfolio:buildcache + cache-to: type=registry,ref=gitlab.impresion3d.pro/root/davidaragon-portfolio:buildcache,mode=max + + - name: Trigger Portainer webhook + run: | + curl -X POST "${{ secrets.PORTAINER_WEBHOOK_URL }}" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7a3f7fd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +# Multi-stage build for Astro static site + +# Stage 1: Build +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci --only=production + +# Copy source +COPY . . + +# Build static site +RUN npm run build + +# Stage 2: Production - nginx +FROM nginx:alpine + +# Copy built site to nginx html directory +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy nginx config (if needed for SPA routing) +COPY nginx.conf /etc/nginx/nginx.conf + +# Expose port +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..594844d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3.8' + +services: + portfolio: + build: . + container_name: davidaragon-portfolio + ports: + - "8080:80" + restart: unless-stopped + environment: + - NODE_ENV=production + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 5s diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..33edffa --- /dev/null +++ b/nginx.conf @@ -0,0 +1,51 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + keepalive_timeout 65; + gzip on; + gzip_vary on; + 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; + + server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Serve static files with cache headers + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # SPA fallback - serve index.html for all routes + location / { + try_files $uri $uri/ /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; + } +}