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.

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
|
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
|
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:
Verify effects:
1
2
3
|
sudo zramctl
sudo cat /proc/swaps
sudo swapon --show
|
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
|
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
|
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.