I have been using caddy as a reverse proxy. See more at Caddy.

Step 1 - Get a Ubuntu server

You can buy a Ubuntu server from any provider you like. Don't forget to follow the best practices here.

Step 2 - Install GitLab

Installing GitLab is super easy. Reference here

sudo apt-get update
sudo apt-get install -y curl openssh-server ca-certificates tzdata perl
curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ee/script.deb.sh | sudo bash

Since we are going to deploy GitLab behind reverse proxy, let's use an internal address first. DO NOT use HTTPS here!

sudo EXTERNAL_URL="http://gitlab.local" apt-get install gitlab-ee

Step 3 - Configure HTTP reverse proxy

For caddy server tips, you can reference the blog mentioned here.

Now edit our Caddyfile to respect the reverse proxy settings:

gitlab.aiursoft.cn { # This is our production domain!
        encode gzip
        reverse_proxy http://gitlab.local { # This is our internal domain!
                header_up Host gitlab.aiursoft.cn # This is our production domain!
        }
}

Don't forget to copy the reverse proxy server IP address! For example: 172.16.1.100.

Step 4 - Configure GitLab to respect our HTTP reverse proxy

Now edit GitLab configuration:

cd /etc/gitlab
sudo vim ./gitlab.rb

Update our public URL:

external_url "https://gitlab.aiursoft.cn/"

Update our reverse proxy settings:

nginx['listen_port'] = 80
nginx['listen_https'] = false
nginx['proxy_protocol'] = false
nginx['http2_enabled'] = true
nginx['real_ip_trusted_addresses'] = ['172.16.1.0/16']
nginx['real_ip_header'] = 'X-Forwarded-For'
nginx['real_ip_recursive'] = 'on'

You can reference here: GitLab Nginx settings

Apply changes:

sudo gitlab-ctl reconfigure

Step 5 - Configure GitLab SSH

At this point we are using a reverse proxy. We have at least two servers: proxy server, gitlab server.

Assuming those two servers are Linux servers, we need to use 22 port to manage those servers.

Now we need to expose another port for git.

Edit the GitLab server to use 2202 as clone port.

sudo vim /etc/gitlab/gitlab.rb

And add these:

gitlab_rails['gitlab_shell_ssh_port'] = 2202

gitlab_sshd['enable'] = true
gitlab_sshd['listen_address'] = '[::]:2202'

Now, the clone address on the web page is correct. But you still can't clone those repos. That is because the reverse proxy server doesn't listen to the 2202 port.

file

Step 6 - Configure Proxy Server SSH Port Forwarding

We need to create a port forwarding from proxy server 2202 to gitlab server 2202.

Now go to the proxy server. Create the file:

sudo touch /etc/systemd/system/proxy-ssh-gitlab.service

Edit the content:

[Unit]
Description=GitLab SSH Proxy Service
After=network.target

[Service]
Type=simple
Restart=on-failure
RestartSec=5s
ExecStart=/usr/bin/ncat --sh-exec "ncat gitlab.local 2202" -l 2202 --keep-open
LimitNOFILE=1048576

[Install]
WantedBy=multi-user.target

Don't forget to enable and start it!

sudo systemctl enable proxy-ssh-gitlab.service
sudo systemctl start proxy-ssh-gitlab.service

Now it's done! Try to clone a repo with SSH!

If your proxy server is also using FRPC to reverse proxy, don't forget to edit /etc/frp/frpc.ini to proxy 2202 port to gitlab or localhost.

Step 7 - Add some runners to your GitLab instance

Reference here: GitLab Document.

The script I used is:

curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - &&\

# dotnet
sudo apt-get install -y gitlab-runner dotnet6 nodejs

# register
sudo gitlab-runner register \
    --url "https://gitlab.aiursoft.cn/" \
    --registration-token "<----your-token--->" \
    --description "Runner" \
    --executor "shell" \
    --tag-list "ubuntu,shared,runner" \
    --run-untagged="true" \
    --locked="false"
		
#	Hack for some UT.	
echo fs.inotify.max_user_instances=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p

sudo gitlab-runner start
sudo gitlab-runner status

Step 8 - Write a project and try to test it with your runner!

Now you can create a new .NET project, and write the .gitlab-ci.yml.

Try to create the project with:

dotnet new console

Add some packages:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="coverlet.collector" Version="3.2.0">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="JunitXml.TestLogger" Version="3.0.124" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
    <PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
    <PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
  </ItemGroup>

  
</Project>

You can write some unit tests. And now git push and wait for GitLab to run your tests!

stages:
  - Building
  - Linting
  - Testing
  - Publishing

before_script:
  - 'export DOTNET_CLI_TELEMETRY_OPTOUT=1'
  - 'export PATH=$PATH:$HOME/.dotnet/tools'
  - 'dotnet tool install JetBrains.ReSharper.GlobalTools --global || echo "JB already installed."'
  - 'dotnet tool install dotnet-reportgenerator-globaltool --global || echo "DRG already installed."'
  - 'echo "Hostname: $(hostname)"'
  - 'dotnet --info'

restore:
  stage: Building
  script:
    - dotnet restore --configfile nuget.config

build:
  stage: Building
  needs: 
    - restore
  script:
    - dotnet build --no-self-contained

lint:
  stage: Linting
  needs: 
    - build
  script:
    - jb inspectcode ./*.sln --output=analyze_output.xml --build
    - grep 'WARNING' analyze_output.xml && cat analyze_output.xml && exit 1 || echo "No warning found"
  artifacts:
    when: always
    expire_in: 1 day
    paths:
      - ./analyze_output.xml

test:
  stage: Testing
  needs: 
    - build
  coverage: '/TOTAL_COVERAGE=(\d+.\d+)/'
  script:
    - dotnet test *.sln --collect:"XPlat Code Coverage" --logger "junit;MethodFormat=Class;FailureBodyFormat=Verbose"
    - reportgenerator -reports:"**/coverage.cobertura.xml" -targetdir:"." -reporttypes:"cobertura"
    - COVERAGE_VALUE=$(grep -oPm 1 'line-rate="\K([0-9.]+)' "./Cobertura.xml")
    - COVERAGE_PERCENTAGE=$(echo "scale=2; $COVERAGE_VALUE * 100" | bc)
    - 'echo "TOTAL_COVERAGE=$COVERAGE_PERCENTAGE%"'
  artifacts:
    when: always
    expire_in: 1 day
    paths:
      - ./**/TestResults.xml
      - ./Cobertura.xml
    reports:
      junit:
        - ./**/TestResults.xml
      coverage_report:
        coverage_format: cobertura
        path: ./Cobertura.xml

publish:
  stage: Publishing
  needs: 
    - lint
    - test
  script:
    - dotnet publish --configuration Release --runtime linux-x64 --no-self-contained *.sln

pack:
  stage: Publishing
  needs: 
    - lint
    - test
  script:
    - dotnet build --configuration Release --no-self-contained *.sln
    - dotnet pack --configuration Release *.sln || echo "Some packaging failed!"
  artifacts:
    expire_in: 1 week
    paths:
      - '**/*.nupkg'