# GitLab CI/CD Pipeline for LittleShop # Builds and deploys to Hostinger VPS stages: - build - test - deploy variables: DOCKER_HOST: unix:///var/run/docker.sock DOCKER_DRIVER: overlay2 # Registry configuration REGISTRY_URL: "localhost:5000" IMAGE_NAME: "littleshop" # Hostinger deployment configuration DEPLOY_HOST: "10.13.13.1" DEPLOY_PORT: "2255" CONTAINER_NAME: "littleshop-admin" # Build from .NET source and create Docker image build: stage: build image: mcr.microsoft.com/dotnet/sdk:9.0 services: - docker:24-dind before_script: - apt-get update && apt-get install -y docker.io - docker --version script: - echo "Building LittleShop application..." - cd LittleShop - dotnet publish -c Production -o ../publish --verbosity minimal - cd .. # Create optimized Dockerfile - | cat > Dockerfile << 'EOF' FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine WORKDIR /app COPY ./publish . # Create required directories and user RUN mkdir -p /app/data /app/wwwroot/uploads && \ adduser -D -u 1658 appuser && \ chown -R appuser:appuser /app USER appuser EXPOSE 8080 ENTRYPOINT ["dotnet", "LittleShop.dll"] EOF # Build and tag Docker image - export VERSION="${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA}" - docker build -t ${REGISTRY_URL}/${IMAGE_NAME}:${VERSION} . - docker tag ${REGISTRY_URL}/${IMAGE_NAME}:${VERSION} ${REGISTRY_URL}/${IMAGE_NAME}:latest # Save image for deployment - docker save ${REGISTRY_URL}/${IMAGE_NAME}:${VERSION} -o littleshop.tar - echo "Build complete for version ${VERSION}" artifacts: paths: - littleshop.tar - publish/ expire_in: 1 hour rules: - if: '$CI_COMMIT_BRANCH == "main"' - if: '$CI_COMMIT_TAG' tags: - docker # Run tests test: stage: test image: mcr.microsoft.com/dotnet/sdk:9.0 script: - echo "Running tests..." - cd LittleShop.Tests - dotnet test --no-restore --verbosity normal || true allow_failure: true rules: - if: '$CI_COMMIT_BRANCH == "main"' - if: '$CI_COMMIT_TAG' # Deploy to Hostinger VPS deploy:hostinger: stage: deploy image: alpine:latest dependencies: - build before_script: - apk add --no-cache openssh-client sshpass curl # Setup SSH key if provided - | if [ -n "$HOSTINGER_SSH_KEY" ]; then echo "$HOSTINGER_SSH_KEY" | base64 -d > /tmp/hostinger_key chmod 600 /tmp/hostinger_key export SSH_CMD="ssh -i /tmp/hostinger_key" export SCP_CMD="scp -i /tmp/hostinger_key" else export SSH_CMD="sshpass -p $DEPLOY_PASSWORD ssh" export SCP_CMD="sshpass -p $DEPLOY_PASSWORD scp" fi script: - export VERSION="${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA}" - echo "Deploying version ${VERSION} to Hostinger..." # Transfer Docker image to server - $SCP_CMD -P ${DEPLOY_PORT} -o StrictHostKeyChecking=no littleshop.tar ${DEPLOY_USER}@${DEPLOY_HOST}:/tmp/ # Deploy on server - | $SSH_CMD -p ${DEPLOY_PORT} -o StrictHostKeyChecking=no ${DEPLOY_USER}@${DEPLOY_HOST} << DEPLOY_SCRIPT set -e # Load Docker image echo "${DEPLOY_PASSWORD}" | sudo -S docker load -i /tmp/littleshop.tar # Stop and remove existing container echo "${DEPLOY_PASSWORD}" | sudo -S docker stop ${CONTAINER_NAME} 2>/dev/null || true echo "${DEPLOY_PASSWORD}" | sudo -S docker rm ${CONTAINER_NAME} 2>/dev/null || true # Run new container with authentication fix and all environment variables echo "${DEPLOY_PASSWORD}" | sudo -S docker run -d \ --name ${CONTAINER_NAME} \ --restart unless-stopped \ -p 5100:8080 \ -v /var/opt/littleshop/data:/app/data \ -v /var/opt/littleshop/uploads:/app/wwwroot/uploads \ -e ASPNETCORE_ENVIRONMENT=Production \ -e WebPush__VapidPublicKey='BDJtQu7zV0H3KF4FkrZ8nPwP3YD_3cEz3hqJvQ6L_gvNpG8ANksQB-FZy2-PDmFAu6duiN4p3mkcNAGnN4YRbws' \ -e WebPush__VapidPrivateKey='Hm_ttUKUqoLn5R8WQP5O1SIGxm0kVJXMZGCPMD1tUDY' \ -e WebPush__VapidSubject='mailto:admin@littleshop.local' \ -e ConnectionStrings__DefaultConnection='Data Source=/app/data/littleshop-production.db' \ -e Jwt__Key='2D7B5FE9C4A3E1D8B6A947F2C8E5D3A1B9F7E4C2D8A6B3E9F1C7D5A2E8B4F6C9' \ -e Jwt__Audience='LittleShop-Production' \ -e Jwt__ExpiryInHours='24' \ -e Jwt__Issuer='LittleShop-Production' \ -e SilverPay__AllowUnsignedWebhooks='false' \ -e SilverPay__WebhookSecret='04126be1b2ca9a586aaf25670c0ddb7a9afa106158074605a1016a2889655c20' \ --health-cmd='curl -f http://localhost:8080/health || exit 1' \ --health-interval=30s \ --health-timeout=10s \ --health-retries=3 \ ${REGISTRY_URL}/${IMAGE_NAME}:${VERSION} # Wait for container health echo "Waiting for container to be healthy..." for i in 1 2 3 4 5 6; do if echo "${DEPLOY_PASSWORD}" | sudo -S docker ps | grep -q "(healthy).*${CONTAINER_NAME}"; then echo "✅ Container is healthy" break fi echo "Waiting for health check... attempt \$i/6" sleep 10 done # Test authentication redirect echo "Testing authentication redirect..." curl -I http://localhost:5100/Admin 2>/dev/null | head -15 # Push to local registry for backup echo "${DEPLOY_PASSWORD}" | sudo -S docker push ${REGISTRY_URL}/${IMAGE_NAME}:${VERSION} 2>/dev/null || true echo "${DEPLOY_PASSWORD}" | sudo -S docker tag ${REGISTRY_URL}/${IMAGE_NAME}:${VERSION} ${REGISTRY_URL}/${IMAGE_NAME}:latest echo "${DEPLOY_PASSWORD}" | sudo -S docker push ${REGISTRY_URL}/${IMAGE_NAME}:latest 2>/dev/null || true # Health check API if curl -f -s http://localhost:5100/api/catalog/products > /dev/null 2>&1; then echo "✅ Deployment successful - API health check passed" else echo "⚠️ API health check failed but container is running" echo "${DEPLOY_PASSWORD}" | sudo -S docker logs ${CONTAINER_NAME} --tail 20 fi # Cleanup rm -f /tmp/littleshop.tar echo "Deployment of version ${VERSION} complete!" DEPLOY_SCRIPT environment: name: production url: http://${DEPLOY_HOST}:5100 rules: - if: '$CI_COMMIT_BRANCH == "main"' when: manual # Require manual approval for production - if: '$CI_COMMIT_TAG' when: manual tags: - docker # Rollback job rollback:hostinger: stage: deploy image: alpine:latest before_script: - apk add --no-cache openssh-client sshpass script: - echo "Rolling back to previous version..." - | if [ -n "$HOSTINGER_SSH_KEY" ]; then echo "$HOSTINGER_SSH_KEY" | base64 -d > /tmp/hostinger_key chmod 600 /tmp/hostinger_key SSH_CMD="ssh -i /tmp/hostinger_key" else SSH_CMD="sshpass -p $DEPLOY_PASSWORD ssh" fi - | $SSH_CMD -p ${DEPLOY_PORT} -o StrictHostKeyChecking=no ${DEPLOY_USER}@${DEPLOY_HOST} << ROLLBACK_SCRIPT set -e # Get previous image PREVIOUS_IMAGE=\$(echo "${DEPLOY_PASSWORD}" | sudo -S docker images ${REGISTRY_URL}/${IMAGE_NAME} --format "{{.Tag}}" | grep -v latest | head -2 | tail -1) if [ -z "\$PREVIOUS_IMAGE" ]; then echo "❌ No previous image found for rollback" exit 1 fi echo "Rolling back to ${REGISTRY_URL}/${IMAGE_NAME}:\$PREVIOUS_IMAGE" # Stop current container echo "${DEPLOY_PASSWORD}" | sudo -S docker stop ${CONTAINER_NAME} echo "${DEPLOY_PASSWORD}" | sudo -S docker rm ${CONTAINER_NAME} # Start with previous image echo "${DEPLOY_PASSWORD}" | sudo -S docker run -d \ --name ${CONTAINER_NAME} \ --restart unless-stopped \ -p 5100:8080 \ -v /var/opt/littleshop/data:/app/data \ -v /var/opt/littleshop/uploads:/app/wwwroot/uploads \ -e ASPNETCORE_ENVIRONMENT=Production \ -e WebPush__VapidPublicKey='BDJtQu7zV0H3KF4FkrZ8nPwP3YD_3cEz3hqJvQ6L_gvNpG8ANksQB-FZy2-PDmFAu6duiN4p3mkcNAGnN4YRbws' \ -e WebPush__VapidPrivateKey='Hm_ttUKUqoLn5R8WQP5O1SIGxm0kVJXMZGCPMD1tUDY' \ -e WebPush__VapidSubject='mailto:admin@littleshop.local' \ -e ConnectionStrings__DefaultConnection='Data Source=/app/data/littleshop-production.db' \ -e Jwt__Key='2D7B5FE9C4A3E1D8B6A947F2C8E5D3A1B9F7E4C2D8A6B3E9F1C7D5A2E8B4F6C9' \ -e Jwt__Audience='LittleShop-Production' \ -e Jwt__ExpiryInHours='24' \ -e Jwt__Issuer='LittleShop-Production' \ -e SilverPay__AllowUnsignedWebhooks='false' \ -e SilverPay__WebhookSecret='04126be1b2ca9a586aaf25670c0ddb7a9afa106158074605a1016a2889655c20' \ --health-cmd='curl -f http://localhost:8080/health || exit 1' \ --health-interval=30s \ --health-timeout=10s \ --health-retries=3 \ ${REGISTRY_URL}/${IMAGE_NAME}:\$PREVIOUS_IMAGE # Wait and check health sleep 30 if curl -f -s http://localhost:5100/api/catalog/products > /dev/null 2>&1; then echo "✅ Rollback complete - service is healthy" else echo "❌ Rollback health check failed" echo "${DEPLOY_PASSWORD}" | sudo -S docker logs ${CONTAINER_NAME} --tail 50 exit 1 fi ROLLBACK_SCRIPT environment: name: production when: manual rules: - if: '$CI_COMMIT_BRANCH == "main"' - if: '$CI_COMMIT_TAG' tags: - docker