From ade27bc715288e9e46b6861d268c06de98bca338 Mon Sep 17 00:00:00 2001 From: Darren Reid Date: Fri, 17 Nov 2023 17:02:15 +1100 Subject: [PATCH 1/2] Sync GitHub Actions --- .deploy/docker-compose.yml | 23 +++++ .deploy/nginx-proxy-compose.yml | 0 .github/workflows/README.md | 144 ++++++++++++++++++++++---------- .github/workflows/build.yml | 4 +- .github/workflows/release.yml | 77 ++++++++++------- Dockerfile | 20 ----- MyApp/App_Data/README.md | 11 +++ docker-compose.override.yml | 19 ----- docker-compose.prod.yml | 31 ------- docker-compose.yml | 15 ---- 10 files changed, 184 insertions(+), 160 deletions(-) create mode 100755 .deploy/docker-compose.yml mode change 100644 => 100755 .deploy/nginx-proxy-compose.yml mode change 100644 => 100755 .github/workflows/README.md mode change 100644 => 100755 .github/workflows/build.yml mode change 100644 => 100755 .github/workflows/release.yml delete mode 100644 Dockerfile create mode 100755 MyApp/App_Data/README.md delete mode 100644 docker-compose.override.yml delete mode 100644 docker-compose.prod.yml delete mode 100644 docker-compose.yml diff --git a/.deploy/docker-compose.yml b/.deploy/docker-compose.yml new file mode 100755 index 0000000..8e64bb4 --- /dev/null +++ b/.deploy/docker-compose.yml @@ -0,0 +1,23 @@ +version: "3.9" +services: + app: + image: ghcr.io/${IMAGE_REPO}:${RELEASE_VERSION} + restart: always + ports: + - "8080" + container_name: ${APP_NAME}_app + environment: + VIRTUAL_HOST: ${HOST_DOMAIN} + VIRTUAL_PORT: 8080 # New default ASP.NET port -> https://learn.microsoft.com/en-us/dotnet/core/compatibility/containers/8.0/aspnet-port + LETSENCRYPT_HOST: ${HOST_DOMAIN} + LETSENCRYPT_EMAIL: ${LETSENCRYPT_EMAIL} + volumes: + - app-mydb:/app/App_Data + +networks: + default: + external: true + name: nginx + +volumes: + app-mydb: diff --git a/.deploy/nginx-proxy-compose.yml b/.deploy/nginx-proxy-compose.yml old mode 100644 new mode 100755 diff --git a/.github/workflows/README.md b/.github/workflows/README.md old mode 100644 new mode 100755 index 8b0baa9..d633ad0 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -1,45 +1,99 @@ -# ServiceStack mix GitHub Actions -The `release.yml` in designed to help with CI deployment to a dedicated server with SSH access, Docker and Docker Compose. - -## Overview -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` uses the following secrets. - -- DEPLOY_HOST - hostname used to SSH to, this can either be an IP address or subdomain with A record pointing to the server. -- 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. - -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) +## Overview + +This template uses the deployment configurations for a ServiceStack .NET 8 application. The application is containerized using Docker and is set up to be automatically built and deployed via GitHub Actions. The recommended deployment target is a stand-alone Linux server running Ubuntu, with an NGINX reverse proxy also containerized using Docker, which a Docker Compose file is included in the template under the `.deploy` directory. + +### Highlights +- 🌐 **NGINX Reverse Proxy**: Utilizes an NGINX reverse proxy to handle web traffic and SSL termination. +- 🚀 **GitHub Actions**: Leverages GitHub Actions for CI/CD, pushing Docker images to GitHub Container Registry and deploying them on a remote server. +- 🐳 **Dockerized ServiceStack App**: The application is containerized, with the image built using `.NET 8`. +- 🔄 **Automated Migrations**: Includes a separate service for running database migrations. + +### Technology Stack +- **Web Framework**: ServiceStack +- **Language**: C# (.NET 8) +- **Containerization**: Docker +- **Reverse Proxy**: NGINX +- **CI/CD**: GitHub Actions +- **OS**: Ubuntu 22.04 (Deployment Server) + + + +## Deployment Server Setup + +To successfully host your ServiceStack applications, there are several components you need to set up on your deployment server. This guide assumes you're working on a standalone Linux server (Ubuntu is recommended) with SSH access enabled. + +### Prerequisites + +1. **SSH Access**: Required for GitHub Actions to communicate with your server. +2. **Docker**: To containerize your application. +3. **Docker-Compose**: For orchestrating multiple containers. +4. **Ports**: 80 and 443 should be open for web access. +5. **nginx-reverse-proxy**: For routing traffic to multiple ServiceStack applications and managing TLS certificates. + +You can use any cloud-hosted or on-premises server like Digital Ocean, AWS, Azure, etc., for this setup. + +### Step-by-Step Guide + +#### 1. Install Docker and Docker-Compose + +It is best to follow the [latest installation instructions on the Docker website](https://docs.docker.com/engine/install/ubuntu/) to ensure to have the correct setup with the latest patches. + +#### 2. Configure SSH for GitHub Actions + +Generate a dedicated SSH key pair to be used by GitHub Actions: + +```bash +ssh-keygen -t rsa -b 4096 -f ~/.ssh/github_actions +``` + +Add the public key to the `authorized_keys` file on your server: + +```bash +cat ~/.ssh/github_actions.pub >> ~/.ssh/authorized_keys +``` + +Then, add the *private* key to your GitHub Secrets as `DEPLOY_KEY` to enable GitHub Actions to SSH into the server securely. + +#### 3. Set Up nginx-reverse-proxy + +You should have a `docker-compose` file similar to the `nginx-proxy-compose.yml` in your repository. Upload this file to your server: + +```bash +scp nginx-proxy-compose.yml user@your_server:~/ +``` + +To bring up the nginx reverse proxy and its companion container for handling TLS certificates, run: + +```bash +docker compose -f ~/nginx-proxy-compose.yml up -d +``` + +This will start an nginx reverse proxy along with a companion container. They will automatically watch for additional Docker containers on the same network and initialize them with valid TLS certificates. + + + +## GitHub Repository Setup + +Configuring your GitHub repository is an essential step for automating deployments via GitHub Actions. This guide assumes you have a `release.yml` workflow file in your repository's `.github/workflows/` directory, and your deployment server has been set up according to the [Deployment Server Setup](#Deployment-Server-Setup) guidelines. + +### Secrets Configuration + +Your GitHub Actions workflow requires the following secrets to be set in your GitHub repository: + +1. **`DEPLOY_HOST`**: The hostname for SSH access. This can be either an IP address or a domain with an A-record pointing to your server. +2. **`DEPLOY_USERNAME`**: The username for SSH login. Common examples include `ubuntu`, `ec2-user`, or `root`. +3. **`DEPLOY_KEY`**: The SSH private key to securely access the deployment server. This should be the same key you've set up on your server for GitHub Actions. +4. **`LETSENCRYPT_EMAIL`**: Your email address, required for Let's Encrypt automated TLS certificates. + +#### Using GitHub CLI for Secret Management + +You can conveniently set these secrets using the [GitHub CLI](https://cli.github.com/manual/gh_secret_set) like this: + +```bash +gh secret set DEPLOY_HOST --body="your-host-or-ip" +gh secret set DEPLOY_USERNAME --body="your-username" +gh secret set DEPLOY_KEY --bodyFile="path/to/your/ssh-private-key" +gh secret set LETSENCRYPT_EMAIL --body="your-email@example.com" +``` + +These secrets will populate environment variables within your GitHub Actions workflow and other configuration files, enabling secure and automated deployment of your ServiceStack applications. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml old mode 100644 new mode 100755 index 66053a7..df2d8ae --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,10 +13,10 @@ jobs: - name: checkout uses: actions/checkout@v3 - - name: setup .net core + - name: Setup dotnet uses: actions/setup-dotnet@v3 with: - dotnet-version: '6.0' + dotnet-version: '8.0' - name: build run: dotnet build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml old mode 100644 new mode 100755 index 6be286d..7a75583 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,7 +34,7 @@ jobs: uses: actions/checkout@v3 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 @@ -48,24 +48,44 @@ jobs: if [ "${{ github.event.inputs.version }}" != "" ]; then echo "TAG_NAME=${{ github.event.inputs.version }}" >> $GITHUB_ENV fi; - + if [ ! -z "${{ secrets.APPSETTINGS_PATCH }}" ]; then + echo "HAS_APPSETTINGS_PATCH=true" >> $GITHUB_ENV + else + echo "HAS_APPSETTINGS_PATCH=false" >> $GITHUB_ENV + fi; + - name: Login to GitHub Container Registry uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + + - name: Setup dotnet + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '8.0' + + - name: Install x tool + if: env.HAS_APPSETTINGS_PATCH == 'true' + run: dotnet tool install -g x + + - name: Apply Production AppSettings + if: env.HAS_APPSETTINGS_PATCH == 'true' + working-directory: ./MyApp + run: | + cat <> appsettings.json.patch + ${{ secrets.APPSETTINGS_PATCH }} + EOF + x patch appsettings.json.patch + # Build and push new docker image, skip for manual redeploy other than 'latest' - - name: Build and push Docker images - uses: docker/build-push-action@v3 - 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 }} - + - name: Build and push Docker image + run: | + dotnet publish --os linux --arch x64 -c Release -p:ContainerRepository=${{ env.image_repository_name }} -p:ContainerRegistry=ghcr.io -p:ContainerImageTags=${{ env.TAG_NAME }} -p:ContainerPort=80 + deploy_via_ssh: needs: push_to_registry runs-on: ubuntu-22.04 @@ -84,8 +104,6 @@ jobs: - 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 @@ -97,14 +115,14 @@ jobs: - name: Create .env file run: | echo "Generating .env file" - - echo "# Autogenerated .env file" > .env - echo "HOST_DOMAIN=${{ secrets.DEPLOY_HOST }}" >> .env - echo "LETSENCRYPT_EMAIL=${{ secrets.LETSENCRYPT_EMAIL }}" >> .env - echo "APP_NAME=${{ github.event.repository.name }}" >> .env - echo "IMAGE_REPO=${{ env.image_repository_name }}" >> .env - echo "RELEASE_VERSION=${{ env.TAG_NAME }}" >> .env - + + echo "# Autogenerated .env file" > .deploy/.env + echo "HOST_DOMAIN=${{ secrets.DEPLOY_HOST }}" >> .deploy/.env + echo "LETSENCRYPT_EMAIL=${{ secrets.LETSENCRYPT_EMAIL }}" >> .deploy/.env + echo "APP_NAME=${{ github.event.repository.name }}" >> .deploy/.env + echo "IMAGE_REPO=${{ env.image_repository_name }}" >> .deploy/.env + echo "RELEASE_VERSION=${{ env.TAG_NAME }}" >> .deploy/.env + # Copy only the docker-compose.yml to remote server home folder - name: copy files to target server via scp uses: appleboy/scp-action@v0.1.3 @@ -113,10 +131,11 @@ jobs: username: ${{ secrets.DEPLOY_USERNAME }} port: 22 key: ${{ secrets.DEPLOY_KEY }} - source: "./docker-compose.yml,./docker-compose.prod.yml,./.env" + strip_components: 2 + source: "./.deploy/docker-compose.yml,./.deploy/.env" target: "~/.deploy/${{ github.event.repository.name }}/" - - - name: Run remote db migrations + + - name: Setup App_Data volume directory uses: appleboy/ssh-action@v0.1.5 env: APPTOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -128,10 +147,12 @@ jobs: port: 22 envs: APPTOKEN,USERNAME script: | + set -e echo $APPTOKEN | docker login ghcr.io -u $USERNAME --password-stdin cd ~/.deploy/${{ github.event.repository.name }} - docker compose -f ./docker-compose.yml -f ./docker-compose.prod.yml pull - docker compose -f ./docker-compose.yml -f ./docker-compose.prod.yml up app-migration + docker compose pull + export APP_ID=$(docker compose run --entrypoint "id -u" --rm app) + docker compose run --entrypoint "chown $APP_ID:$APP_ID /app/App_Data" --user root --rm app # Deploy Docker image with your application using `docker compose up` remotely - name: remote docker-compose up via ssh @@ -148,5 +169,5 @@ jobs: script: | echo $APPTOKEN | docker login ghcr.io -u $USERNAME --password-stdin cd ~/.deploy/${{ github.event.repository.name }} - docker compose -f ./docker-compose.yml -f ./docker-compose.prod.yml pull - docker compose -f ./docker-compose.yml -f ./docker-compose.prod.yml up app -d + docker compose pull + docker compose up app -d diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 13106a0..0000000 --- a/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM mcr.microsoft.com/dotnet/sdk:6.0-focal AS build -WORKDIR /app - -RUN curl -sL https://deb.nodesource.com/setup_16.x | bash - \ - && apt-get install -y --no-install-recommends nodejs \ - && echo "node version: $(node --version)" \ - && echo "npm version: $(npm --version)" \ - && rm -rf /var/lib/apt/lists/* - -COPY . . -RUN dotnet restore - -WORKDIR /app/MyApp -RUN npm install && npm run build -RUN dotnet publish -c release -o /out --no-restore - -FROM mcr.microsoft.com/dotnet/aspnet:6.0-focal AS runtime -WORKDIR /app -COPY --from=build /out ./ -ENTRYPOINT ["dotnet", "MyApp.dll"] diff --git a/MyApp/App_Data/README.md b/MyApp/App_Data/README.md new file mode 100755 index 0000000..5bebc95 --- /dev/null +++ b/MyApp/App_Data/README.md @@ -0,0 +1,11 @@ +## App Writable Folder + +This directory is designated for: + +- **Embedded Databases**: Such as SQLite. +- **Writable Files**: Files that the application might need to modify during its operation. + +For applications running in **Docker**, it's a common practice to mount this directory as an external volume. This ensures: + +- **Data Persistence**: App data is preserved across deployments. +- **Easy Replication**: Facilitates seamless data replication for backup or migration purposes. diff --git a/docker-compose.override.yml b/docker-compose.override.yml deleted file mode 100644 index d8fd152..0000000 --- a/docker-compose.override.yml +++ /dev/null @@ -1,19 +0,0 @@ -version: "3.9" -services: - app: - build: . - ports: - - "5000:80" - volumes: - - app-mydb:/app/App_Data - app-migration: - build: . - restart: "no" - profiles: - - migration - command: --AppTasks=migrate - volumes: - - app-mydb:/app/App_Data - -volumes: - app-mydb: \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml deleted file mode 100644 index 248d793..0000000 --- a/docker-compose.prod.yml +++ /dev/null @@ -1,31 +0,0 @@ -version: "3.9" -services: - app: - image: ghcr.io/${IMAGE_REPO}:${RELEASE_VERSION} - restart: always - ports: - - "80" - container_name: ${APP_NAME}_app - environment: - VIRTUAL_HOST: ${HOST_DOMAIN} - LETSENCRYPT_HOST: ${HOST_DOMAIN} - LETSENCRYPT_EMAIL: ${LETSENCRYPT_EMAIL} - volumes: - - app-mydb:/app/App_Data - app-migration: - image: ghcr.io/${IMAGE_REPO}:${RELEASE_VERSION} - restart: "no" - container_name: ${APP_NAME}_app_migration - profiles: - - migration - command: --AppTasks=migrate - volumes: - - app-mydb:/app/App_Data - -networks: - default: - external: true - name: nginx - -volumes: - app-mydb: \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index bdc4cc0..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,15 +0,0 @@ -version: "3.9" -services: - app: - volumes: - - app-mydb:/app/App_Data - app-migration: - restart: "no" - profiles: - - migration - command: --AppTasks=migrate - volumes: - - app-mydb:/app/App_Data - -volumes: - app-mydb: \ No newline at end of file From ae45d24eeeea18ee5245cf2e970d4309c358c151 Mon Sep 17 00:00:00 2001 From: Darren Reid Date: Mon, 20 Nov 2023 09:36:24 +1100 Subject: [PATCH 2/2] Update .csproj files to net8.0 and adjust versions --- MyApp.ServiceInterface/MyApp.ServiceInterface.csproj | 4 ++-- MyApp.ServiceModel/MyApp.ServiceModel.csproj | 4 ++-- MyApp.Tests/MyApp.Tests.csproj | 6 +++--- MyApp/MyApp.csproj | 5 +++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/MyApp.ServiceInterface/MyApp.ServiceInterface.csproj b/MyApp.ServiceInterface/MyApp.ServiceInterface.csproj index 338e890..91d428a 100644 --- a/MyApp.ServiceInterface/MyApp.ServiceInterface.csproj +++ b/MyApp.ServiceInterface/MyApp.ServiceInterface.csproj @@ -1,11 +1,11 @@ - net6.0 + net8.0 - + diff --git a/MyApp.ServiceModel/MyApp.ServiceModel.csproj b/MyApp.ServiceModel/MyApp.ServiceModel.csproj index b7c6133..ecc170a 100644 --- a/MyApp.ServiceModel/MyApp.ServiceModel.csproj +++ b/MyApp.ServiceModel/MyApp.ServiceModel.csproj @@ -1,11 +1,11 @@ - net6.0 + net8.0 - + diff --git a/MyApp.Tests/MyApp.Tests.csproj b/MyApp.Tests/MyApp.Tests.csproj index df646d3..5d43068 100644 --- a/MyApp.Tests/MyApp.Tests.csproj +++ b/MyApp.Tests/MyApp.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 portable Library @@ -13,8 +13,8 @@ - - + + diff --git a/MyApp/MyApp.csproj b/MyApp/MyApp.csproj index 1a41c66..3ac8ca0 100644 --- a/MyApp/MyApp.csproj +++ b/MyApp/MyApp.csproj @@ -1,7 +1,8 @@  - net6.0 + DefaultContainer + net8.0 enable enable latest @@ -17,7 +18,7 @@ - +