Deploy React app via GitHub Actions
Setup app on VPS
- Login to root account.
- Download the script.
curl -sSL https://kb.subratlima.com/scripts/vps/setup_app -o setup_app
chmod +x setup_app
- Run the script, example given below.
./setup_app "kb"
# where
# kb : app/user name
Add Secret keys
- Open the GitHub repo page.
- Goto
Project > Settings > Security > Secrets and variables > Actions. - Add the following secret keys in
New Repository secret.
| name | secret |
|---|---|
| VPS_IP | server ip |
| VPS_USERNAME | server login username |
| VPS_PRIVATE_KEY | server user ssh private key |
| VPS_APP_DIR | server 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
- Login again to root account.
- Download the script.
curl -sSL https://kb.subratlima.com/scripts/vps/caddy_static -o caddy_static
chmod +x caddy_static
- 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
- 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;
}
}
- 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)
| Purpose | Command |
|---|---|
| Install a specific Python version | uv python install <version> |
| List available Python versions | uv python list |
| Use a specific Python version in a project | uv python use <version> |
| Automatically install the required Python version | uv run --python <version> script.py |
| Pin the Python version for a project | uv python pin |
Manage environment (replaces venv)
| Purpose | Command |
|---|---|
| Create a virtual environment | uv 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 environment | uv remove |
| Reinstall all dependencies in the virtual environment | uv sync --reinstall |
Manage project (replaces poetry)
| Purpose | Command |
|---|---|
| Initialize a new project | uv init <project-name> |
| Add a package as a dependency | uv add <package-name> |
| Add a dev dependency | uv add --dev <package-name> |
| Add a package from Git | uv add git+https://github.com/user/repo.git |
| Remove a package | uv remove <package-name> |
| Lock dependencies to exact versions | uv lock |
Upgrade a specific package only on uv.lock | uv lock --upgrade-package <package-name> |
Upgrade all dependencies only on uv.lock | uv lock --upgrade |
| Build a Python package | uv build |
| Publish a package to PyPI | uv publish |
Manage packages (replaces pip, pipx)
| Purpose | Command |
|---|---|
| Install a package | uv add <package-name> |
| Remove a package | uv remove <package-name> |
| Install dependencies from pyproject.toml | uv sync |
| Install dependencies while excluding some groups | uv sync --no-group dev --no-group lint |
| Install dependencies from requirements.txt | uv pip install -r requirements.txt |
| Freeze dependencies into requirements.txt | uv pip freeze > requirements.txt |
| Generate requirements.txt from uv.lock | uv export --format requirements-txt > requirements.txt |
Upgrade only the uv.lock file | uv 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 globally | uv tool install <tool-name> |
| List all installed tools | uv tool list |
| Remove a globally installed CLI tool | uv tool uninstall <tool-name> |
| Upgrade all installed CLI tools | uv tool upgrade --all |
Manage scripts (replaces python tools)
| Purpose | Command |
|---|---|
| Run a Python script inside the virtual environment | uv run <script.py> |
| Run a script while automatically installing deps | uv run --with <package> python script.py |
| Run a command inside the virtual environment | uv run -- <command> |
| Run a one-time CLI tool without installing globally | uvx <tool-name> --version |
| Install a tool globally | uv tool install <tool-name> |
| Upgrade a specific tool | uv tool upgrade <tool-name> |
| Upgrade all installed tools | uv tool upgrade --all |
| Enable shell auto-completion for uv | eval "$(uv generate-shell-completion bash)" |