initial commit
This commit is contained in:
169
.gitea/workflows/ci.yml
Normal file
169
.gitea/workflows/ci.yml
Normal file
@@ -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
|
||||
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@@ -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;"]
|
||||
111
README.md
Normal file
111
README.md
Normal file
@@ -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 |
|
||||
14
assets/scripts/main.js
Normal file
14
assets/scripts/main.js
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
103
assets/styles/style.css
Normal file
103
assets/styles/style.css
Normal file
@@ -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;
|
||||
}
|
||||
22
docker/default.conf.template
Normal file
22
docker/default.conf.template
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
26
docker/nginx.conf
Normal file
26
docker/nginx.conf
Normal file
@@ -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;
|
||||
}
|
||||
74
index.html
Normal file
74
index.html
Normal file
@@ -0,0 +1,74 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Template Showcase</title>
|
||||
<link rel="stylesheet" href="assets/styles/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="navbar">
|
||||
<div class="container">
|
||||
<h1 class="logo">MyTemplate</h1>
|
||||
<nav>
|
||||
<a href="#features">Features</a>
|
||||
<a href="#about">About</a>
|
||||
<a href="#contact">Contact</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<h2>Ready-to-Deploy Website Template</h2>
|
||||
<p>
|
||||
A simple, fast, and Docker-ready showcase website.
|
||||
</p>
|
||||
<button id="cta-btn">Get Started</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="features" class="features container">
|
||||
<div class="feature">
|
||||
<h3>Simple</h3>
|
||||
<p>Plain HTML, CSS, and JS. No framework lock-in.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>Fast</h3>
|
||||
<p>Optimized for performance and easy deployment.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>Deployable</h3>
|
||||
<p>Designed to be containerized and shipped anywhere.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="about" class="about">
|
||||
<div class="container">
|
||||
<h2>About</h2>
|
||||
<p>
|
||||
This repository serves as a starting point for production-ready
|
||||
websites with CI/CD, Docker, and server deployment.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="contact" class="contact container">
|
||||
<h2>Contact</h2>
|
||||
<form id="contact-form">
|
||||
<input type="text" placeholder="Your name" required />
|
||||
<input type="email" placeholder="Your email" required />
|
||||
<textarea placeholder="Message"></textarea>
|
||||
<button type="submit">Send</button>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<p>© 2026 MyTemplate. All rights reserved.</p>
|
||||
</footer>
|
||||
|
||||
<script src="assets/scripts/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user