From f332d62f8a47f99f0f2dff44a6d662137ae047be Mon Sep 17 00:00:00 2001 From: nicomahery Date: Mon, 26 Jan 2026 14:19:24 +0100 Subject: [PATCH] initial commit --- .gitea/workflows/ci.yml | 169 +++++++++++++++++++++++++++++++++++ Dockerfile | 19 ++++ README.md | 111 +++++++++++++++++++++++ assets/scripts/main.js | 14 +++ assets/styles/style.css | 103 +++++++++++++++++++++ docker/default.conf.template | 22 +++++ docker/nginx.conf | 26 ++++++ index.html | 74 +++++++++++++++ 8 files changed, 538 insertions(+) create mode 100644 .gitea/workflows/ci.yml create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 assets/scripts/main.js create mode 100644 assets/styles/style.css create mode 100644 docker/default.conf.template create mode 100644 docker/nginx.conf create mode 100644 index.html diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..829ee42 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,169 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + +env: + IMAGE_NAME: ${{ vars.IMAGE_NAME }} + REGISTRY_LOCATION: ${{ vars.REGISTRY_LOCATION }} + PORT: ${{ vars.PORT }} + TEST_PORT: ${{ vars.TEST_PORT }} + REGISTRY_USER: ${{ secrets.REGISTRY_USER }} + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + PROD_SERVER_HOST: ${{ secrets.PROD_SERVER_HOST }} + DEPLOY_USER: ${{ secrets.DEPLOY_USER }} + +jobs: + lint: + name: βœ… Lint & Validate + runs-on: ubuntu-latest + + steps: + - name: πŸ“₯ Checkout repository + uses: actions/checkout@v4 + + - name: πŸ”§ Validate HTML + run: | + echo "Basic HTML validation" + test -f index.html + + - name: πŸ‹ Validate Dockerfile + run: | + dockerfilelint Dockerfile || true + + build: + name: Build & Push Docker Image + runs-on: ubuntu-latest + needs: lint + + steps: + - name: πŸ“₯ Checkout repository + uses: actions/checkout@v4 + + - name: πŸ”– Set image metadata + run: | + echo "RUN_NUMBER=${GITEA_RUN_NUMBER}" >> $GITEA_ENV + + - name: πŸ‹ Build Docker image + run: | + docker build \ + -t $IMAGE_NAME:$RUN_NUMBER \ + -t $IMAGE_NAME:latest \ + . + + - name: ℹ️ Image info + run: docker images | grep $IMAGE_NAME + + - name: πŸ”‘ Login to registry + run: | + echo "$REGISTRY_TOKEN" | docker login $REGISTRY_LOCATION \ + -u "$REGISTRY_USER" --password-stdin + + - name: 🏷️ Build images + run: | + docker tag $IMAGE_NAME:latest $REGISTRY_LOCATION/$IMAGE_NAME:latest + docker tag $IMAGE_NAME:$RUN_NUMBER $REGISTRY_LOCATION/$IMAGE_NAME:build-$RUN_NUMBER + + - name: πŸš€ Push images + run: | + docker push $REGISTRY_LOCATION/$IMAGE_NAME:latest + docker push $REGISTRY_LOCATION/$IMAGE_NAME:build-$RUN_NUMBER + + deploy: + name: πŸš€ Deploy image to prod + runs-on: ubuntu-latest + needs: build + + steps: + - name: πŸ”‘ Setup SSH + run: | + mkdir -p ~/.ssh + echo "$DEPLOY_SSH_KEY" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -H $PROD_SERVER_HOST >> ~/.ssh/known_hosts + + - name: πŸ”‘ Login to registry + run: | + echo "$REGISTRY_TOKEN" | docker login $REGISTRY_LOCATION \ + -u "$REGISTRY_USER" --password-stdin + + - name: 🚒 Deploy container + run: | + ssh $DEPLOY_USER@$PROD_SERVER_HOST << 'EOF' + set -e + NEW_IMAGE_NAME="$REGISTRY_LOCATION/$IMAGE_NAME:build-$RUN_NUMBER" + OLD_CONTAINER_NAME=$(docker inspect $IMAGE_NAME --format='{{.Config.Image}}') + NEW_CONTAINER_IMAGE="$IMAGE_NAME:build-$RUN_NUMBER" + + echo "Pulling image $NEW_IMAGE_NAME" + docker pull "$NEW_IMAGE_NAME" + + echo "Starting new test container" + docker run -d \ + --name $NEW_CONTAINER_IMAGE \ + --health-cmd="wget -qO- http://localhost:$TEST_PORT/health || exit 1" \ + --health-interval=5s \ + --health-retries=5 \ + --health-timeout=2s \ + --health-start-period=5s \ + -p $TEST_PORT:80 \ + "$NEW_CONTAINER_IMAGE" + + echo "Waiting for healthcheck..." + for i in {1..15}; do + STATUS=$(docker inspect --format='{{.State.Health.Status}}' $NEW_CONTAINER_IMAGE) + [ "$STATUS" = "healthy" ] && break + [ "$STATUS" = "unhealthy" ] && break + sleep 2 + done + + docker stop $NEW_CONTAINER_IMAGE + docker image rm $NEW_CONTAINER_IMAGE + + if [ "$STATUS" = "healthy" ]; then + echo "βœ… Container is healthy - starting deployment" + docker stop $OLD_CONTAINER_NAME + docker run -d \ + --name $NEW_CONTAINER_IMAGE \ + --health-cmd="wget -qO- http://localhost:$PORT/health || exit 1" \ + --health-interval=5s \ + --health-retries=5 \ + --health-timeout=2s \ + --health-start-period=5s \ + -p $PORT:80 \ + "$NEW_CONTAINER_IMAGE" + sleep 10 + + DEPLOYMENT_STATUS=$(docker inspect --format='{{.State.Health.Status}}' $NEW_CONTAINER_IMAGE) + + if [ "$DEPLOYMENT_STATUS" != "healthy" ]; then + echo "❌ Deployment failed β€” rolling back" + docker logs $NEW_CONTAINER_IMAGE + docker rm -f $NEW_CONTAINER_IMAGE + + if [ -n "$OLD_CONTAINER_NAME" ]; then + docker run -d \ + --name $OLD_CONTAINER_NAME \ + --health-cmd="wget -qO- http://localhost:$PORT/health || exit 1" \ + --health-interval=5s \ + --health-retries=5 \ + --health-timeout=2s \ + --health-start-period=5s \ + -p $PORT:80 \ + "$OLD_CONTAINER_NAME" + echo "❌ Deployment failed β€” βœ… rollback successfull" + fi + exit 1 + fi + + docker image rm $OLD_CONTAINER_NAME + echo "βœ… Deployment successfull" + + else + echo "❌ Deployment failed β€” ❀️ healthcheck failed on the test container" + fi + EOF \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f8472ac --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM nginx:1.29-alpine + +# Remove default config +RUN rm /etc/nginx/conf.d/default.conf + +# Copy nginx main config +COPY docker/nginx.conf /etc/nginx/nginx.conf + +# Copy virtual host template +COPY docker/default.conf.template /etc/nginx/templates/default.conf.template + +# Copy static website +COPY . /usr/share/nginx/html + +# Expose HTTP port +EXPOSE 80 + +# Nginx supports envsubst natively in /etc/nginx/templates +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..f5bf6eb --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ +# πŸš€ Showcase Website – CI/CD Docker Template + +This repository is a **production-ready template** for deploying a static website using: +- Plain **HTML / CSS / JavaScript** +- **Docker + Nginx** +- **Gitea Actions** for CI/CD +- Automated **build numbering** +- Safe **deployment with healthchecks & rollback** + +It is designed to be **forked and reused** as a starting point for real-world deployments. + +## πŸ“¦ What’s included +- βœ… Simple showcase website +- βœ… Configurable Nginx container +- βœ… Dockerfile ready for production +- βœ… CI pipeline (build + tag + push) +- βœ… CD pipeline (deploy + healthcheck) +- βœ… Automatic rollback on failed deployment + +## πŸ“ Repository structure +``` +. +β”œβ”€β”€ .gitea/ +β”‚ └── workflows/ +β”‚ └── ci.yml +β”œβ”€β”€ assets/ +β”œβ”€β”€ docker/ +β”‚ β”œβ”€β”€ default.conf.template +β”‚ └── nginx.conf +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ css/ +β”‚ └── js/ +β”œβ”€β”€ Dockerfile +β”œβ”€β”€ index.html +└── README.md +``` + +## 🐳 Docker image +### Image tags +Each build produces multiple tags: +| Tag | Description | +| ----------- | ----------- | +| `build-N` | Incremental build number with N as the build number (recommended for deployments) | +| `latest` | Pointer to most recent build | + +## πŸ” CI/CD Pipeline Overview + +### CI (Build) +Triggered on push to `main`: +1. Logs into the Docker registry +2. Build Docker image +3. Tag with: `build-N` & `latest` +4. Push image to registry + +### CD (Deploy) +The deploy job: +1. Connects to the server via SSH +2. Logs into the Docker registry +3. Pulls the new `build-N` image +4. Starts the new `build-N` container on a test port `81` +5. Runs healthchecks +6. **If unhealthy** + 1. Stops the new `build-N` container + 2. Exits the CD process (keeping the old container running) +7. **If healthy β†’ stop the new `build-N` container and stop here** + 1. Stops the new `build-N` container + 2. Stops the new old container + 3. Starts the new `build-N` container on the correct port `80` + 4. Remove the docker image of the old container + +This guarantees: +- No broken deployments +- Clear CI failure signal +- Easy rollback + +## ❀️ Healthcheck + +The container exposes: +``` +GET /health +``` + +Expected response: +``` +200 OK +ok +``` + +Used by: +- Docker healthchecks +- Deployment validation + +## πŸ” Required Gitea Secrets & Variables +The following secrets and variables **must be setup** inside the destination repository. + +### Variables +| Variable | Description | +| ------------------- | --------------------------------------------------- | +| `IMAGE_NAME` | Name of the docker image (built and deployed) | +| `REGISTRY_LOCATION` | Location of the image registery | +| `PORT` | Port on which the site will be exposed on | +| `TEST_PORT` | Port on which the test container will be exposed on | + +### Secrets +| Secrets | Description | +| ------------------ | --------------------------------------------------------------------- | +| `REGISTRY_USER` | User used in order to log into the image registery | +| `REGISTRY_TOKEN` | Token (or password) used in order to log into the image registery | +| `PROD_SERVER_HOST` | Location of the production server host (must be reachable from GITEA) | +| `DEPLOY_USER` | User used in order to ssh into the production server | +| `DEPLOY_SSH_KEY` | SSH private key used in order to ssh into the production server | diff --git a/assets/scripts/main.js b/assets/scripts/main.js new file mode 100644 index 0000000..a63d783 --- /dev/null +++ b/assets/scripts/main.js @@ -0,0 +1,14 @@ +document.addEventListener("DOMContentLoaded", () => { + const ctaBtn = document.getElementById("cta-btn"); + const form = document.getElementById("contact-form"); + + ctaBtn.addEventListener("click", () => { + alert("This template is ready to be extended πŸš€"); + }); + + form.addEventListener("submit", (e) => { + e.preventDefault(); + alert("Form submission placeholder."); + form.reset(); + }); +}); diff --git a/assets/styles/style.css b/assets/styles/style.css new file mode 100644 index 0000000..7a95aa5 --- /dev/null +++ b/assets/styles/style.css @@ -0,0 +1,103 @@ +:root { + --primary: #2b2d42; + --accent: #ef233c; + --light: #f8f9fa; + --text: #333; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; + color: var(--text); + line-height: 1.6; + background-color: var(--light); +} + +.container { + max-width: 1100px; + margin: auto; + padding: 2rem; +} + +.navbar { + background: var(--primary); + color: white; +} + +.navbar .container { + display: flex; + justify-content: space-between; + align-items: center; +} + +.navbar a { + color: white; + margin-left: 1rem; + text-decoration: none; +} + +.hero { + background: linear-gradient(135deg, var(--primary), #1a1c2c); + color: white; + text-align: center; + padding: 5rem 2rem; +} + +.hero button { + margin-top: 1.5rem; + padding: 0.75rem 2rem; + border: none; + background: var(--accent); + color: white; + font-size: 1rem; + cursor: pointer; +} + +.features { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 2rem; +} + +.feature { + background: white; + padding: 2rem; + border-radius: 8px; +} + +.about { + background: #edf2f4; +} + +.contact form { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.contact input, +.contact textarea { + padding: 0.75rem; + font-size: 1rem; +} + +.contact button { + align-self: flex-start; + padding: 0.75rem 1.5rem; + background: var(--primary); + color: white; + border: none; + cursor: pointer; +} + +.footer { + text-align: center; + padding: 1.5rem; + background: var(--primary); + color: white; +} diff --git a/docker/default.conf.template b/docker/default.conf.template new file mode 100644 index 0000000..ebddbc4 --- /dev/null +++ b/docker/default.conf.template @@ -0,0 +1,22 @@ +server { + listen ${NGINX_PORT}; + server_name ${NGINX_SERVER_NAME}; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /health { + access_log off; + return 200 "ok\n"; + add_header Content-Type text/plain; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..6de161e --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,26 @@ +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; + keepalive_timeout 65; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..b4a3ee8 --- /dev/null +++ b/index.html @@ -0,0 +1,74 @@ + + + + + + Template Showcase + + + + + +
+
+
+

Ready-to-Deploy Website Template

+

+ A simple, fast, and Docker-ready showcase website. +

+ +
+
+ +
+
+

Simple

+

Plain HTML, CSS, and JS. No framework lock-in.

+
+
+

Fast

+

Optimized for performance and easy deployment.

+
+
+

Deployable

+

Designed to be containerized and shipped anywhere.

+
+
+ +
+
+

About

+

+ This repository serves as a starting point for production-ready + websites with CI/CD, Docker, and server deployment. +

+
+
+ +
+

Contact

+
+ + + + +
+
+
+ + + + + +