diff --git a/.deploy/docker-compose-template.yml b/.deploy/docker-compose-template.yml new file mode 100644 index 0000000..1f9ee7a --- /dev/null +++ b/.deploy/docker-compose-template.yml @@ -0,0 +1,12 @@ +version: "3.9" +services: + ${APP_NAME}: + image: ghcr.io/${IMAGE_REPO}:${RELEASE_VERSION} + restart: always + network_mode: bridge + ports: + - "80" + environment: + VIRTUAL_HOST: ${HOST_DOMAIN} + LETSENCRYPT_HOST: ${HOST_DOMAIN} + LETSENCRYPT_EMAIL: ${LETSENCRYPT_EMAIL} diff --git a/.deploy/nginx-proxy-compose.yml b/.deploy/nginx-proxy-compose.yml new file mode 100644 index 0000000..eccae8d --- /dev/null +++ b/.deploy/nginx-proxy-compose.yml @@ -0,0 +1,40 @@ +version: '2' + +services: + nginx-proxy: + image: jwilder/nginx-proxy + container_name: nginx-proxy + restart: always + ports: + - "80:80" + - "443:443" + volumes: + - conf:/etc/nginx/conf.d + - vhost:/etc/nginx/vhost.d + - html:/usr/share/nginx/html + - dhparam:/etc/nginx/dhparam + - certs:/etc/nginx/certs:ro + - /var/run/docker.sock:/tmp/docker.sock:ro + network_mode: bridge + + letsencrypt: + image: jrcs/letsencrypt-nginx-proxy-companion:2.0 + container_name: nginx-proxy-le + restart: always + environment: + - DEFAULT_EMAIL=you@example.com + volumes_from: + - nginx-proxy + volumes: + - certs:/etc/nginx/certs:rw + - acme:/etc/acme.sh + - /var/run/docker.sock:/var/run/docker.sock:ro + network_mode: bridge + +volumes: + conf: + vhost: + html: + dhparam: + certs: + acme: diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..e4ea320 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,56 @@ +# ServiceStack mix GitHub Actions +`release.yml` generated from `x mix release-ghr-vanilla`, this template in designed to help with CI deployment to a dedicated server with SSH access. + +## Overview +`release.yml` is designed to work with a ServiceStack app deploying directly to a single server via SSH. A docker image is built and stored on GitHub's `ghcr.io` docker registry when a GitHub Release is created. + +GitHub Actions specified in `release.yml` then copy files remotely via scp and use `docker-compose` to run the app remotely via SSH. + +## Deployment server setup +To get this working, a server needs to be setup with the following: + +- SSH access +- docker +- docker-compose +- ports 443 and 80 for web access of your hosted application + +This can be your own server or any cloud hosted server like Digital Ocean, AWS, Azure etc. + +When setting up your server, you'll want to use a dedicated SSH key for access to be used by GitHub Actions. GitHub Actions will need the *private* SSH key within a GitHub Secret to authenticate. This can be done via ssh-keygen and copying the public key to the authorized clients on the server. + +To let your server handle multiple ServiceStack applications and automate the generation and management of TLS certificates, an additional docker-compose file is provided via the `x mix` template, `nginx-proxy-compose.yml`. This docker-compose file is ready to run and can be copied to the deployment server. + +For example, once copied to remote `~/nginx-proxy-compose.yml`, the following command can be run on the remote server. + +``` +docker-compose -f ~/nginx-proxy-compose.yml up -d +``` + +This will run an nginx reverse proxy along with a companion container that will watch for additional containers in the same docker network and attempt to initialize them with valid TLS certificates. + +## GitHub Repository setup +The `release.yml` assumes 6 secrets have been setup. + +- CR_PAT - GitHub Personal Token with read/write access to packages. +- DEPLOY_HOST - hostname used to SSH to, this can either be an IP address or subdomain with A record pointing to the server. +- DEPLOY_PORT - SSH port, usually `22`. +- DEPLOY_USERNAME - the username being logged into via SSH. Eg, `ubuntu`, `ec2-user`, `root` etc. +- DEPLOY_KEY - SSH private key used to remotely access deploy server/app host. +- LETSENCRYPT_EMAIL - Email address, required for Let's Encrypt automated TLS certificates. + +These secrets can use the [GitHub CLI](https://cli.github.com/manual/gh_secret_set) for ease of creation. Eg, using the GitHub CLI the following can be set. + +```bash +gh secret set CR_PAT -b"" +gh secret set DEPLOY_HOST -b"" +gh secret set DEPLOY_PORT -b"" +gh secret set DEPLOY_USERNAME -b"" +gh secret set DEPLOY_KEY -b"" +gh secret set LETSENCRYPT_EMAIL -b"" +``` + +These secrets are used to populate variables within GitHub Actions and other configuration files. + +## What's the process of `release.yml`? + +![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/mix/release-ghr-vanilla-diagram.png) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..5af79e6 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,35 @@ +name: Build + +on: + pull_request: {} + push: + branches: + - '**' # matches every branch + +jobs: + build: + runs-on: ubuntu-20.04 + steps: + - name: checkout + uses: actions/checkout@v2.0.0 + + - name: setup .net core + uses: actions/setup-dotnet@v1.7.2 + with: + dotnet-version: 6.0.100 + + - name: build + run: dotnet build + working-directory: . + + - name: test + run: | + dotnet test + if [ $? -eq 0 ]; then + echo TESTS PASSED + else + echo TESTS FAILED + exit 1 + fi + working-directory: ./MyApp.Tests + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ea306d5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,138 @@ +name: Release +on: + # Triggered on new GitHub Release + release: + types: [published] + # Triggered on every successful Build action + workflow_run: + workflows: ["Build"] + branches: [main,master] + types: + - completed + # Manual trigger for rollback to specific release or redeploy latest + workflow_dispatch: + inputs: + version: + default: latest + description: Tag you want to release. + required: true + +jobs: + push_to_registry: + runs-on: ubuntu-20.04 + if: ${{ github.event.workflow_run.conclusion != 'failure' }} + steps: + # Checkout latest or specific tag + - name: checkout + if: ${{ github.event.inputs.version == '' || github.event.inputs.version == 'latest' }} + uses: actions/checkout@v2 + - name: checkout tag + if: ${{ github.event.inputs.version != '' && github.event.inputs.version != 'latest' }} + uses: actions/checkout@v2 + with: + ref: refs/tags/${{ github.event.inputs.version }} + + # Assign environment variables used in subsequent steps + - name: Env variable assignment + run: echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV + # TAG_NAME defaults to 'latest' if not a release or manual deployment + - name: Assign version + run: | + echo "TAG_NAME=latest" >> $GITHUB_ENV + if [ "${{ github.event.release.tag_name }}" != "" ]; then + echo "TAG_NAME=${{ github.event.release.tag_name }}" >> $GITHUB_ENV + fi; + if [ "${{ github.event.inputs.version }}" != "" ]; then + echo "TAG_NAME=${{ github.event.inputs.version }}" >> $GITHUB_ENV + fi; + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.CR_PAT }} + + # Build and push new docker image, skip for manual redeploy other than 'latest' + - name: Build and push Docker images + uses: docker/build-push-action@v2.2.2 + if: ${{ github.event.inputs.version == '' || github.event.inputs.version == 'latest' }} + with: + file: Dockerfile + context: . + push: true + tags: ghcr.io/${{ env.image_repository_name }}:${{ env.TAG_NAME }} + + deploy_via_ssh: + needs: push_to_registry + runs-on: ubuntu-20.04 + if: ${{ github.event.workflow_run.conclusion != 'failure' }} + steps: + # Checkout latest or specific tag + - name: checkout + if: ${{ github.event.inputs.version == '' || github.event.inputs.version == 'latest' }} + uses: actions/checkout@v2 + - name: checkout tag + if: ${{ github.event.inputs.version != '' && github.event.inputs.version != 'latest' }} + uses: actions/checkout@v2 + with: + ref: refs/tags/${{ github.event.inputs.version }} + + - name: repository name fix and env + run: | + echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV + echo "domain=${{ secrets.DEPLOY_HOST }}" >> $GITHUB_ENV + echo "letsencrypt_email=${{ secrets.LETSENCRYPT_EMAIL }}" >> $GITHUB_ENV + echo "TAG_NAME=latest" >> $GITHUB_ENV + if [ "${{ github.event.release.tag_name }}" != "" ]; then + echo "TAG_NAME=${{ github.event.release.tag_name }}" >> $GITHUB_ENV + fi; + if [ "${{ github.event.inputs.version }}" != "" ]; then + echo "TAG_NAME=${{ github.event.inputs.version }}" >> $GITHUB_ENV + fi; + + # Populate docker-compose.yml with variables from build process, including TAG_NAME. + - name: docker-compose file prep + uses: danielr1996/envsubst-action@1.0.0 + env: + RELEASE_VERSION: ${{ env.TAG_NAME }} + IMAGE_REPO: ${{ env.image_repository_name }} + APP_NAME: ${{ github.event.repository.name }} + HOST_DOMAIN: ${{ env.domain }} + LETSENCRYPT_EMAIL: ${{ env.letsencrypt_email }} + with: + input: .deploy/docker-compose-template.yml + output: .deploy/${{ github.event.repository.name }}-docker-compose.yml + + # Copy only the docker-compose.yml to remote server home folder + - name: copy compose file via scp + uses: appleboy/scp-action@v0.1.1 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USERNAME }} + port: 22 + key: ${{ secrets.DEPLOY_KEY }} + source: ".deploy/${{ github.event.repository.name }}-docker-compose.yml" + target: "~/" + + - name: Set the value + run: | + echo "GH_TOKEN=${{ secrets.CR_PAT }}" >> $GITHUB_ENV + echo "USERNAME=${{ secrets.DEPLOY_USERNAME }}" >> $GITHUB_ENV + + # Deploy Docker image with ServiceStack application using `docker compose up` remotely + - name: remote docker-compose up via ssh + uses: appleboy/ssh-action@v0.1.4 + env: + APPTOKEN: ${{ env.GH_TOKEN }} + USERNAME: ${{ env.USERNAME }} + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USERNAME }} + key: ${{ secrets.DEPLOY_KEY }} + port: 22 + envs: APPTOKEN,USERNAME + script: | + echo $APPTOKEN | docker login ghcr.io -u $USERNAME --password-stdin + docker-compose -f ~/.deploy/${{ github.event.repository.name }}-docker-compose.yml pull + docker-compose -f ~/.deploy/${{ github.event.repository.name }}-docker-compose.yml up -d diff --git a/Dockerfile b/Dockerfile index 5b67c07..549ba69 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:6.0-focal AS build WORKDIR /app COPY . . @@ -7,7 +7,7 @@ RUN dotnet restore WORKDIR /app/MyApp RUN dotnet publish -c release -o /out --no-restore -FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS runtime +FROM mcr.microsoft.com/dotnet/aspnet:6.0-focal AS runtime WORKDIR /app COPY --from=build /out ./ ENTRYPOINT ["dotnet", "MyApp.dll"]