diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..aae518b --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,48 @@ +# 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 are used to populate variables within GitHub Actions and other configuration files. + +> If you are deploying multiple ServiceStack apps via this pattern, make sure the host port in the `docker-compose-template.yml` is not being used. This defaults as 8081. + + +## 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..49342c2 --- /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: 5.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: ./ReactSpaTemplate.Tests + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..fcde927 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,86 @@ +name: Release +on: + release: + types: [published] +jobs: + push_to_registry: + runs-on: ubuntu-20.04 + steps: + - name: checkout + uses: actions/checkout@v2 + + - name: dotnet publish + run: | + dotnet publish ./ReactSpaTemplate/ -o publish -c release + + - name: repository name fix + run: echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.CR_PAT }} + + - name: Build and push Docker images + uses: docker/build-push-action@v2.2.2 + with: + file: ReactSpaTemplate/Dockerfile + context: . + push: true + tags: ghcr.io/${{ env.image_repository_name }}:${{ github.event.release.tag_name }} + + deploy_via_ssh: + needs: push_to_registry + runs-on: ubuntu-20.04 + steps: + - name: checkout + uses: actions/checkout@v2 + + - 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 + + - name: docker-compose file prep + uses: danielr1996/envsubst-action@1.0.0 + env: + RELEASE_VERSION: ${{ github.event.release.tag_name }} + IMAGE_REPO: ${{ env.image_repository_name }} + HOST_DOMAIN: ${{ env.domain }} + LETSENCRYPT_EMAIL: ${{ env.letsencrypt_email }} + with: + input: deploy/docker-compose-template.yml + output: deploy/ReactSpaTemplate-docker-compose.yml + + - name: copy compose file via scp + uses: appleboy/scp-action@v0.1.1 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USERNAME }} + port: ${{ secrets.DEPLOY_PORT }} + key: ${{ secrets.DEPLOY_KEY }} + source: "deploy/ReactSpaTemplate-docker-compose.yml" + target: "~/" + + - name: Set the value + run: | + echo "GH_TOKEN=${{ secrets.CR_PAT }}" >> $GITHUB_ENV + echo "USERNAME=${{ secrets.DEPLOY_USERNAME }}" >> $GITHUB_ENV + + - 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: ${{ secrets.DEPLOY_PORT }} + envs: APPTOKEN,USERNAME + script: | + echo $APPTOKEN | docker login ghcr.io -u $USERNAME --password-stdin + docker-compose -f ~/deploy/ReactSpaTemplate-docker-compose.yml up -d diff --git a/ReactSpaTemplate/Dockerfile b/ReactSpaTemplate/Dockerfile new file mode 100644 index 0000000..1e3c4af --- /dev/null +++ b/ReactSpaTemplate/Dockerfile @@ -0,0 +1,4 @@ +FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS runtime +WORKDIR /app +COPY publish/* ./ +ENTRYPOINT ["dotnet", "ReactSpaTemplate.dll"] diff --git a/deploy/docker-compose-template.yml b/deploy/docker-compose-template.yml new file mode 100644 index 0000000..a7376e1 --- /dev/null +++ b/deploy/docker-compose-template.yml @@ -0,0 +1,17 @@ +version: "3.9" +services: + ReactSpaTemplate: + 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} + +networks: + shared: + external: + name: webproxy diff --git a/deploy/nginx-proxy-compose.yml b/deploy/nginx-proxy-compose.yml new file mode 100644 index 0000000..11ba1a9 --- /dev/null +++ b/deploy/nginx-proxy-compose.yml @@ -0,0 +1,45 @@ +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 + 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: + +networks: + default: + external: + name: webproxy \ No newline at end of file