Yesterday and today I did some work on CI/CD for my Old School Runescape blog/guides website lilgobguides, which I redeveloped recently as a Razor pages app with Bootstrap and an SQLite database. This post just notes some details on what I did for that.
Bootstrap as an NPM package and SCSS compilation
There were some considerations with regards to Bootstrap; a new Razor pages app created with dotnet new razor
has already compiled Bootstrap in the project inside wwwroot/js
and wwwroot/css
. This approach limits how much customization you can do (see Customize Bootstrap docs). In order to customize the Bootstrap theme, I install Bootstrap as an npm package and have this scss/custom-bootstrap.scss
in the project root:
$primary: #ff00a6;
$body-bg: #121212;
$font-family-base: 'Segoe UI', sans-serif;
@import "../node_modules/bootstrap/scss/bootstrap";
Then in package.json, sass
is included as a dependency, and there is an npm script to compile the Bootstrap SCSS and place the compiled Bootstrap CSS in wwwroot/css
:
{
"name": "lilgobguides",
"version": "1.0.0",
"description": "Old School Runescape Guides For Ultimate Irons",
"main": "index.js",
"scripts": {
"build-bootstrap-css": "sass scss/custom-bootstrap.scss wwwroot/css/bootstrap-custom.css",
"build-bootstrap-js-win": "copy \"node_modules\\bootstrap\\dist\\js\\bootstrap.bundle.min.js\" \"wwwroot\\js\\bootstrap.bundle.min.js\"",
"build-bootstrap-js-linux": "cp node_modules/bootstrap/dist/js/bootstrap.bundle.min.js wwwroot/js/bootstrap.bundle.min.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"bootstrap": "^5.3.6",
"sass": "^1.89.0"
}
}
In the above there are also two scripts for copying the Bootstrap JS to wwwroot/js
; one is for my Windows machine that I use to develop the site, and the other is for the GitHub Actions runner which is Linux.
GitHub action
With those npm scripts, the GitHub action is:
name: deploy-lilgobguides
on:
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install npm packages
working-directory: .
run: npm install
- name: Build Bootstrap CSS
working-directory: .
run: npm run build-bootstrap-css
- name: Build Bootstrap JS
working-directory: .
run: npm run build-bootstrap-js-linux
- name: Publish self-contained executable
run: dotnet publish . -r linux-arm64 --self-contained true
- name: SCP dotnet executable to VM
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.PRIVATE_SSH_KEY }}
source: "./bin/Release/net8.0/linux-arm64/publish"
target: ${{ secrets.lilgobguides_DEPLOY_DIRECTORY }}
The above checks out the repo, installs .NET, Node, and the npm packages, runs those scripts in package.json
to compile/copy the Bootstrap assets to wwwroot
, compiles the .NET project (now including those frontend assets) into a self-contained assembly targeting linux-arm64 and SCPs the resultant artifact to the host machine with the appleboy/scp-action
.
systemd service
On the Azure VM there is a systemd service file for the app /etc/systemd/system/lilgobguides.service
:
[Unit]
Description=lilgobguides
[Service]
Type=simple
WorkingDirectory=/path_to/lilgobguides/bin/Release/net8.0/linux-arm64/publish
User=user-which-app-runs-as
ExecStart=/path_to/lilgobguides/bin/Release/net8.0/linux-arm64/publish/lilgobguides
Environment=ASPNETCORE_URLS="http://0.0.0.0:5002"
Environment=lilgobguides_DATABASE_PATH="Data Source=/path_to/lilgobguides.db"
[Install]
WantedBy=multi-user.target
This allows starting up the app with sudo service lilgobguides start
or sudo service lilgobguides restart
; if changes are made to that file it is necessary to run sudo systemctl daemon-reload
first; and sudo service lilgobguides status
shows the status and if that shows the app has exited sudo journalctl -u lilgobguides.service -n 50
can be useful to see the problem.
User=
sets the user that the process will start under; as the Program.cs
has the SQLite database initially created by migrations, that affects the file ownership of that database.
Specifying the working directory avoids path-related errors.
Environment=ASPNETCORE_URLS="http://0.0.0.0:5002"
sets an environment variable that has the effect of the running app listening to HTTP on that port. If this environment variable was set to two origins, those would be for HTTP and HTTPS, and my experience with this work was that with two origins set, the app would listen on the HTTPS one. In my case I am using Apache to serve the site over port 443 with HTTPS and acting as a reverse proxy to the app server, so it’s fine for it to use HTTP.
The above also sets an environment variable for the path to the SQLite database; in Program.cs it is used here:
string dbConnection = null!;
if (builder.Environment.IsDevelopment())
{
dbConnection = builder.Configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("No database connection string");
}
else if (builder.Environment.IsProduction())
{
dbConnection = builder.Configuration["lilgobguides_DATABASE_PATH"]
?? throw new InvalidOperationException("Is there environment variable for database path?");
}
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlite(dbConnection));
If the "DefaultConnection"
value in appsettings.json
were used as in development, the database would be created in the deployment folder with the .NET assemblies.
Apache configuration
The Apache .conf
file for the site:
<VirtualHost *:443>
ServerName lilgobslayerguides.net
ProxyPreserveHost On
ProxyPass / http://localhost:5002/
ProxyPassReverse / http://localhost:5002/
ErrorLog ${APACHE_LOG_DIR}/app-error.log
CustomLog ${APACHE_LOG_DIR}/app-access.log common
SSLEngine On
SSLCertificateFile /path_to/lilgobslayerguides_net.crt
SSLCertificateKeyFile /path_to/lilgobslayerguides.net.key
SSLCertificateChainFile /path_to/lilgobslayerguides_net.ca-bundle
SSLProtocol -all +TLSv1.2
</VirtualHost>
With Apache virtual hosts, the VM is able to serve multiple websites over the same port and IP address (port 443 for HTTPS, and it is the same IP address since it is the same VM) by matching the ServerName
with the request; this shows how Apache is used as a reverse proxy and conventional names for the TLS cert files.