diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 829ee42..ba4c409 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -3,12 +3,13 @@ name: CI on: push: branches: - - main + - master pull_request: env: IMAGE_NAME: ${{ vars.IMAGE_NAME }} REGISTRY_LOCATION: ${{ vars.REGISTRY_LOCATION }} + REGISTRY_ORGANIZATION: ${{ vars.REGISTRY_ORGANIZATION }} PORT: ${{ vars.PORT }} TEST_PORT: ${{ vars.TEST_PORT }} REGISTRY_USER: ${{ secrets.REGISTRY_USER }} @@ -16,6 +17,7 @@ env: DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} PROD_SERVER_HOST: ${{ secrets.PROD_SERVER_HOST }} DEPLOY_USER: ${{ secrets.DEPLOY_USER }} + BUILD_NUMBER: build-${{ gitea.run_number }} jobs: lint: @@ -31,10 +33,6 @@ jobs: 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 @@ -44,14 +42,10 @@ jobs: - 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:$BUILD_NUMBER \ -t $IMAGE_NAME:latest \ . @@ -65,13 +59,13 @@ jobs: - 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 + docker tag $IMAGE_NAME:latest $REGISTRY_LOCATION/$REGISTRY_ORGANIZATION/$IMAGE_NAME:latest + docker tag $IMAGE_NAME:$BUILD_NUMBER $REGISTRY_LOCATION/$REGISTRY_ORGANIZATION/$IMAGE_NAME:$BUILD_NUMBER - name: ๐Ÿš€ Push images run: | - docker push $REGISTRY_LOCATION/$IMAGE_NAME:latest - docker push $REGISTRY_LOCATION/$IMAGE_NAME:build-$RUN_NUMBER + docker push $REGISTRY_LOCATION/$REGISTRY_ORGANIZATION/$IMAGE_NAME:latest + docker push $REGISTRY_LOCATION/$REGISTRY_ORGANIZATION/$IMAGE_NAME:$BUILD_NUMBER deploy: name: ๐Ÿš€ Deploy image to prod @@ -86,26 +80,56 @@ jobs: chmod 600 ~/.ssh/id_rsa ssh-keyscan -H $PROD_SERVER_HOST >> ~/.ssh/known_hosts - - name: ๐Ÿ”‘ Login to registry + - name: ๐Ÿ—๏ธ Login to registry run: | echo "$REGISTRY_TOKEN" | docker login $REGISTRY_LOCATION \ -u "$REGISTRY_USER" --password-stdin - - name: ๐Ÿšข Deploy container + - name: ๐Ÿ”Œ SSH into the prod server 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" + sleep 5 - echo "Pulling image $NEW_IMAGE_NAME" + - name: ๐Ÿš€ Deploy container to prod + run: | + set -e + NEW_IMAGE_NAME="$REGISTRY_LOCATION/$REGISTRY_ORGANIZATION/$IMAGE_NAME:$BUILD_NUMBER" + TEST_CONTAINER_NAME=test-$IMAGE_NAME + OLD_IMAGE_NAME=$(docker inspect $IMAGE_NAME --format='{{.Config.Image}}') + NEW_CONTAINER_IMAGE="$IMAGE_NAME:$BUILD_NUMBER" + + echo "๐Ÿ—๏ธ Building functions" + + # checks the health of the given container name $1 + # returns "healthy" or "unhealthy" + is_container_healthy() { + for i in {1..15}; do + RETURN=$(docker inspect --format='{{.State.Health.Status}}' $1) + [ "$RETURN" = "healthy" ] && break + [ "$RETURN" = "unhealthy" ] && break + sleep 2 + done + echo "$RETURN" + } + + + echo "๐ŸŽฃ Pulling image $NEW_IMAGE_NAME" docker pull "$NEW_IMAGE_NAME" + + echo "๐Ÿงช Test the new container health" + + # if old test container is running then stop and remove it + if docker ps -a --format '{{.Names}}' | grep "^$TEST_CONTAINER_NAME$"; then + echo "Container ${TEST_CONTAINER_NAME} is running. Stopping and removing..." + docker stop $TEST_CONTAINER_NAME + docker rm $TEST_CONTAINER_NAME + fi + echo "Starting new test container" docker run -d \ - --name $NEW_CONTAINER_IMAGE \ - --health-cmd="wget -qO- http://localhost:$TEST_PORT/health || exit 1" \ + --name $TEST_CONTAINER_NAME \ + --health-cmd="curl -f http://$PROD_SERVER_HOST:$TEST_PORT/health || exit 1" \ --health-interval=5s \ --health-retries=5 \ --health-timeout=2s \ @@ -113,57 +137,73 @@ jobs: -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 + TEST_CONTAINER_HEALTHCHECK=$(is_container_healthy $TEST_CONTAINER_NAME) - 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 stop $TEST_CONTAINER_NAME + docker rm $TEST_CONTAINER_NAME + + echo "๐Ÿšš Deploy the new image if healthy, otherwise rollback" + if [ "$TEST_CONTAINER_HEALTHCHECK" = "healthy" ]; then + echo "โœ… Test Container is healthy - starting deployment" + + # Stop previous prod running container if found + if [ -n "$OLD_IMAGE_NAME" ]; then + echo "Stopping previous prod running container" + docker stop $IMAGE_NAME + docker rm $IMAGE_NAME + fi + docker run -d \ - --name $NEW_CONTAINER_IMAGE \ - --health-cmd="wget -qO- http://localhost:$PORT/health || exit 1" \ + --name $IMAGE_NAME \ + --health-cmd="curl -f http://$PROD_SERVER_HOST:$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) + PROD_CONTAINER_HEALTHCHECK=$(is_container_healthy $IMAGE_NAME) - if [ "$DEPLOYMENT_STATUS" != "healthy" ]; then + if [ "$PROD_CONTAINER_HEALTHCHECK" != "healthy" ]; then echo "โŒ Deployment failed โ€” rolling back" - docker logs $NEW_CONTAINER_IMAGE - docker rm -f $NEW_CONTAINER_IMAGE + docker logs $IMAGE_NAME + docker stop $IMAGE_NAME + docker rm -f $IMAGE_NAME + docker image rm $NEW_CONTAINER_IMAGE - if [ -n "$OLD_CONTAINER_NAME" ]; then + if [ -n "$OLD_IMAGE_NAME" ]; then docker run -d \ - --name $OLD_CONTAINER_NAME \ - --health-cmd="wget -qO- http://localhost:$PORT/health || exit 1" \ + --name $IMAGE_NAME \ + --health-cmd="curl -f http://$PROD_SERVER_HOST:$PORT/health || exit 1" \ --health-interval=5s \ --health-retries=5 \ --health-timeout=2s \ --health-start-period=5s \ -p $PORT:80 \ - "$OLD_CONTAINER_NAME" + "$OLD_IMAGE_NAME" + + if [["$(is_container_healthy $IMAGE_NAME)" == "healthy" ]]; then echo "โŒ Deployment failed โ€” โœ… rollback successfull" + else + echo "โŒ Deployment failed โ€” โŒ rollback failed" + fi + exit 1 fi - exit 1 + echo "โŒ Deployment failed โ€” โŒ rollback not possible (no previous version was running)" + exit 1 fi - docker image rm $OLD_CONTAINER_NAME - echo "โœ… Deployment successfull" - + if [ -n "$OLD_IMAGE_NAME" ]; then + docker image rm $OLD_IMAGE_NAME + fi + + echo "โœ… Deployment successfull ($IMAGE_NAME)" + exit 0 else + + docker image rm $NEW_CONTAINER_IMAGE echo "โŒ Deployment failed โ€” โค๏ธ healthcheck failed on the test container" + exit 1 fi - EOF \ No newline at end of file + \ No newline at end of file diff --git a/README.md b/README.md index f5bf6eb..f493f49 100644 --- a/README.md +++ b/README.md @@ -24,12 +24,11 @@ It is designed to be **forked and reused** as a starting point for real-world de โ”‚ โ””โ”€โ”€ workflows/ โ”‚ โ””โ”€โ”€ ci.yml โ”œโ”€โ”€ assets/ +โ”‚ โ”œโ”€โ”€ scripts/ +โ”‚ โ””โ”€โ”€ styles/ โ”œโ”€โ”€ docker/ โ”‚ โ”œโ”€โ”€ default.conf.template โ”‚ โ””โ”€โ”€ nginx.conf -โ”œโ”€โ”€ src/ -โ”‚ โ”œโ”€โ”€ css/ -โ”‚ โ””โ”€โ”€ js/ โ”œโ”€โ”€ Dockerfile โ”œโ”€โ”€ index.html โ””โ”€โ”€ README.md @@ -94,12 +93,13 @@ Used by: 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 | +| Variable | Description | +| ----------------------- | --------------------------------------------------- | +| `IMAGE_NAME` | Name of the docker image (built and deployed) | +| `REGISTRY_LOCATION` | Location of the image registery | +| `REGISTRY_ORGANIZATION` | Name of the organisation in 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 | diff --git a/docker/default.conf.template b/docker/default.conf.template index ebddbc4..b695435 100644 --- a/docker/default.conf.template +++ b/docker/default.conf.template @@ -1,6 +1,6 @@ server { - listen ${NGINX_PORT}; - server_name ${NGINX_SERVER_NAME}; + listen 80; + server_name _; root /usr/share/nginx/html; index index.html;