−25%

on annual Windows plans, until 31 Oct. See plans

EQVPS
Get started

How to set up an Alpine Linux VPS: apk, doas, OpenRC and a firewall

First steps on a fresh Alpine Linux 3.21 VPS: update and reboot, create a doas user that can actually log in, turn off root and password SSH, add ufw, switch on cron and daily updates. Includes the Docker port that ufw doesn't block.

Alpine Linux is the smallest image we offer. On our servers a fresh Alpine 3.21 used 55 MB of RAM and 153 MB of disk at idle, against 313 MB and 2.7 GB for Ubuntu 24.04. You get that by giving up the usual tools. Alpine uses apk instead of apt, doas instead of sudo, OpenRC instead of systemd, and musl instead of glibc.

The steps below are the same first ten minutes as on Debian or Ubuntu, translated to Alpine. We ran every command on our Alpine 3.21 image before publishing, and two of them caught us out the first time. Log in as root with the password from your dashboard.

1. Update, then reboot once

apk update && apk upgrade
uname -r
ls /lib/modules

Your server already installed available updates on its first boot, so apk upgrade usually has little left to do. Look at the last two lines instead. If that first-boot update brought a new kernel, the running kernel (uname -r) no longer has its modules on disk. Anything that loads a module then fails until you reboot. On our test server the firewall install printed ERROR: Couldn't determine iptables version. If the two versions differ, reboot now:

reboot

Our test server was back in under a minute.

2. Create a user that can log in

adduser -D deploy
passwd deploy
adduser deploy wheel

Don't skip passwd. adduser -D creates the account without a password, Alpine marks it as locked, and sshd refuses a locked account even with a correct key. In our test the only clue was this line in /var/log/messages:

User deploy not allowed because account is locked

The password also matters later: it's what doas will ask for.

If you added an SSH key when you ordered the server, it is in root's authorized_keys. Copy it to the new user:

install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
install -m 600 -o deploy -g deploy /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys

No key yet? Create one on your computer and paste the public half into /home/deploy/.ssh/authorized_keys.

3. Make doas ask for a password

Alpine uses doas instead of sudo, and our image already allows the wheel group to use it. The rule in /etc/doas.d/wheel.conf is permit nopass :wheel, so any member of wheel becomes root without typing anything. Make it ask for the password, then remember it for a few minutes:

echo 'permit persist :wheel' > /etc/doas.d/wheel.conf

From now on doas apk add nano works the way sudo apt install nano does on Ubuntu.

4. SSH: keys only, no root

sshd reads the drop-in files in /etc/ssh/sshd_config.d/ in name order and keeps the first value it finds for each setting. On our image the first file is 00-eqvps.conf, which allows root and password logins so you can get in on day one. Replace its contents:

cat > /etc/ssh/sshd_config.d/00-eqvps.conf <<'EOF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
EOF
sshd -t && rc-service sshd restart
sshd -T | grep -E '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication) '

The third line matters on Alpine. The image runs the PAM build of OpenSSH, and with only the first two lines our test server still offered keyboard-interactive login. With all three, a login attempt without a key gets:

Permission denied (publickey).

Keep your root session open. In a second terminal, check that ssh deploy@YOUR.SERVER.IP works (on a NAT plan, add -p and the personal port from your dashboard) and that doas id accepts the password.

5. Firewall: ufw

ufw is in the community repository, which is enabled on our image:

apk add ufw
ufw limit 22/tcp
ufw --force enable
rc-update add ufw default
ufw status

limit allows SSH but slows down anyone opening many connections in a row. rc-update matters here, because OpenRC doesn't start the firewall after a reboot unless you add it to the runlevel. After a reboot our server came back with Status: active. On a NAT plan leave SSH on port 22: your personal external port is forwarded to 22 inside the server. On a plan with its own IPv4, open web ports when you need them (ufw allow 80/tcp, ufw allow 443/tcp). The ufw guide covers the rest.

6. Switch on cron and daily updates

A fresh Alpine server doesn't run cron at all. Turn it on:

rc-update add crond default
rc-service crond start

Alpine's root crontab already runs everything in /etc/periodic/daily at 02:00. Drop an update script there:

cat > /etc/periodic/daily/apk-upgrade <<'EOF'
#!/bin/sh
apk upgrade --update-cache --no-progress >> /var/log/apk-auto.log 2>&1
EOF
chmod +x /etc/periodic/daily/apk-upgrade
run-parts --test /etc/periodic/daily

The last command lists the scripts cron will run, and yours should be on the list. This keeps you on the fixes for 3.21. Moving to the next Alpine release is a separate, manual step. Kernel updates still need a reboot: repeat the uname -r / ls /lib/modules check from step 1 now and then.

7. OpenRC in one table

If you know systemd, this is most of what changes:

TasksystemdAlpine (OpenRC)
Start a servicesystemctl start nginxrc-service nginx start
Start at bootsystemctl enable nginxrc-update add nginx default
Statussystemctl status nginxrc-service nginx status
What's runningsystemctl list-unitsrc-status
System logjournalctl/var/log/messages

8. Docker, and the port ufw doesn't block

Alpine makes a good Docker host because the system itself stays out of the way:

apk add docker docker-cli-compose
rc-update add docker default
rc-service docker start
docker run --rm hello-world

On our server this took 12 seconds including the download. With the Docker daemon running, used memory went from 57 MB to 124 MB.

One trap that isn't specific to Alpine: Docker writes its own firewall rules. With ufw allowing only SSH, we started nginx with -p 8080:80 and reached it from outside the server on port 8080. ufw didn't block it. If a container should only be reached through a reverse proxy on the same server, publish it on localhost:

docker run -d -p 127.0.0.1:8080:80 nginx:alpine

Which plan

Alpine leaves almost all of the RAM to your own software. The Nano plan (2 vCPU, 1 GB, 15 GB, $3/mo) is enough for a small service or a few containers. It's a NAT plan, so SSH comes in through a personal port. If you need ports 80 and 443 open to the internet, Nano-IP has its own IPv4 for $8/mo. For heavier Docker stacks, Micro gives you 2 GB for $5/mo.

Alpine 3.21 is on the Alpine VPS page and available on every Linux plan in VPS plans. Payment is in USDT, USDC or ETH on Base, Ethereum or Polygon, and no ID or card is needed.

FAQ

Why is Alpine so much smaller than Ubuntu or Debian?

It uses musl instead of glibc, BusyBox for the basic commands and OpenRC instead of systemd. On our servers a fresh Alpine 3.21 used 55 MB of RAM and 153 MB of disk at idle. Ubuntu 24.04 used 313 MB and 2.7 GB.

Why does my new user get "account is locked" even with the right key?

adduser -D creates the account without a password, which Alpine marks as locked, and sshd then refuses that user even with a valid key. The log line is "User deploy not allowed because account is locked". Give the user a password with passwd and key login starts working.

Why doesn't an SSH tunnel work on Alpine?

Alpine's default /etc/ssh/sshd_config sets AllowTcpForwarding no, so ssh -L forwards are refused. If you need tunnels, change it to yes in that file and run rc-service sshd restart.

Will my program run on Alpine?

Anything from the Alpine package repositories will, and so will Docker containers built on any base image. Prebuilt Linux binaries made for glibc often won't start on musl. Look for an Alpine or musl build, or run the program in Docker. The gcompat package helps with some simple glibc binaries.

Which plan is enough for Alpine?

The 1 GB Nano plan at $3 a month has plenty of room: the system itself uses about 55 MB. If you need ports 80 and 443 open to the internet, take Nano-IP at $8 a month, which has its own IPv4.

Comments

No comments yet. Be the first.

Leave a comment

Comments are moderated before they appear.