diff --git a/.deploy/docker-compose-template.yml b/.deploy/docker-compose-template.yml deleted file mode 100644 index a1df46c..0000000 --- a/.deploy/docker-compose-template.yml +++ /dev/null @@ -1,25 +0,0 @@ -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} - volumes: - - ${APP_NAME}-mydb:/app/App_Data - ${APP_NAME}-migration: - image: ghcr.io/${IMAGE_REPO}:${RELEASE_VERSION} - restart: "no" - profiles: - - migration - command: --AppTasks=migrate - volumes: - - ${APP_NAME}-mydb:/app/App_Data - -volumes: - ${APP_NAME}-mydb: \ No newline at end of file diff --git a/.deploy/docker-compose.yml b/.deploy/docker-compose.yml new file mode 100644 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 index eccae8d..14a709c 100644 --- a/.deploy/nginx-proxy-compose.yml +++ b/.deploy/nginx-proxy-compose.yml @@ -1,8 +1,8 @@ -version: '2' +version: "3.9" services: nginx-proxy: - image: jwilder/nginx-proxy + image: nginxproxy/nginx-proxy container_name: nginx-proxy restart: always ports: @@ -15,21 +15,27 @@ services: - dhparam:/etc/nginx/dhparam - certs:/etc/nginx/certs:ro - /var/run/docker.sock:/tmp/docker.sock:ro - network_mode: bridge + labels: + - "com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy" letsencrypt: - image: jrcs/letsencrypt-nginx-proxy-companion:2.0 + image: nginxproxy/acme-companion:2.2 container_name: nginx-proxy-le restart: always + depends_on: + - "nginx-proxy" environment: - DEFAULT_EMAIL=you@example.com - volumes_from: - - nginx-proxy volumes: - certs:/etc/nginx/certs:rw - acme:/etc/acme.sh + - vhost:/etc/nginx/vhost.d + - html:/usr/share/nginx/html - /var/run/docker.sock:/var/run/docker.sock:ro - network_mode: bridge + +networks: + default: + name: nginx volumes: conf: @@ -37,4 +43,4 @@ volumes: html: dhparam: certs: - acme: + acme: \ No newline at end of file diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 8133dba..f6656a2 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -1,46 +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. +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. -## Deployment server setup -To get this working, a server needs to be setup with the following: +### Prerequisites -- SSH access -- docker -- docker-compose -- ports 443 and 80 for web access of your hosted application +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. -This can be your own server or any cloud hosted server like Digital Ocean, AWS, Azure etc. +You can use any cloud-hosted or on-premises server like Digital Ocean, AWS, Azure, etc., for this setup. -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. +### Step-by-Step Guide -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. +#### 1. Install Docker and Docker-Compose -For example, once copied to remote `~/nginx-proxy-compose.yml`, the following command can be run on the remote server. +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 ``` -docker-compose -f ~/nginx-proxy-compose.yml up -d + +Add the public key to the `authorized_keys` file on your server: + +```bash +cat ~/.ssh/github_actions.pub >> ~/.ssh/authorized_keys ``` -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. +Then, add the *private* key to your GitHub Secrets as `DEPLOY_KEY` to enable GitHub Actions to SSH into the server securely. -## GitHub Repository setup -The `release.yml` uses the following secrets. +#### 3. Set Up nginx-reverse-proxy -- 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. +You should have a `docker-compose` file similar to the `nginx-proxy-compose.yml` in your repository. Upload this file to your server: -These secrets can use the [GitHub CLI](https://cli.github.com/manual/gh_secret_set) for ease of creation. +```bash +scp nginx-proxy-compose.yml user@your_server:~/ +``` -These secrets are used to populate variables within GitHub Actions and other configuration files. +To bring up the nginx reverse proxy and its companion container for handling TLS certificates, run: -## What's the process of `release.yml`? +```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" +``` -![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/mix/release-ghr-vanilla-diagram.png) +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 index 67b3460..df2d8ae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,15 +8,15 @@ on: jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: checkout - uses: actions/checkout@v2.0.0 + uses: actions/checkout@v3 - - name: setup .net core - uses: actions/setup-dotnet@v1.7.2 + - name: Setup dotnet + uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.0.100 + dotnet-version: '8.0' - name: build run: dotnet build @@ -31,5 +31,5 @@ jobs: echo TESTS FAILED exit 1 fi - working-directory: ./ExampleDataApis.Tests + working-directory: ./MyApp.Tests diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 118bd87..7a75583 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,10 +28,10 @@ jobs: # Checkout latest or specific tag - name: checkout if: ${{ github.event.inputs.version == '' || github.event.inputs.version == 'latest' }} - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: checkout tag if: ${{ github.event.inputs.version != '' && github.event.inputs.version != 'latest' }} - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: ref: refs/tags/${{ github.event.inputs.version }} @@ -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@v1 + uses: docker/login-action@v2 with: registry: ghcr.io - username: ${{ github.repository_owner }} + 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@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 }} - + - 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 @@ -74,18 +94,16 @@ jobs: # Checkout latest or specific tag - name: checkout if: ${{ github.event.inputs.version == '' || github.event.inputs.version == 'latest' }} - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: checkout tag if: ${{ github.event.inputs.version != '' && github.event.inputs.version != 'latest' }} - uses: actions/checkout@v2 + uses: actions/checkout@v3 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 @@ -94,31 +112,31 @@ jobs: 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 - + - name: Create .env file + run: | + echo "Generating .env file" + + 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 compose file via scp + - name: copy files to target server via scp uses: appleboy/scp-action@v0.1.3 with: host: ${{ secrets.DEPLOY_HOST }} username: ${{ secrets.DEPLOY_USERNAME }} - port: ${{ secrets.DEPLOY_PORT }} + port: 22 key: ${{ secrets.DEPLOY_KEY }} - source: ".deploy/${{ github.event.repository.name }}-docker-compose.yml" - target: "~/" - - name: Run remote db migrations - uses: appleboy/ssh-action@v0.1.8 + strip_components: 2 + source: "./.deploy/docker-compose.yml,./.deploy/.env" + target: "~/.deploy/${{ github.event.repository.name }}/" + + - name: Setup App_Data volume directory + uses: appleboy/ssh-action@v0.1.5 env: APPTOKEN: ${{ secrets.GITHUB_TOKEN }} USERNAME: ${{ secrets.DEPLOY_USERNAME }} @@ -129,13 +147,16 @@ jobs: port: 22 envs: APPTOKEN,USERNAME script: | + set -e 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 ${{ github.event.repository.name }}-migration + cd ~/.deploy/${{ github.event.repository.name }} + 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 ServiceStack application using `docker compose up` remotely + # Deploy Docker image with your application using `docker compose up` remotely - name: remote docker-compose up via ssh - uses: appleboy/ssh-action@v0.1.8 + uses: appleboy/ssh-action@v0.1.5 env: APPTOKEN: ${{ secrets.GITHUB_TOKEN }} USERNAME: ${{ secrets.DEPLOY_USERNAME }} @@ -143,9 +164,10 @@ jobs: host: ${{ secrets.DEPLOY_HOST }} username: ${{ secrets.DEPLOY_USERNAME }} key: ${{ secrets.DEPLOY_KEY }} - port: ${{ secrets.DEPLOY_PORT }} + 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 + cd ~/.deploy/${{ github.event.repository.name }} + docker compose pull + docker compose up app -d diff --git a/ExampleDataApis.ServiceInterface/ExampleDataApis.ServiceInterface.csproj b/ExampleDataApis.ServiceInterface/ExampleDataApis.ServiceInterface.csproj index 4dc56b8..cc28523 100644 --- a/ExampleDataApis.ServiceInterface/ExampleDataApis.ServiceInterface.csproj +++ b/ExampleDataApis.ServiceInterface/ExampleDataApis.ServiceInterface.csproj @@ -1,11 +1,11 @@ - net6.0 + net8.0 - + diff --git a/ExampleDataApis.ServiceModel/ExampleDataApis.ServiceModel.csproj b/ExampleDataApis.ServiceModel/ExampleDataApis.ServiceModel.csproj index b7c6133..ecc170a 100644 --- a/ExampleDataApis.ServiceModel/ExampleDataApis.ServiceModel.csproj +++ b/ExampleDataApis.ServiceModel/ExampleDataApis.ServiceModel.csproj @@ -1,11 +1,11 @@ - net6.0 + net8.0 - + diff --git a/ExampleDataApis.Tests/ExampleDataApis.Tests.csproj b/ExampleDataApis.Tests/ExampleDataApis.Tests.csproj index 51a37b7..b309540 100644 --- a/ExampleDataApis.Tests/ExampleDataApis.Tests.csproj +++ b/ExampleDataApis.Tests/ExampleDataApis.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 portable Library @@ -13,8 +13,9 @@ - - + + + diff --git a/ExampleDataApis/ExampleDataApis.csproj b/ExampleDataApis/ExampleDataApis.csproj index 38d7c63..d07bc97 100644 --- a/ExampleDataApis/ExampleDataApis.csproj +++ b/ExampleDataApis/ExampleDataApis.csproj @@ -1,9 +1,10 @@ - net6.0 + net8.0 enable enable + DefaultContainer @@ -16,9 +17,9 @@ - - - + + +