Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update CI process #9

Merged
merged 2 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .deploy/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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

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:
Empty file modified .deploy/nginx-proxy-compose.yml
100644 → 100755
Empty file.
99 changes: 99 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
## 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="[email protected]"
```

These secrets will populate environment variables within your GitHub Actions workflow and other configuration files, enabling secure and automated deployment of your ServiceStack applications.
38 changes: 28 additions & 10 deletions .github/workflows/build.yml
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
name: Build

on: [push]
on:
pull_request: {}
push:
branches:
- '**' # matches every branch

jobs:
build:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Setup dotnet
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0'
- name: Build
run: dotnet build
- name: Test
run: dotnet test ./MyApp.Tests
- name: checkout
uses: actions/checkout@v3

- name: Setup dotnet
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0'

- 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

76 changes: 48 additions & 28 deletions .github/workflows/release.yml
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,7 @@ jobs:
uses: actions/checkout@v3
with:
ref: refs/tags/${{ github.event.inputs.version }}

- name: Setup dotnet
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0'


# Assign environment variables used in subsequent steps
- name: Env variable assignment
run: echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
Expand All @@ -53,20 +48,42 @@ jobs:
if [ "${{ github.event.inputs.version }}" != "" ]; then
echo "TAG_NAME=${{ github.event.inputs.version }}" >> $GITHUB_ENV
fi;

# Authenticate, build and push to GitHub Container Registry (ghcr.io)
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 <<EOF >> 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 image
working-directory: ./MyApp
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=8080
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
Expand Down Expand Up @@ -97,42 +114,45 @@ jobs:
- name: Create .env file
run: |
echo "Generating .env file"

echo "# Autogenerated .env file" > .env
echo "HOST_DOMAIN=${{ secrets.DEPLOY_API }}" >> .env
echo "DEPLOY_CDN=${{ secrets.DEPLOY_CDN }}" >> .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/[email protected]
with:
host: ${{ secrets.DEPLOY_API }}
host: ${{ secrets.DEPLOY_HOST }}
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
uses: appleboy/[email protected]
env:
APPTOKEN: ${{ secrets.GITHUB_TOKEN }}
USERNAME: ${{ secrets.DEPLOY_USERNAME }}
with:
host: ${{ secrets.DEPLOY_API }}
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USERNAME }}
key: ${{ secrets.DEPLOY_KEY }}
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
docker compose up app-migration --exit-code-from app-migration

# Deploy Docker image with your application using `docker compose up` remotely
- name: remote docker-compose up via ssh
Expand All @@ -141,13 +161,13 @@ jobs:
APPTOKEN: ${{ secrets.GITHUB_TOKEN }}
USERNAME: ${{ secrets.DEPLOY_USERNAME }}
with:
host: ${{ secrets.DEPLOY_API }}
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
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
16 changes: 0 additions & 16 deletions Dockerfile

This file was deleted.

5 changes: 0 additions & 5 deletions MyApp.Client/MyApp.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@
<ItemGroup>
<ProjectReference Include="..\MyApp.ServiceModel\MyApp.ServiceModel.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="wwwroot\appsettings.Production.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>


3 changes: 1 addition & 2 deletions MyApp.Client/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@
// Use / for local or CDN resources
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

var hasConfiguredDeployApi = !builder.Configuration["ApiBaseUrl"]?.Contains("{DEPLOY_API}");
var apiBaseUrl = hasConfiguredDeployApi == true ? (builder.Configuration["ApiBaseUrl"] ?? builder.HostEnvironment.BaseAddress) : builder.HostEnvironment.BaseAddress;
var apiBaseUrl = builder.Configuration["ApiBaseUrl"] ?? builder.HostEnvironment.BaseAddress;
builder.Services.AddBlazorApiClient(apiBaseUrl);
builder.Services.AddLocalStorage();

Expand Down
3 changes: 0 additions & 3 deletions MyApp.Client/wwwroot/appsettings.Production.json

This file was deleted.

Loading
Loading