Introduction

This article compiles a Standard Operating Procedure (SOP) for Alpine Linux, as a companion to Setting Up a New Linux Server.

Install Alpine Linux

Install via Custom ISO

If your provider (e.g., Netcup, BuyVM, BeroHost) supports uploading customized ISOs, we recommend using the VIRTUAL edition of Alpine Linux, which is stripped and optimized for KVM virtualization.

virtual-iso.webp

One-click DD Reinstall Script

For providers that do not support mounting custom ISOs, you can use a one-click DD reinstall script:

1
2
3
4
5
6
7
8
# Download the script
curl -O https://raw.githubusercontent.com/bin456789/reinstall/main/reinstall.sh || wget -O ${_##*/} $_

# Reinstall Alpine Linux
bash reinstall.sh alpine 3.23 --password 'StrongPassword'

# Reboot
reboot

After completion, log into the new server via SSH; below starts our configuration.

Modify Hostname

1
2
3
4
hostname <Hostname>

# Permanent effect
echo "Hostname" > /etc/hostname

Configure Regular User and sudo Privileges

Avoid directly using the root account for daily operations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Install sudo package
apk add sudo

# Create user
adduser dejavu

# Add dejavu to the wheel group
addgroup dejavu wheel

# Enable passwordless sudo for the wheel group
sed -i 's/# %wheel ALL=(ALL:ALL) NOPASSWD: ALL/%wheel ALL=(ALL:ALL) NOPASSWD: ALL/' /etc/sudoers

Install Base Software Packages

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Update and upgrade existing packages
sudo apk update && sudo apk upgrade

# Frequently used tools
sudo apk add \
    ca-certificates \
    tzdata \
    git \
    curl \
    wget \
    unzip \
    tmux \
    btop \
    bind-tools \
    tree \
    vim

Enable BBR Algorithm

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# Load kernel BBR module
sudo modprobe tcp_bbr

# Write config to ensure auto-loading on boot
echo "tcp_bbr" | sudo tee /etc/modules-load.d/bbr.conf

# Ensure modules service is enabled on boot
sudo rc-update add modules boot

# Write kernel parameters
sudo cat > /etc/sysctl.d/99-bbr.conf <<'EOF'
net.core.default_qdisc = fq_codel
net.ipv4.tcp_congestion_control = bbr
EOF

# Apply configuration
sudo service sysctl restart

# Verify
lsmod | grep bbr
sysctl net.ipv4.tcp_congestion_control

SSH Hardening

Add your public key to the SSH server replacing the traditional password login:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Switch to regular user
su dejavu

# Create authorized key directory
mkdir -p ~/.ssh

# Manually edit SSH public key
vim ~/.ssh/authorized_keys

# Or upload SSH public key
# ssh -i /path/to/your/ed25519_key username@<IP> -p <port>

# Set permissions
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

Refer to SSH Configuration for basic SSH hardening:

1
2
3
4
5
6
7
8
# Edit SSH server configuration
sudo vim /etc/ssh/sshd_config

# Check configuration
sudo sshd -t

# Restart SSH service
sudo rc-service sshd restart

Configure ZRAM and Swap

According to personal habit, you can refer to the following allocation ratios:

CPU RAM ZRAM Swap
1vCPU 1GB 512MB (50%) 2GB
1vCPU 4GB 2GB (50%) 4GB
2vCPU 4GB 2GB (50%) 4GB
4vCPU 20GB+ 4GB (20%) 0~2GB

For small memory servers (RAM ≤ 4GB), I usually enable ZRAM combined with disk Swap.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 1. Create a 4GB Swap file
sudo fallocate -l 4G /swapfile

# Alternative using dd
# sudo dd if=/dev/zero of=/swapfile bs=1M count=4096

# Set permissions and format
sudo chmod 600 /swapfile
sudo mkswap /swapfile

# Auto-mount on boot (pri=10 sets priority)
echo '/swapfile none swap sw,pri=10 0 0' | sudo tee -a /etc/fstab

# Start the service
sudo rc-update add swap boot
sudo service swap start

Configure ZRAM with a higher priority than disk Swap:

1
2
3
4
5
# Install ZRAM initialization tools
sudo apk update && sudo apk add zram-init

# Edit ZRAM configuration file
sudo vim /etc/conf.d/zram-init

Note the following fields:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# /etc/conf.d/zram-init
load_on_start=yes
unload_on_stop=yes
num_devices=1
type0=swap

# Adjust according to demands (MB)
size0=2048

# Use zstd compression algorithm
algo0=zstd

Start the service:

1
2
sudo rc-update add zram-init default
sudo service zram-init start

Optimize kernel memory scheduling:

1
2
3
4
5
sudo cat >> /etc/sysctl.conf <<'EOF'
vm.swappiness = 80
vm.watermark_scale_factor = 125
vm.page-cluster = 0
EOF

Apply kernel parameters:

1
sudo sysctl -p

Verify effects:

1
2
3
sudo zramctl
sudo cat /proc/swaps
sudo swapon --show

Configure Nftables Firewall

1
2
3
4
5
# Install
sudo apk update && sudo apk add nftables

# Edit rules
sudo vim /etc/nftables.nft

Example Configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#!/usr/sbin/nft -f

# Flush all rules
flush ruleset

table inet filter {

    # ============================================================
    # Cloudflare CDN IP Sets
    # ============================================================
    set cloudflare_v4 {
        type ipv4_addr; flags interval;
        elements = {
            173.245.48.0/20, 103.21.244.0/22, 103.22.200.0/22, 103.31.4.0/22,
            141.101.64.0/18, 108.162.192.0/18, 190.93.240.0/20, 188.114.96.0/20,
            197.234.240.0/22, 198.41.128.0/17, 162.158.0.0/15, 104.16.0.0/13,
            104.24.0.0/14, 172.64.0.0/13, 131.0.72.0/22
        }
    }

    set cloudflare_v6 {
        type ipv6_addr; flags interval;
        elements = {
            2400:cb00::/32, 2606:4700::/32, 2803:f800::/32, 2405:b500::/32,
            2405:8100::/32, 2a06:98c0::/29, 2c0f:f248::/32
        }
    }

    # ============================================================
    # INPUT Chain
    # ============================================================
    chain input {
        type filter hook input priority filter; policy drop;

        # Allow loopback interface and established connections
        iif "lo" accept
        ct state { established, related } accept
        ct state invalid drop

        # Allow critical ICMPv4 rules, preventing PMTU blackholes
        ip protocol icmp icmp type { echo-reply, destination-unreachable, echo-request, time-exceeded, parameter-problem } accept

        # Allow critical ICMPv6 rules
        icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, echo-request, echo-reply } accept

        # Allow IPv6 SLAAC and Neighbor Discovery
        icmpv6 type { nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } ip6 hoplimit 255 accept

        # Allow SSH service
        tcp dport 22122 accept

        # Only allow Cloudflare IP ranges to access 80/443
        ip saddr @cloudflare_v4 tcp dport { 80, 443 } accept
        ip6 saddr @cloudflare_v6 tcp dport { 80, 443 } accept
    }

    # ============================================================
    # FORWARD Chain
    # ============================================================
    chain forward {
        type filter hook forward priority filter; policy drop;

        ct state { established, related } accept
        ct state invalid drop

        # qBitTorrent Docker Port Forwarding
        # tcp dport 10880 accept
        # udp dport 10880 accept

        # Allow Docker container outbound and mutual connection
        iifname "docker0" accept
        iifname "br-*" accept
        iifname "docker0" oifname "docker0" accept
        iifname "br-*" oifname "br-*" accept
    }

    # ============================================================
    # OUTPUT Chain
    # ============================================================
    chain output {
        type filter hook output priority filter; policy accept;
    }

}

# Include external dependent configs
include "/var/lib/nftables/*.nft"
include "/etc/nftables.d/*.nft"

Apply rules and start the service:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Verify config
sudo nft -c -f /etc/nftables.nft

# Start service
sudo rc-update add nftables default
sudo service nftables start

# Inspect enabled rules
sudo nft list ruleset

# Subsequent rule updates
sudo nft -f /etc/nftables.nft
sudo service nftables restart

# Restart Docker and Fail2ban after nftables
sudo service fail2ban restart && sudo service docker restart

Configure Fail2ban

1
2
3
4
# Install Fail2ban
sudo apk update && sudo apk add fail2ban

sudo vim /etc/fail2ban/jail.local

Protecting SSH server:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1
bantime  = 1d
findtime = 10m
maxretry = 3
banaction = nftables-multiport
banaction_allports = nftables-allports

[sshd]
enabled = true
# SSH service listening port (must match correctly)
port    = 22122
backend = auto
logpath = /var/log/messages
mode    = aggressive

Start the service:

1
2
3
4
5
sudo rc-update add fail2ban default
sudo service fail2ban start

# Check service status
sudo service fail2ban status

Testing ban:

1
2
3
4
5
# Simulate banning IP
sudo fail2ban-client set sshd banip 2400:6180:0:d2:0:2:9699:d000

# Inspect if f2b dynamic table exists in nftables rules
sudo nft list ruleset | grep f2b

Inspection Rules:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# View running jails
sudo fail2ban-client status

# Inspect specific sshd ban details
sudo fail2ban-client status sshd

# Unban test IP after testing
sudo fail2ban-client set sshd unbanip 2400:6180:0:d2:0:2:9699:d000

# Restart service
sudo service fail2ban restart

Install and Configure Nginx

1
2
3
4
5
6
7
8
# Install Nginx package
sudo apk add nginx openssl

# Enable on boot
sudo rc-update add nginx default

# Start service immediately
sudo service nginx start

Generate self-signed TLS/SSL certificate, preventing scanning traffic on default sites not matching domains.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Create cert directory
sudo mkdir -p /etc/nginx/cert
sudo chmod 700 /etc/nginx/cert

# Generate self-signed certificate
sudo openssl req -x509 -new -nodes -newkey rsa:2048 \
  -sha256 \
  -days 3650 \
  -keyout /etc/nginx/cert/deny.key \
  -out /etc/nginx/cert/deny.pem \
  -subj "/C=XX/ST=Denied/L=Denied/O=Denied/CN=invalid.local" \
  -addext "subjectAltName=DNS:invalid.local"

# Set permissions
sudo chmod 600 /etc/nginx/cert/deny.key
sudo chmod 644 /etc/nginx/cert/deny.pem

# Backup default config
sudo mv /etc/nginx/http.d/default.conf /etc/nginx/http.d/default.conf.bak

# New default virtual host config
sudo vim /etc/nginx/http.d/00-default.conf

Modify content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    return 444; 
}

server {
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;
    server_name _;
    
    ssl_certificate /etc/nginx/cert/deny.pem; 
    ssl_certificate_key /etc/nginx/cert/deny.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;
    error_page 497 =444 /dev/null;
    return 444;
}

If Cloudflare CDN is strict, obtain real client IP addresses:

1
sudo vim /etc/nginx/nginx.conf

Add inside the http section:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
http {
    # ...
    set_real_ip_from 173.245.48.0/20;
    set_real_ip_from 103.21.244.0/22;
    set_real_ip_from 103.22.200.0/22;
    set_real_ip_from 103.31.4.0/22;
    set_real_ip_from 141.101.64.0/18;
    set_real_ip_from 108.162.192.0/18;
    set_real_ip_from 190.93.240.0/20;
    set_real_ip_from 188.114.96.0/20;
    set_real_ip_from 197.234.240.0/22;
    set_real_ip_from 198.41.128.0/17;
    set_real_ip_from 162.158.0.0/15;
    set_real_ip_from 104.16.0.0/13;
    set_real_ip_from 104.24.0.0/14;
    set_real_ip_from 172.64.0.0/13;
    set_real_ip_from 131.0.72.0/22;
    set_real_ip_from 2400:cb00::/32;
    set_real_ip_from 2606:4700::/32;
    set_real_ip_from 2803:f800::/32;
    set_real_ip_from 2405:b500::/32;
    set_real_ip_from 2405:8100::/32;
    set_real_ip_from 2a06:98c0::/29;
    set_real_ip_from 2c0f:f248::/32;
    real_ip_header CF-Connecting-IP;
    # ...
}

Reload Nginx:

1
2
3
4
5
# Verify
sudo nginx -t

# Reload Nginx config
sudo nginx -s reload

Install Docker

1
sudo apk update && sudo apk add docker docker-cli-compose

Optional IPv6 Support, refer to Docker IPv6 Configuration

1
2
sudo mkdir -p /etc/docker
sudo vim /etc/docker/daemon.json

Start Docker service:

1
2
3
4
5
sudo rc-update add docker boot
sudo service docker start

# Optional IPv6 outbound test
sudo docker run --rm curlimages/curl curl -s -I -6 https://blog.zsh.moe

SSD Optimization via fstrim

1
2
3
4
5
6
7
8
# Weekly cron tasks
sudo tee /etc/periodic/weekly/fstrim <<'EOF'
#!/bin/sh
/sbin/fstrim -v / >> /var/log/fstrim.log 2>&1
EOF

# Grant executable permission
sudo chmod +x /etc/periodic/weekly/fstrim

NTP Time Synchronization

Set UTC timezone:

1
2
3
4
sudo setup-timezone -z UTC

# Verify
date

Alpine defaults to ntpd. We recommend chrony servce which converges faster:

1
2
sudo apk add chrony
sudo vim /etc/chrony/chrony.conf

Example Config:

1
2
3
4
server time.cloudflare.com iburst
initstepslew 10 time.cloudflare.com
driftfile /var/lib/chrony/chrony.drift
rtcsync

Start Chrony:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Stop ntpd
sudo service ntpd stop
sudo rc-update del ntpd default

# Start chrony
sudo rc-update add chronyd default
sudo service chronyd start

# Verify syncing status
chronyc sources -v

That concludes the base configuration for this Alpine Linux server.