Skip to content
Open
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
178 changes: 178 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
name: Build, Push, and Deploy

on:
push:
branches: ["main"]
workflow_dispatch:

env:
REGISTRY: ${{ secrets.REGISTRY }}
IMAGE_REPOSITORY: ${{ secrets.IMAGE_REPOSITORY }}

jobs:
test:
name: Run unit and build checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "18"
cache: "yarn"

- name: Install frontend dependencies
run: yarn install --frozen-lockfile --non-interactive

- name: Build frontend
run: yarn build

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.19"

- name: Run Go tests
working-directory: signalling
run: go test ./...

build-and-push:
name: Build and push container image
runs-on: ubuntu-latest
needs: test
permissions:
contents: read
packages: write
outputs:
image_uri: ${{ steps.image-metadata.outputs.uri }}
image_latest: ${{ steps.image-metadata.outputs.latest }}
steps:
- uses: actions/checkout@v4

- name: Derive image metadata
id: image-metadata
run: |
if [ -z "${REGISTRY}" ]; then
echo "REGISTRY secret must be provided" >&2
exit 1
fi
if [ -z "${IMAGE_REPOSITORY}" ]; then
echo "IMAGE_REPOSITORY secret must be provided" >&2
exit 1
fi
IMAGE_URI="${REGISTRY}/${IMAGE_REPOSITORY}:${GITHUB_SHA}"
IMAGE_LATEST="${REGISTRY}/${IMAGE_REPOSITORY}:latest"
echo "uri=${IMAGE_URI}" >> "$GITHUB_OUTPUT"
echo "latest=${IMAGE_LATEST}" >> "$GITHUB_OUTPUT"
echo "IMAGE_URI=${IMAGE_URI}" >> "$GITHUB_ENV"
echo "IMAGE_LATEST=${IMAGE_LATEST}" >> "$GITHUB_ENV"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}

- name: Build and push image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: |
${{ env.IMAGE_URI }}
${{ env.IMAGE_LATEST }}

deploy:
name: Deploy to VM
runs-on: ubuntu-latest
needs: build-and-push
if: ${{ github.ref == 'refs/heads/main' }}
steps:
- name: Prepare deployment directory
uses: appleboy/ssh-action@v0.1.10
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
set -euo pipefail
DEPLOY_DIR=${{ secrets.DEPLOY_DIR }}
if [ -z "$DEPLOY_DIR" ]; then
echo "DEPLOY_DIR secret must be provided" >&2
exit 1
fi
mkdir -p "$DEPLOY_DIR"

- name: Copy docker-compose file
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: deploy/docker-compose.yml
target: ${{ secrets.DEPLOY_DIR }}

- name: Copy environment template when missing
uses: appleboy/ssh-action@v0.1.10
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
set -euo pipefail
DEPLOY_DIR=${{ secrets.DEPLOY_DIR }}
if [ -z "$DEPLOY_DIR" ]; then
echo "DEPLOY_DIR secret must be provided" >&2
exit 1
fi
if [ ! -f "$DEPLOY_DIR/.env" ]; then
cat <<'ENV' > "$DEPLOY_DIR/.env"
# Environment overrides for Kabootar deployment
CONTAINER_NAME=kabootar
WEB_PORT=80
SIGNALLING_PORT=5000
TURN_PORT=18937
CORS_ENDPOINT=*
TURN_REALM=localhost
TURN_LISTEN_IP=0.0.0.0
PUBLIC_IP=0.0.0.0
SERVER_NAME=_
ENV
fi

- name: Deploy updated container
uses: appleboy/ssh-action@v0.1.10
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
set -euo pipefail
if [ -z "${{ secrets.REGISTRY_USERNAME }}" ] || [ -z "${{ secrets.REGISTRY_PASSWORD }}" ]; then
echo "Registry credentials are required for deployment" >&2
exit 1
fi
DEPLOY_DIR=${{ secrets.DEPLOY_DIR }}
if [ -z "$DEPLOY_DIR" ]; then
echo "DEPLOY_DIR secret must be provided" >&2
exit 1
fi
IMAGE_URI=${{ needs.build-and-push.outputs.image_uri }}
IMAGE_LATEST=${{ needs.build-and-push.outputs.image_latest }}

mkdir -p "$DEPLOY_DIR"
docker login ${{ env.REGISTRY }} -u ${{ secrets.REGISTRY_USERNAME }} -p ${{ secrets.REGISTRY_PASSWORD }}
docker pull "$IMAGE_URI"
docker tag "$IMAGE_URI" "$IMAGE_LATEST"

cd "$DEPLOY_DIR"
export IMAGE="$IMAGE_URI"
docker compose pull kabootar
docker compose up -d --remove-orphans
docker image prune -f
54 changes: 28 additions & 26 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,44 +1,46 @@
# Use the official Golang image to create a build artifact.
FROM golang:1.18 as builder
# Build the signalling server binary
FROM golang:1.18 AS signalling-builder

WORKDIR /app

# Copy the Go Modules manifests
COPY signalling/go.mod signalling/go.sum ./
# Download the dependencies
RUN go mod download

# Copy the source code from the signalling directory
COPY signalling/ .

# Build the application
COPY signalling/ ./
RUN CGO_ENABLED=0 GOOS=linux go build -o main ./cmd/signalling

# Use Nginx image
FROM nginx:alpine
# Build the frontend assets
FROM node:18-alpine AS frontend-builder

# Remove the default Nginx configuration file
RUN rm /etc/nginx/conf.d/default.conf
WORKDIR /app

# Copy a new configuration file from your project
COPY nginx.conf /etc/nginx/conf.d/nginx.conf.template
RUN npm install -g yarn@1.22.22

COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile --non-interactive

RUN apk --no-cache add ca-certificates
COPY tsconfig.json vite.config.ts tailwind.config.js postcss.config.js ./
COPY index.html manifest.json ./
COPY public ./public
COPY src ./src

WORKDIR /root/
RUN yarn build

# Copy the pre-built binary file from the previous stage
COPY --from=builder /app/main /usr/local/bin/main
# Final runtime image with Nginx and the signalling server
FROM nginx:alpine

RUN rm /etc/nginx/conf.d/default.conf \
&& apk --no-cache add ca-certificates gettext

# Copy the start.sh script
COPY signalling/start.sh .
WORKDIR /app

COPY --from=signalling-builder /app/main /usr/local/bin/main
COPY --from=frontend-builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/nginx.conf.template
COPY signalling/start.sh ./start.sh

# Ensure start.sh is executable
RUN chmod +x start.sh
RUN chmod +x ./start.sh

# Expose the port the app runs on
EXPOSE 80 443 18937

# Use the start script as the entry point
# cat the generated start.sh script to see the contents
CMD ["/bin/sh", "./start.sh"]
CMD ["/bin/sh", "./start.sh"]
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,40 @@ Inside the `signalling` directory:
1. Run `yarn install --frozen-lockfile`
1. Run `yarn dev`

## Automated builds and deployments

The repository ships with a GitHub Actions workflow in
`.github/workflows/deploy.yml` that builds, publishes and deploys the combined
Nginx/signalling container whenever changes land on `main`.

### Required GitHub secrets

Add the following secrets under **Settings → Secrets and variables → Actions**:

| Secret | Description |
| ------------------------------------------- | --------------------------------------------------------------- |
| `REGISTRY` | Registry host (for example `ghcr.io` or `docker.io`). |
| `IMAGE_REPOSITORY` | Repository path inside the registry (`owner/kabootar`). |
| `REGISTRY_USERNAME` / `REGISTRY_PASSWORD` | Credentials used to push and pull the image. |
| `SSH_HOST` / `SSH_USER` / `SSH_PRIVATE_KEY` | SSH connection details for the target VM. |
| `DEPLOY_DIR` | Absolute path on the VM where the compose file and `.env` live. |

If you store the image in GitHub Container Registry you can create a fine-grained
PAT with `packages:write` scope and use it for the registry credentials.

### Preparing the VM

1. Install Docker Engine and the Docker Compose v2 plugin.
2. Create the directory referenced by `DEPLOY_DIR` (for example `/opt/kabootar`).
3. Copy `deploy/.env.example` to `.env` inside that directory and update the
values with the public hostname, TURN settings and any TURN credentials.
GitHub Actions will manage the `IMAGE` entry automatically.
4. Ensure the deployment user can run `docker` commands without sudo.

The workflow copies `deploy/docker-compose.yml` to the VM, pulls the freshly
published image and runs `docker compose up -d --remove-orphans`, so the
signalling server and frontend are served together via the Nginx container.

## People

- Akshit Garg
Expand Down
19 changes: 19 additions & 0 deletions deploy/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copy this file to kabootar.env and adjust values for your deployment.
# Secrets (TURN credentials, etc.) should be added after copying rather than
# committed to the repository.

# The GitHub Actions workflow populates IMAGE automatically during deployments.
# When running manually, set IMAGE to a built container reference
# (e.g. ghcr.io/owner/kabootar:latest).
IMAGE=

# Optional overrides for docker-compose.
CONTAINER_NAME=kabootar
WEB_PORT=80
SIGNALLING_PORT=5000
TURN_PORT=18937
CORS_ENDPOINT=*
TURN_REALM=localhost
TURN_LISTEN_IP=0.0.0.0
PUBLIC_IP=0.0.0.0
SERVER_NAME=_
19 changes: 19 additions & 0 deletions deploy/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: "3.8"

services:
kabootar:
image: ${IMAGE:?Set IMAGE to the published Kabootar image}
container_name: ${CONTAINER_NAME:-kabootar}
restart: unless-stopped
ports:
- "${WEB_PORT:-80}:80"
- "${TURN_PORT:-18937}:${TURN_PORT:-18937}/udp"
environment:
LISTEN_ADDRESS: 0.0.0.0
PORT: ${SIGNALLING_PORT:-5000}
CORS_ENDPOINT: ${CORS_ENDPOINT:-*}
TURN_REALM: ${TURN_REALM:-localhost}
TURN_LISTEN_IP: ${TURN_LISTEN_IP:-0.0.0.0}
PUBLIC_IP: ${PUBLIC_IP:-0.0.0.0}
TURN_PORT: ${TURN_PORT:-18937}
SERVER_NAME: ${SERVER_NAME:-_}
49 changes: 44 additions & 5 deletions nginx.conf
Original file line number Diff line number Diff line change
@@ -1,14 +1,53 @@
server {
listen 80;
server_name ${SERVER_NAME};

server_name _;
root /usr/share/nginx/html;
index index.html;

location / {
proxy_pass http://localhost:$PORT ; # Ensure this matches the port your Go app listens on
location /ws/ {
proxy_pass http://127.0.0.1:${PORT};
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_read_timeout 10800; # 3 hours connection timeout
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 10800;
}

location /discover {
proxy_pass http://127.0.0.1:${PORT};
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 10800;
}

location /room {
proxy_pass http://127.0.0.1:${PORT};
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

location /ping {
proxy_pass http://127.0.0.1:${PORT};
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

location / {
try_files $uri $uri/ /index.html;
}
}
}
Loading