Tutorial

Caddy Server Configuration for static website and Gitlab CI/CD Configuration

Pinterest LinkedIn Tumblr
CI/CD Illustration

Introduction

Caddy is a modern web server that is designed to be simple to set up and use while also being powerful and efficient. It aims to provide developers with a hassle-free experience when deploying websites and web applications. It’s particularly well-suited for those who may not be familiar with the complexities of traditional web server configurations.
In simple terms, Imagine you’ve built a fantastic website or web application that you want to share with the world. However, before others can access it on the internet, you need a special kind of software called a web server. A web server is like a traffic cop for the internet – it directs incoming requests from users’ browsers to the appropriate parts of your website so they can see your content.

Benefits:

  1. Automatic HTTPS:
    One of the standout features of Caddy is its automatic HTTPS setup. When you use Caddy to host your website, it automatically generates and configures SSL certificates to enable secure connections. This is crucial for protecting user data and improving your website’s search engine ranking
  2. Automatic Certificate Renewal:
    When you make changes to your Caddy configuration, you don’t have to restart the server. Caddy will automatically reload its configuration, making it easy to iterate and test changes without disrupting your website’s availability.

Prequisites

We recommend that you have the following basic requirements:

  1. Server with public IP address
  2. Root-level access for the server where configuration is needed.
  3. Basic understanding of pipeline
  4. What is a job in CI/CD?

Caddy Installation In Server

    We need to follow the instructions below step by step
  1. Access the server:
    To access the server, we need to add ssh root@ip_address to the command line and enter the server where ip_address is the IP address for the server. Example: 49.123.4.79
    • Install Caddy in server:
      Please follow the series of commands provided below to install Caddy on your server. We’ll also break down the purpose of each command to enhance your understanding.
     
    sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https 
    curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg 
    curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list 
    sudo apt update 
    sudo apt install caddy
    
    
  2. Installing Required Packages:
    To ensure secure package management, we install necessary components using this command
    sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
    

    This equips the system with tools to manage packages safely and transfer them using HTTPS.

  3. Adding Caddy’s Verification Key:
    We fetch a special key to confirm the authenticity of Caddy packages by running
    curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
    

    With this command, we download a special digital key that acts like a seal for Caddy’s packages. We transform it into a format that our system can use to confirm the legitimacy of Caddy’s software

  4. Configuring Caddy’s Repository:
    By executing,
    curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
    

    We’re telling the system where to find Caddy’s collection of software packages. It’s like adding a new store to your list of trusted shopping places.

  5. Refreshing Package Information:
    We keep our package information up to date with:
    sudo apt update
  6. Installing Caddy:
    This command refreshes the package information from repositories, ensuring the latest available package versions are known.

    sudo apt install caddy

  7. Now, we have successfully install the caddy on our server.🎉

    Note: Caddy is extensible with lots of extensible plugins(https://github.com/caddy-plugins), If you plan to use any of these plugins, you should install caddy via xcaddy (our tutorial link https://thedevpost.com/blog/wildcard-cloudflare-token-xcaddy/)

  8. Server Configuration:
    After we have successfully installed caddy on our server. We will have a caddy directory in etc directory. We can navigate to the caddy directory.

    cd /etc/caddy

    In the Caddy directory, we have a Caddyfile where we need to add our server configuration.
    Since, In our case we need to configure caddy for two different environment.

    • Production
    • Non production (develop, qa, staging)

    so, let’s create two different directories, one for production and one for non production.

    
           root@ubuntu-2gb-fsn1-1:/etc/caddy# ls
            Caddyfile non_production  production 
           root@ubuntu-2gb-fsn1-1:/etc/caddy# 
    

    Now, let’s update Caddyfile to compile configuration of server from both directory i.e production, non_production.
    We can open the file by following one of these commands.

    nano Caddyfile

    Or,

    vim Caddyfile

    As a next step, we are updating content in Caddyfile.

    
        import /etc/caddy/non_production/*.caddy
        import /etc/caddy/production/*.caddy	
      

    so, if we add any caddy file within these two directories, those configurations will be automatically added into Caddyfile. You can reload caddy with caddy reload -f /etc/caddy/Caddyfile to see your new config in action..

Non Production Caddy Server Configuration:

Now, Let’s move by adding a configuration caddy file for non production. For instance, we will create file called contractTemplate.caddy
In the code block below, It set up how Caddy should handle incoming requests.


 *.contract-template.truemark.com.np {
	# Set this path to your site's directory.
	root * /var/www/html/non_production/contract_template/{labels.4}
	encode gzip
	try_files {path} /index.html

	# Enable the static file server.
	file_server

	#logger configuration
	log {
		output file /var/log/caddy/access.log
		format json
	}

       tls {
               dns cloudflare w**************s

       }
}
 

Let’s breakdown how configuration is working.

root * /var/www/html/non_production/contract_template/{labels.4}

In the domain *.contract-template.truemark.com.np, when counting labels from right to left:

labels.0 refers to the rightmost label, which is np.
labels.1 refers to the label immediately to the left, which is com.
labels.2 is truemark.
labels.3 is contract-template.
labels.4 is *.
So, labels.0 is the rightmost label (TLD), and labels.4 is the leftmost label, which is the wildcard *.
So, if you have a request to https://feature-ci-cd.contract-template.truemark.com.np, where feature-ci-cd is a dynamically generated value, {labels.4} will represent that dynamically generated feature-ci-cd, and the path will be constructed accordingly based on the value of the subdomain.

encode gzip: This directive indicates that Caddy should enable Gzip compression for files to optimize content delivery.

try_files {path} / index.html : Tells Caddy to try serving the requested file, and if not found then serve index.html file.

file_server: This directive enables Caddy’s static file server to serve files from the specified root directory.

log { ... }: This block configures logging. It specifies that access logs should be written to /var/log/caddy/access.log in JSON format.

					
 tls {
     dns cloudflare w**************s
 }

Instead of using tls example@gmail.com directly in our configuration, we are using cloudflare service. Caddy communicates with Let’s Encrypt’s production server to obtain SSL/TLS certificates. Let’s Encrypt has rate limits on the number of certificate issuances you can make in a given time frame, and these limits are designed to prevent abuse and ensure fair usage.
By using the tls directive with email address and reaching the rate limits, we might run into issues where we cannot obtain new certificates until those limits reset. This can potentially impact site’s availability if we need to frequently obtain new certificates.In other hand, when we use service like cloudflare, it’s help to avoid these issues.

Note: To create DNS token you can follow along documentation.

Production Caddy Server Configuration:

Now, Let’s create a similar file in the production director contractTemplate.caddy . The server configuration for production will be similar to non production but there are few differences which we will discuss about it.

 
contract-template.com.np { 
	redir http://www.contract-template.com.np{uri} 
}
www.contract-template.truemark.com.np{
	# Set this path to your site's directory.
	root * /var/www/html/non_production/contract_template/main
	encode gzip
	try_files {path} /index.html

	# Enable the static file server.
	file_server

	tls example@gmail.com

 }

Below block of the configuration deals with the non-www version of the domain contract-template.com.np and redirects any HTTP requests to the www subdomain:


contract-template.com.np { 
	redir http://www.contract-template.com.np{uri} 
}

redir http://www.contract-template.com.np{uri}: This line redirects any incoming HTTP requests for contract-template.com.np to the www subdomain while keeping the requested URI intact.

tls example@gmail.com 

Enables TLS (SSL) encryption for the connection and specifies the email address used to request a certificate. This means the certificate for www.contract-template.com.np will be obtained and managed automatically by Caddy.

This way, we have successfully configured caddy. If you want to understand more detail on the structure of the configuration then you can go through official documentation.

Now, let’s create different directories for production and non production.

cd /var/www/html/

The directories structure will be as below


var/
|-- www/
|   |-- html/
|       |-- production
|       |-- non_production

It’s advisable to utilize a mounted volume for storing all your project builds. To streamline this process further, consider creating a symbolic link (symlink) that connects to the mounted volume.


 ln -s /mnt/non_production /var/www/html/non_production
 ln -s /mnt/non_production /home/deploy/non_production 

It is necessary as we will update the build files in deploy user through gitlab CI and we are serving from /var/www/html/non_production

It will be similar approach for production.


 ln -s /mnt/production /var/www/html/production
 ln -s /mnt/production /home/deploy/production

Although, we could also add build files directly in /home/deploy/non_production and /home/deploy/production


 ln -s /home/deploy/non_production /var/www/html/non_production
 ln -s /home/deploy/production /var/www/html/production

Gitlab CI Configuration

We need to follow the below instructions step by step.

  1. Create .gitlab-ci.yml within your root directory of the project.
  2. We will create three different stages and introduce variables.
  3. 
           variables:
             APP_NAME: contract_template
             BASE_DOCKER_IMAGE: node:18.18.0
             BRANCH_BASE_URL: contract-template.truemark.com.np
             PRODUCTION_URL: https://www.contract-template.truemark.com.np
             PRODUCTION_ENV_FILE_NAME: .env
             BUILD_DIRECTORY: build
    
            stages:
              - branch_deploy
              - production_deploy
              - after_deploy
          

    Along the tutorial, we will build two different environment for branch based deployment, production deployment and post deployed url over merge request.

  4. Branch based Configuration: In branch_deploy job, we will updated build files to server and before we could upload build files, we need to maintain ssh connection between server and gitlab container.
      
      branch_based_job:
        extends: .branch_based_job
        image: $BASE_DOCKER_IMAGE
        stage: branch_deploy
        environment:
          name: preview/$CI_COMMIT_REF_NAME
          url: https://$CI_COMMIT_REF_SLUG.$BRANCH_BASE_URL
        before_script:
          - apt update && apt upgrade -y
          # check whether package openssh is available or not and install it if not available
          - 'command -v ssh-agent >/dev/null || ( apt install -y openssh )'
          # Command is used to setup a SSH connection
          - eval $(ssh-agent -s)
          # add ssh private to ssh connection during CI
          - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
          # create director in server
          - mkdir -p ~/.ssh
          # change the permission of .ssh i.e read, write, delete
          - chmod 700 ~/.ssh
          # use to fetch public key of host server to known_hosts
          - ssh-keyscan $SERVER_IP >> ~/.ssh/known_hosts
          # changes permission of known_hosts file to read and write.
          - chmod 644 ~/.ssh/known_hosts
          # update package
          - apt install -y rsync
          - npm install --legacy-peer-deps

      As a image for branch_deploy, we have used $BASE_DOCKER_IMAGE as we have introduces in variables.
      The before_script contains a series of commands that are executed to maintain ssh connection between container and server.

    • It will check whether package open ssh is available or not and install it if not available
      'command -v ssh-agent >/dev/null || ( apt install -y openssh )'
      
    • It will adds the SSH private key provided as an environment variable ($SSH_PRIVATE_KEY) to the SSH agent.
       
               eval $(ssh-agent -s)
               echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
              
    • To creates and configures the .ssh directory for SSH-related files and permissions.
      
      - mkdir -p ~/.ssh
      - chmod 700 ~/.ssh
      
    • we have added below command to fetch the public key of the host server to known_hosts. The environment variable $SERVER_IP in gitlab CI variable section.
      ssh-keyscan $SERVER_IP >> ~/.ssh/known_hosts
    • we have to add below command for read and write permission to known hosts.
      chmod 644 ~/.ssh/known_hosts
    • we have to upgrade packages, install the rsync package, install node modules and build react application
      
      - apt update && apt upgrade -y && apt install -y rsync
      - npm install --legacy-peer-deps
      - npm run build
    • Note: You can learn about how to configure custom CI/CD variables over this article.

    • Above configuration should have maintain ssh connection between container and server. In below block we will update build files to server.
      
           script:
             # create directory by using branch name
             - ssh $SSH_USER@$SERVER_IP mkdir -p /home/deploy/non_production/$APP_NAME/$CI_COMMIT_REF_SLUG
             - cat $envfile_non_production > $PRODUCTION_ENV_FILE_NAME
             - npm run build
             # publish static assets or files to a remote server and ensure only changed files are transferred
             - rsync -atv --delete --progress ./$BUILD_DIRECTORY/ root@$SERVER_IP:/home/deploy/non_production/$APP_NAME/$CI_COMMIT_REF_SLUG
        
      • we will access the host server and create a directory using slug name (for ex: feature-ci-cd). Environment variables such as $SSH_USER and $SERVER_IP are added over gitlab CI remote repository while $CI_COMMIT_REF_SLUG pre-defined variable.
        
        - ssh $SSH_USER@$SERVER_IP mkdir -p /home/deploy/non_production/$APP_NAME/$CI_COMMIT_REF_SLUG
      • It publishes static assets or files to a remote server and ensures only changed files are transferred.
        
        - rsync -atv --delete --progress ./build/ $SSH_USER@$SERVER_IP:/home/deploy/non_production/$APP_NAME/$CI_COMMIT_REF_SLUG
      • After publishing the build file and completing the job, it will save the build folder in $BUILD_DIRECTORY (i.e build) directory within the CI server. So, that we can look over it.
        
          artifacts:
            paths:
             - $BUILD_DIRECTORY
        
      • It will created environment for deployed branch.
        
          environment:
            name: preview/$CI_COMMIT_REF_NAME
            url: https://$CI_COMMIT_REF_SLUG.$BRANCH_BASE_URL
         
      • We need to add rule so that job only run when merge request is created. Since we will required same rule for after_deploy job too. so, we will create base rule.
        
           .branch_based_job:
             rules:
              - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
                when: never
              - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
                when: on_success
          
      • Gitab CI/CD Configuration For Production
        The configuration for gitlab CI will be similar to branch based deployment but we will have few slight adjustments to above configuration.
        • Since, we will be only hosting from a single directory called main (note: we have added main in production configuration).
          
          - ssh $SSH_USER@$SERVER_IP mkdir -p /home/deploy/production/$APP_NAME/main
          
        • Since we need to run job in main branch only. so, we will add rule accordingly
          
            rules:
              - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
                when: on_success
           
        • Now, we will require new changes to the main branch. We can do that using rsync
          - rsync -atv --delete --progress ./build/ $SSH_USER@$SERVER_IP:/home/deploy/production/$APP_NAME/main
        • we will also add environment for production
          
          environment:
            name: production
            url: $PRODUCTION_URL
          
        • If we follow as above, we will get job as below.

        
        production_deploy:
          image: $BASE_DOCKER_IMAGE
          stage: production_deploy
          environment:
            name: production
            url: $PRODUCTION_URL
          rules:
            - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
              when: on_success
        
          before_script:
            - apt update && apt upgrade -y
            # check whether package openssh is available or not and install it if not available
            - 'command -v ssh-agent >/dev/null || ( apt install -y openssh )'
            # Command is used to setup a SSH connection
            - eval $(ssh-agent -s)
            # add ssh private to ssh connection during CI
            - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
            # create directory in server
            - mkdir -p ~/.ssh
            # change the permission of .ssh i.e read, write, delete
            - chmod 700 ~/.ssh
            # use to fetch public key of host server to known_hosts
            - ssh-keyscan $SERVER_IP >> ~/.ssh/known_hosts
            # changes permission of known_hosts file to read and write.
            - chmod 644 ~/.ssh/known_hosts
            # update package
            - apt install -y rsync
            - npm install --legacy-peer-deps
        
            script:
              - ssh $SSH_USER@$SERVER_IP mkdir -p /home/deploy/production/$APP_NAME/main
              - cat $envfile_production > $PRODUCTION_ENV_FILE_NAME
              - npm run build
              # publish static assets or files to a remote server and ensure only changed files are transferred
              - rsync -atv --delete --progress ./$BUILD_DIRECTORY/ root@$SERVER_IP:/home/deploy/production/$APP_NAME/main
        
          artifacts:
            paths:
             - $BUILD_DIRECTORY
          
      • Post-Deployment Notifications
        Let’s create another job which will comment in the merge request to access the deployed URL.
        
        post_message_to_mr:
          extends: .branch_based_job
          stage: after_deploy
          image: $BASE_DOCKER_IMAGE
          script:
            - export GITLAB_TOKEN=$TRUEMARK_GITLAB_KEY
            - apt update
            # install package curl
            - apt install -y curl
            - 'curl --location --request POST "https://gitlab.com/api/v4/projects/$CI_MERGE_REQUEST_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes" --header "PRIVATE-TOKEN: $GITLAB_TOKEN" --header "Content-Type: application/json" --data-raw "{ \"body\": \"## :tada:   Latest changes from this :deciduous_tree: branch are now :package: deployed \n * :bulb: Please prefer to post snapshots of UI issues from this latest deployment while you review this PR (br tag) \n * :small_red_triangle: Preview URL might not work/exist once this Merge/Pull request is merged (hr tag) :tv:   [Preview Deployed Application/website](https://$CI_COMMIT_REF_SLUG.$BRANCH_BASE_URL).\" }"'
        
        • This job will run in same image as above branch based job and production deployment job.
          
            image: $BASE_DOCKER_IMAGE
           
        • we also extends same common rule used in branch based deployment here. so, that job will run only merge request is created
        • There, we will assign $GITLAB_TOKEN to $TRUEMARK_GITLAB_KEY (It is added over remote repository).
          export GITLAB_TOKEN=$TRUEMARK_GITLAB_KEY
        • It will update the package on the server and install curl package.
          
          - apt-get update
          # install package curl
          - apt-get install -y curl
        • It will spend the post request and add comment in Merge Request with deployed URL.
          - 'curl --location --request POST "https://gitlab.com/api/v4/projects/$CI_MERGE_REQUEST_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes" --header "PRIVATE-TOKEN: $GITLAB_TOKEN" --header "Content-Type: application/json" --data-raw "{ \"body\": \"Hi Look at this awesome message with a link to a [deployed environment](https://$SLUG.contract-template.truemark.com.np).\" }"'
          

      After following along above instruction, we will get following configuration.

      
       # GITLAB CI/CD Variables PREREQUISITES:
      # ........................
      # SSH_USER, SERVER_IP, SSH_PRIVATE_KEY
      # CI_DEFAULT_BRANCH (optional in case you want to change production deploy in different branch)
      # envfile_production, envfile_non_production (type: file)
      # SKIP_TEST i.e true or false
      # TRUEMARK_GITLAB_KEY (prefer inherit from project group and set by lead developer)
      # ..........................
      
      # UPDATE VARIABLE_IN_CONFIGURATION
      # ..................
      # APP_NAME: Prefer snake case for project name
      # BASE_DOCKER_IMAGE: prefer node image which uses same node version used during development
      # BRANCH_BASE_URL: preview domain for test
      # PRODUCTION_URL: production domain
      # PRODUCTION_ENV_FILE_NAME: environment variable (.env/.env.production) based on web application
      # BUILD_DIRECTORY: parent directory of build file
      
      variables:
        APP_NAME: contract_template
        BASE_DOCKER_IMAGE: node:18.18.0
        BRANCH_BASE_URL: contract-template.truemark.com.np
        PRODUCTION_URL: https://www.contract-template.truemark.com.np
        PRODUCTION_ENV_FILE_NAME: .env
        BUILD_DIRECTORY: build
      
      stages:
        - branch_deploy
        - production_deploy
        - after_deploy
      
      .branch_based_job:
        rules:
          - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
            when: never
          - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
            when: on_success
      
      build_branch_deploy:
        extends: .branch_based_job
        image: $BASE_DOCKER_IMAGE
        stage: branch_deploy
        environment:
          name: preview/$CI_COMMIT_REF_NAME
          url: https://$CI_COMMIT_REF_SLUG.$BRANCH_BASE_URL
        before_script:
          - apt update && apt upgrade -y
          # check whether package openssh is available or not and install it if not available
          - 'command -v ssh-agent >/dev/null || ( apt install -y openssh )'
          # Command is used to setup a SSH connection
          - eval $(ssh-agent -s)
          # add ssh private to ssh connection during CI
          - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
          # create director in server
          - mkdir -p ~/.ssh
          # change the permission of .ssh i.e read, write, delete
          - chmod 700 ~/.ssh
          # use to fetch public key of host server to known_hosts
          - ssh-keyscan $SERVER_IP >> ~/.ssh/known_hosts
          # changes permission of known_hosts file to read and write.
          - chmod 644 ~/.ssh/known_hosts
          # update package
          - apt install -y rsync
          - npm install --legacy-peer-deps
      
        script:
          # create directory by using branch name
          - ssh $SSH_USER@$SERVER_IP mkdir -p /home/deploy/non_production/$APP_NAME/$CI_COMMIT_REF_SLUG
          - cat $envfile_non_production > $PRODUCTION_ENV_FILE_NAME
          - npm run build
          # publish static assets or files to a remote server and ensure only changed files are transferred
          - rsync -atv --delete --progress ./$BUILD_DIRECTORY/ root@$SERVER_IP:/home/deploy/non_production/$APP_NAME/$CI_COMMIT_REF_SLUG
      
        artifacts:
          paths:
            - $BUILD_DIRECTORY
      
      production_deploy:
        image: $BASE_DOCKER_IMAGE
        stage: production_deploy
        environment:
          name: production
          url: $PRODUCTION_URL
        rules:
          - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
            when: on_success
      
        before_script:
          - apt update && apt upgrade -y
          # check whether package openssh is available or not and install it if not available
          - 'command -v ssh-agent >/dev/null || ( apt install -y openssh )'
          # Command is used to setup a SSH connection
          - eval $(ssh-agent -s)
          # add ssh private to ssh connection during CI
          - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
          # create directory in server
          - mkdir -p ~/.ssh
          # change the permission of .ssh i.e read, write, delete
          - chmod 700 ~/.ssh
          # use to fetch public key of host server to known_hosts
          - ssh-keyscan $SERVER_IP >> ~/.ssh/known_hosts
          # changes permission of known_hosts file to read and write.
          - chmod 644 ~/.ssh/known_hosts
          # update package
          - apt install -y rsync
          - npm install --legacy-peer-deps
      
        script:
          - ssh $SSH_USER@$SERVER_IP mkdir -p /home/deploy/production/$APP_NAME/main
          - cat $envfile_production > $PRODUCTION_ENV_FILE_NAME
          - npm run build
          # publish static assets or files to a remote server and ensure only changed files are transferred
          - rsync -atv --delete --progress ./$BUILD_DIRECTORY/ root@$SERVER_IP:/home/deploy/production/$APP_NAME/main
      
        artifacts:
          paths:
            - $BUILD_DIRECTORY
      
      post_message_to_mr:
        extends: .branch_based_job
        stage: after_deploy
        image: $BASE_DOCKER_IMAGE
      
        script:
          - export GITLAB_TOKEN=$TRUEMARK_GITLAB_KEY
          - apt update
          # install package curl
          - apt install -y curl
          - 'curl --location --request POST "https://gitlab.com/api/v4/projects/$CI_MERGE_REQUEST_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes" --header "PRIVATE-TOKEN: $GITLAB_TOKEN" --header "Content-Type: application/json" --data-raw "{ \"body\": \"## :tada:   Latest changes from this :deciduous_tree: branch are now :package: deployed \n * :bulb: Please prefer to post snapshots of UI issues from this latest deployment while you review this PR (br tag) \n * :small_red_triangle: Preview URL might not work/exist once this Merge/Pull request is merged (hr tag) :tv:   [Preview Deployed Application/website](https://$CI_COMMIT_REF_SLUG.$BRANCH_BASE_URL).\" }"'
      

      Conclusion

      In summary, our journey covered the installation of the Caddy web server on our server and configuring it for both production and non-production deployments using Caddyfiles.
      Additionally, we successfully set up three distinct GitLab jobs for branch-based deployments, production releases, and post-deployment notifications.

      Thank you for joining us on this learning adventure!

      Reference

      Write A Comment