Solun Deploy
Deployment
The Concept
We wanted to have a simple and automated deployment process that is easy to use and maintain. The idea was that we have a dev and a prod branch, which we managed to do. We wanted that every time the new code is pushed the apps are redeployed.
The Implementation
So we added a GitHub Action that is building the Docker Images and pushing them to the GitHub Container Registry. Here is the Code for the Action:
name: Create and publish a Docker image
on:
push:
branches:
- main
- dev
- docker
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
At the moment, we are only building the Docker Image and not Deploying it.
To restart the Containers we are using the apt package webhooks
on the API Sever.
Our Traefik listening on the port 80 and 443 and is forwarding the requests to the API Server.
http:
routers:
webhooks:
rule: 'Host(`restart.solun.pm`)'
tls:
options: default
service: sv-webhooks
services:
sv-webhooks:
loadBalancer:
servers:
- url: 'http://172.17.0.30:9000'
The Webhooks are listening on port 9000 and are running a bash scripts to restart the containers if a request is sent to the endpoints. These bash scripts are running the following commands:
#!/bin/bash
(ssh [email protected] "docker image prune -f && cd /root && docker compose pull && docker compose up -d") >&2
In this Docker Compose file, we are setting up the environment variables and the volumes. A sample File can be found here.
Deploy from GitHub
The deployment from GitHub is after this Setup very easy. After you can just run a Webhook to restart the Container. Now we just have to add this to the Docker Build Action:
- name: Restart Node 1
run: |
curl -X POST ${{ secrets.WEBHOOK_DOMAIN }}/hooks/restart-app-1
- name: Wait for server 1 to restart
run: |
sleep 60s
- name: Restart Node 2
run: |
curl -X POST ${{ secrets.WEBHOOK_DOMAIN }}/hooks/restart-app-2
- name: Setup Node.js environment
uses: actions/setup-[email protected]
Why are we using two Frontend Servers?
As you can see, we are using two Frontend Servers. Mainly because we want to provide a high uptime for our users even if we are deploying a new Version. As I said earlier, we are using Traefik as a reverse proxy and load balancer. If we deploy a New Version of out App the Traefik Health Check and Load Balancer will automatically move the traffic to the other server.
The Version History
So that our users can see when a new version is deployed, a webhook is sent to our Discord server after a successful deployment and provided with information. This is done with the following code:
- name: Discord Webhook for Main
if: github.ref == 'refs/heads/main'
run: |
PACKAGE_VERSION=$(node -p "require('./package.json').version")
COMMIT_LINK="https://github.com/${{ github.repository }}/commit/${{ github.sha }}"
curl -X POST ${{ secrets.DISCORD_WEBHOOK }} \
-H "Content-Type: application/json" \
-d '{
"embeds": [{
"title": "New Production Version deployed on solun.pm",
"url": "https://solun.pm",
"description": "[Changes]('$COMMIT_LINK')",
"fields": [
{ "name": "Version", "value": "'$PACKAGE_VERSION'", "inline": false }
],
"color": 255
}]
}'
- name: Discord Webhook for Dev
if: github.ref == 'refs/heads/dev'
run: |
PACKAGE_VERSION=$(node -p "require('./package.json').version")
COMMIT_LINK="https://github.com/${{ github.repository }}/commit/${{ github.sha }}"
curl -X POST ${{ secrets.DISCORD_WEBHOOK }} \
-H "Content-Type: application/json" \
-d '{
"embeds": [{
"title": "New Development Version deployed on dev.solun.pm",
"url": "https://dev.solun.pm",
"description": "[Changes]('$COMMIT_LINK')",
"fields": [
{ "name": "Version", "value": "'$PACKAGE_VERSION'", "inline": false }
],
"color": 16753920
}]
}'
Problems
The biggest problem we had was how we can build the Docker Images and still use the environment variables. To build the Docker Images, we Need a Dockerfile that provides Information about environment variables and the ports. The Problem with this is that after a NextJS app is built, the environment variables are fixed and cannot be changed. So we had to find a way to build the Docker Images and still use the environment variables.
The Solution
The solution was to use a Dockerfile that is only used to install the Packages and not to build the NextJS app. For this, we are using the following Dockerfile:
Dockerfile
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm install
ENV MONGODB_URL=
ENV JWT_SECRET_KEY=
ENV MAILSERVER_BASEURL=
ENV MAILSERVER_API_KEY=
ENV NEXT_PUBLIC_API_DOMAIN=
ENV NEXT_PUBLIC_MAIN_DOMAIN=
ENV NEXT_PUBLIC_AUTH_DOMAIN=
ENV NEXT_PUBLIC_WEBMAIL_DOMAIN=
ENV NEXT_PUBLIC_WEBMAIL_AUTH_DOMAIN=
ENV NEXT_PUBLIC_MAIL_HOST=
ENV NEXT_PUBLIC_IMAP_PORT=
ENV NEXT_PUBLIC_SMTP_PORT=
CMD npm run build && npm run start
To exclude the .evn file from the Docker Image, we are using a .dockerignore file:
.dockerignore
.env
.env.local
.env.sample
As you can see, we are only installing the packages and not building the NextJS app. The NextJS app is build after Starting the Container locally and defining the environment variables in the Compose file.
docker-compose.yml
version: '3.8'
services:
solun:
image: ghcr.io/solun-pm/solun:docker
container_name: solun
ports:
- 3000:3000
environment:
- MONGODB_URL=
- JWT_SECRET_KEY=
- MAILSERVER_BASEURL=
- MAILSERVER_API_KEY=
- NEXT_PUBLIC_API_DOMAIN=
- NEXT_PUBLIC_MAIN_DOMAIN=
- NEXT_PUBLIC_AUTH_DOMAIN=
- NEXT_PUBLIC_WEBMAIL_DOMAIN=
- NEXT_PUBLIC_WEBMAIL_AUTH_DOMAIN=
- NEXT_PUBLIC_MAIL_HOST=
- NEXT_PUBLIC_IMAP_PORT=
- NEXT_PUBLIC_SMTP_PORT=
volumes:
- /your/path:/app/public/uploads/files
restart: always