Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Deploy React app via GitHub Actions

Setup app on VPS

  1. Login to root account.
  2. Download the script.
curl -sSL https://kb.subratlima.com/scripts/vps/setup_app -o setup_app
chmod +x setup_app
  1. Run the script, example given below.
./setup_app "kb"
# where
#   kb  : app/user name

Add Secret keys

  1. Open the GitHub repo page.
  2. Goto Project > Settings > Security > Secrets and variables > Actions.
  3. Add the following secret keys in New Repository secret.
namesecret
VPS_IPserver ip
VPS_USERNAMEserver login username
VPS_PRIVATE_KEYserver user ssh private key
VPS_APP_DIRserver application dir

Create the GitHub workflow

Create the workflow file .github/workflows/deploy.yml.

React App workflow
name: Build and Deploy

on:
  push:
    branches: [main]                            # branch name

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v6

      - name: Install pnpm
        uses: pnpm/action-setup@v4

      - name: Set up Node.js
        uses: actions/setup-node@v6
        with:
          node-version-file: 'package.json'
          cache: 'pnpm'

      - name: Install Dependencies
        run: pnpm install --frozen-lockfile

      - name: Build Project
        run: pnpm run build                     # This generates the dist folder

      - name: Copy Dist to VPS
        uses: appleboy/scp-action@master
        with:
          port: 22                              # SSH port
          source: "dist/*"                      # Path to the dist files
          host: ${{ secrets.VPS_IP }}           # Your VPS IP
          username: ${{ secrets.VPS_USERNAME }} # Your VPS username
          key: ${{ secrets.VPS_PRIVATE_KEY }}   # Your SSH private key
          target: ${{ secrets.VPS_APP_DIR }}    # Destination on your VPS
          strip_components: 1

Push changes to GitHub

Add the workflow, commit and push to GitHub.

Add website to caddy on VPS

  1. Login again to root account.
  2. Download the script.
curl -sSL https://kb.subratlima.com/scripts/vps/caddy_static -o caddy_static
chmod +x caddy_static
  1. Run the script, example given below.
./caddy_static "kb.subratlima.com" "kb"
# where
#   kb.subratlima.com   : domain name
#   kb                  : app/user name

Your app should now auto deploy the updates.

Debian security setup

Restrict ports

  1. Edit the file /etc/nftables.conf.
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;

        # 1. Essential: Allow established/related traffic
        ct state { established, related } accept
        ct state invalid drop

        # 2. Localhost & ICMP (Ping/IPv6 Discovery)
        iifname "lo" accept
        icmp type echo-request accept
        icmpv6 type { echo-request, nd-neighbor-solicit, nd-neighbor-advert, nd-router-solicit, nd-router-advert } accept

        # 3. SSH (Port 2047) with basic rate limiting
        # Allows 10 new connections in a burst, then 2 per minute
        tcp dport 2047 ct state new limit rate 2/minute burst 10 packets accept

        # 4. Web services
        tcp dport { 80, 443 } ct state new accept
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
    }

    chain output {
        type filter hook output priority 0; policy accept;
    }
}
  1. Run the following script:
# check/validate config
nft -c -f /etc/nftables.conf

# load and apply ruleset to the kernel
nft -f /etc/nftables.conf

# start service at boot
systemctl enable nftables

# start the service immediately
systemctl start nftables

Configure SSH

Run this script to harden SSH.

# Set custom port
sed -i 's/^#\?Port .*/Port 2047/' /etc/ssh/sshd_config

# Only allow Ed25519 host keys
sed -i 's|^HostKey /etc/ssh/ssh_host_.*_key|# &|' /etc/ssh/sshd_config
echo "HostKey /etc/ssh/ssh_host_ed25519_key" >> /etc/ssh/sshd_config

# Disable password authentication
sed -i 's/^#\?PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config

# Disable empty passwords
sed -i 's/^#\?PermitEmptyPasswords .*/PermitEmptyPasswords no/' /etc/ssh/sshd_config

# Disable root login (Best practice)
sed -i 's/^#\?PermitRootLogin .*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config

# Enable Public Key Authentication (Usually on by default)
sed -i 's/^#\?PubkeyAuthentication .*/PubkeyAuthentication yes/' /etc/ssh/sshd_config

# Verify syntax and reload
sshd -t && systemctl restart sshd

GitHub CI/CD workflows

Introduction

Create the workflow file .github/workflows/deploy.yml.

React App workflow

name: Build and Deploy

on:
  push:
    branches: [main]                            # branch name

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v6

      - name: Install pnpm
        uses: pnpm/action-setup@v4

      - name: Set up Node.js
        uses: actions/setup-node@v6
        with:
          node-version-file: 'package.json'
          cache: 'pnpm'

      - name: Install Dependencies
        run: pnpm install --frozen-lockfile

      - name: Build Project
        run: pnpm run build                     # This generates the dist folder

      - name: Copy Dist to VPS
        uses: appleboy/scp-action@master
        with:
          port: 22                              # SSH port
          source: "dist/*"                      # Path to the dist files
          host: ${{ secrets.VPS_IP }}           # Your VPS IP
          username: ${{ secrets.VPS_USERNAME }} # Your VPS username
          key: ${{ secrets.VPS_PRIVATE_KEY }}   # Your SSH private key
          target: ${{ secrets.VPS_APP_DIR }}    # Destination on your VPS
          strip_components: 1

GitLab CI/CD workflows

Introduction

Create the workflow file .gitlab-ci.yml.

MDBook App workflow

stages:
  - build_and_deploy

deploy-kb:
  stage: build_and_deploy
  image:
    name: peaceiris/mdbook:latest
    entrypoint: [""]
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      changes:
        - src/**/*
        - book.toml
    - if: $CI_PIPELINE_SOURCE == "web"
  script:
    # 1. Build
    - mdbook build
    
    # 2. Setup Tools (Added curl for IP discovery)
    - apk add --no-cache openssh-client rsync curl
    
    # 3. SSH Agent setup
    - eval $(ssh-agent -s)
    - chmod 400 "$VPS_PRIVATE_KEY"
    - ssh-add "$VPS_PRIVATE_KEY"
    - mkdir -p ~/.ssh
    - ssh-keyscan -p $VPS_SSH_PORT $VPS_IP >> ~/.ssh/known_hosts
    
    # 4. Sync
    - rsync -azq -e "ssh -p $VPS_SSH_PORT" --delete ./book/ $VPS_APP_NAME@$VPS_IP:/var/www/$VPS_APP_NAME/

Plain HTML/CSS/JS App workflow

stages:
  - deploy

deploy:
  image: alpine:latest
  stage: deploy
  script:
    # 1. Setup Deploy Tools
    - apk add --no-cache openssh-client rsync

    # 2. SSH Agent setup
    - eval $(ssh-agent -s)
    - chmod 400 "$VPS_PRIVATE_KEY"
    - ssh-add "$VPS_PRIVATE_KEY"
    - mkdir -p ~/.ssh
    - ssh-keyscan -p $VPS_SSH_PORT $VPS_IP >> ~/.ssh/known_hosts

    # 3. Sync
    # Using -azq: archive mode, compress, quiet
    - rsync -azq -e "ssh -p $VPS_SSH_PORT" --delete --exclude=".git/" . $VPS_APP_NAME@$VPS_IP:/var/www/$VPS_APP_NAME/

uv cheatsheet

Manage python version (replaces pyenv)

PurposeCommand
Install a specific Python versionuv python install <version>
List available Python versionsuv python list
Use a specific Python version in a projectuv python use <version>
Automatically install the required Python versionuv run --python <version> script.py
Pin the Python version for a projectuv python pin

Manage environment (replaces venv)

PurposeCommand
Create a virtual environmentuv venv
Create a virtual environment with specific Python ver.uv venv --python <version>
Activate virtual environment (Linux/macOS)source .venv/bin/activate
Activate virtual environment (Windows).venv\Scripts\activate
Remove a virtual environmentuv remove
Reinstall all dependencies in the virtual environmentuv sync --reinstall

Manage project (replaces poetry)

PurposeCommand
Initialize a new projectuv init <project-name>
Add a package as a dependencyuv add <package-name>
Add a dev dependencyuv add --dev <package-name>
Add a package from Gituv add git+https://github.com/user/repo.git
Remove a packageuv remove <package-name>
Lock dependencies to exact versionsuv lock
Upgrade a specific package only on uv.lockuv lock --upgrade-package <package-name>
Upgrade all dependencies only on uv.lockuv lock --upgrade
Build a Python packageuv build
Publish a package to PyPIuv publish

Manage packages (replaces pip, pipx)

PurposeCommand
Install a packageuv add <package-name>
Remove a packageuv remove <package-name>
Install dependencies from pyproject.tomluv sync
Install dependencies while excluding some groupsuv sync --no-group dev --no-group lint
Install dependencies from requirements.txtuv pip install -r requirements.txt
Freeze dependencies into requirements.txtuv pip freeze > requirements.txt
Generate requirements.txt from uv.lockuv export --format requirements-txt > requirements.txt
Upgrade only the uv.lock fileuv lock --upgrade
Upgrade all packages (uv.lock and execution)uv sync --upgrade
Upgrade a single package (uv.lock and execution)uv sync --upgrade-package <package-name>
Install CLI tools globallyuv tool install <tool-name>
List all installed toolsuv tool list
Remove a globally installed CLI tooluv tool uninstall <tool-name>
Upgrade all installed CLI toolsuv tool upgrade --all

Manage scripts (replaces python tools)

PurposeCommand
Run a Python script inside the virtual environmentuv run <script.py>
Run a script while automatically installing depsuv run --with <package> python script.py
Run a command inside the virtual environmentuv run -- <command>
Run a one-time CLI tool without installing globallyuvx <tool-name> --version
Install a tool globallyuv tool install <tool-name>
Upgrade a specific tooluv tool upgrade <tool-name>
Upgrade all installed toolsuv tool upgrade --all
Enable shell auto-completion for uveval "$(uv generate-shell-completion bash)"

Attribution