Server Setup Recipes
Server Setup Recipes
Sub-chapter 1 of Linux · The CYGWIN home-server runbook, in command form
The Linux primer covers concepts. This sub-chapter is the practical recipe book distilled from the actual Welzin home server setup notes (codename: CYGWIN). Every command here has been run on a real Ubuntu Server LTS box. Treat it as a starting kit - when you next stand up a server (homelab, customer VM, cloud instance), this is the order of operations.
Credentials note. Every password / token / connection string in the original setup doc has been replaced with a
<PLACEHOLDER>. Never paste real credentials into markdown, Slack, or a Google doc. Real values live in 1Password / Bitwarden / AWS Secrets Manager. If you see a<PLACEHOLDER>below, fetch the real value at use-time.
Outline
- First boot - Ubuntu base setup, packages, hostname
- SSH access - server side + your ssh config
- Firewall - UFW recipes
- Users, sudo, permissions
- Editor + Git - make the box pleasant to live in
- Web servers - Apache vs Nginx setup
- LAMP stack - MySQL + PHP + phpMyAdmin
- WordPress - install, configure, change the domain in DB
- Cockpit - web-based server admin
- Tailscale - joining the private mesh
- Ollama - local LLMs on the box
- Daily command reference
CYGWIN - the reference box
For context. The recipes below were validated on this hardware:
| Spec | Value |
|---|---|
| Model | Dell desktop, repurposed as a server |
| CPU | Intel Core i5-6500 @ 3.20 GHz, 2 cores |
| RAM | 16 GB DDR3 |
| Disk | 512 GB HDD |
| Network | Gigabit Ethernet + UPS |
| OS | Ubuntu Server LTS |
Anywhere the commands assume Ubuntu/Debian-flavoured apt. On Amazon Linux / Fedora swap to dnf/yum; on Alpine swap to apk. The intent stays the same.
1. First boot - base setup
After a fresh Ubuntu install:
# Always update the package index + applied patches first
sudo apt-get update && sudo apt-get upgrade -y
# Optional: install a desktop on a server (rare; usually skip)
# sudo apt install ubuntu-desktop
# Confirm your IP - useful when remoting in
hostname -I
Set a memorable hostname:
sudo hostnamectl set-hostname cygwin
# Then update /etc/hosts so 'sudo' stops complaining
sudo sed -i "s/127\.0\.1\.1.*/127.0.1.1\tcygwin/" /etc/hosts
Install the always-useful basics:
sudo apt install -y curl wget htop tmux ca-certificates gnupg lsb-release
2. SSH access
Install the OpenSSH server and enable it on boot:
sudo apt install -y openssh-server
sudo systemctl enable --now ssh
sudo systemctl status ssh # confirm: active (running)
sudo systemctl restart ssh # after edits to /etc/ssh/sshd_config
On your laptop (not the server), generate a key once if you don't have one:
ssh-keygen -o -t ed25519 -C "you@welzin.ai"
ssh-copy-id <username>@<server-ip>
Add an entry to ~/.ssh/config for ergonomics:
Host cygwin
HostName 192.168.1.5 # or the Tailscale name
User <your-linux-user>
IdentityFile ~/.ssh/id_ed25519
Then ssh cygwin Just Works.
Harden the server's sshd (optional but recommended). Edit /etc/ssh/sshd_config:
PasswordAuthentication no
PermitRootLogin no
Reload: sudo systemctl reload ssh.
3. Firewall - UFW
The default policy: deny all incoming, allow all outgoing, then open only what you need.
sudo apt install -y ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH # port 22
sudo ufw enable
sudo ufw status verbose
Common allow rules:
sudo ufw allow 'Apache' # 80
sudo ufw allow 'Apache Full' # 80 + 443
sudo ufw allow 80/tcp # raw port form
sudo ufw allow 123/udp # NTP outbound (sometimes)
sudo ufw allow from 192.168.1.5 to any port 80 # source-restricted
sudo ufw allow 9090/tcp # Cockpit (only behind Tailscale!)
sudo ufw app list # named app profiles UFW knows about
Reload after changes:
sudo ufw reload
To rate-limit SSH (slows brute-force):
sudo ufw limit OpenSSH
4. Users, sudo, permissions
Create a non-root user with sudo:
sudo adduser welzin # interactive: set password, etc.
sudo usermod -aG sudo welzin
sudo -u welzin -i # become that user
Permission cheat-sheet:
chmod 644 file # rw- r-- r--
chmod 755 script.sh # rwx r-x r-x
chmod 600 ~/.ssh/id_ed25519 # rw- --- --- ← required for SSH keys
chown user:group file
sudo -i # become root for a session
5. Editor and Git
sudo apt install -y vim nano git
git --version
# Per-machine git config
git config --global user.name "Your Name"
git config --global user.email "you@welzin.ai"
git config --global init.defaultBranch main
git config --global pull.rebase true
git config --global rebase.autoStash true
git config --list
Generate a key for GitHub on this machine (separate from the SSH-into-server key, conventionally):
ssh-keygen -t ed25519 -C "cygwin-github"
cat ~/.ssh/id_ed25519.pub # paste into GitHub → SSH keys
6. Web servers - Apache or Nginx
Apache
sudo apt install -y apache2
sudo systemctl status apache2
sudo ufw allow 'Apache Full'
# Default site lives at /var/www/html
Site config goes in /etc/apache2/sites-available/<name>.conf:
<VirtualHost *:80>
ServerName yourdomain.com
DocumentRoot /var/www/html/<your-app>
<Directory /var/www/html/<your-app>>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
Enable the site + mod_rewrite:
sudo a2ensite <name>
sudo a2enmod rewrite
sudo systemctl restart apache2
Nginx (alternative)
sudo apt install -y nginx
sudo systemctl status nginx
sudo ufw allow 'Nginx Full'
(Caddy is the third option and the simplest - it auto-fetches Let's Encrypt certificates. Covered in the DevOps chapter's homelab runbook.)
7. LAMP stack - MySQL + PHP + phpMyAdmin
# MySQL server
sudo apt install -y mysql-server
sudo mysql_secure_installation # set root password, lock things down
mysql -V # confirm version
# Connect as root
sudo mysql -u root # then enter your strong password
Create a non-root MySQL user inside mysql>:
CREATE USER 'phpmyadmin_user'@'localhost' IDENTIFIED BY '<DB_PASSWORD>';
GRANT ALL PRIVILEGES ON *.* TO 'phpmyadmin_user'@'localhost' WITH GRANT OPTION;
FLUSH PRIVILEGES;
EXIT;
PHP + phpMyAdmin:
sudo apt install -y php php-mysql php-curl php-gd php-xml php-mbstring php-zip libapache2-mod-php
sudo apt install -y phpmyadmin
The phpMyAdmin installer will ask which web server (pick Apache or Nginx), whether to use dbconfig-common (yes), and a password.
Smoke-test PHP by creating /var/www/html/info.php:
<?php phpinfo(); ?>
Visit http://<server-ip>/info.php. Delete the file immediately after - phpinfo leaks too much detail to leave live.
8. WordPress
Download and place:
cd /tmp
curl -O https://wordpress.org/latest.tar.gz
tar xzvf latest.tar.gz
sudo mkdir -p /var/www/html/wordpress
sudo cp -a wordpress/. /var/www/html/wordpress
Permissions:
sudo chown -R www-data:www-data /var/www/html/wordpress
sudo find /var/www/html/wordpress -type d -exec chmod 755 {} \;
sudo find /var/www/html/wordpress -type f -exec chmod 644 {} \;
Apache vhost (/etc/apache2/sites-available/wordpress.conf):
<VirtualHost *:80>
ServerName yourdomain.com
DocumentRoot /var/www/html/wordpress
<Directory /var/www/html/wordpress>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
Enable:
sudo a2ensite wordpress
sudo a2enmod rewrite
sudo systemctl restart apache2
Edit wp-config.php and fill in DB credentials (read from your secrets manager, not committed to git):
define( 'DB_NAME', 'wordpress' );
define( 'DB_USER', '<DB_USER>' );
define( 'DB_PASSWORD', '<DB_PASSWORD>' );
define( 'DB_HOST', '<DB_HOST>' ); // 'localhost' or RDS endpoint
Changing the WordPress domain later
After a move (or a stage→prod cutover), update both wp-config.php and the DB.
In wp-config.php:
define( 'WP_HOME', 'https://newdomain.com' );
define( 'WP_SITEURL', 'https://newdomain.com' );
In MySQL / phpMyAdmin (replace oldurl.com and newurl.com):
UPDATE wp_options
SET option_value = REPLACE(option_value, 'oldurl.com', 'newurl.com')
WHERE option_name IN ('home', 'siteurl');
UPDATE wp_posts SET guid = REPLACE(guid, 'oldurl.com', 'newurl.com');
UPDATE wp_posts SET post_content = REPLACE(post_content, 'oldurl.com', 'newurl.com');
UPDATE wp_postmeta SET meta_value = REPLACE(meta_value, 'oldurl.com', 'newurl.com');
(The wp_ prefix may differ on customised installs - check $table_prefix in wp-config.php.)
9. Cockpit - web-based admin
A friendly browser UI for system administration. Never expose it to the public internet - bind it behind Tailscale.
. /etc/os-release
sudo apt install -y -t ${VERSION_CODENAME}-backports cockpit
sudo systemctl enable --now cockpit.socket
sudo systemctl status cockpit
sudo ufw allow 9090/tcp
sudo ufw reload
Open https://<server-name-or-tailscale-name>:9090 and log in with a regular Linux user.
10. Tailscale - the private mesh
Install:
curl -fsSL https://tailscale.com/install.sh | sh
Bring it up (this prints a one-time login URL):
sudo tailscale up
Authenticate in your browser with the team's Welzin account. Then from any other node on the same tailnet:
tailscale ip -4 # see this box's 100.x.y.z address
ssh <user>@<tailscale-name> # SSH over the mesh, no public port
Tailscale's MagicDNS makes ssh cygwin work team-wide without /etc/hosts edits.
11. Ollama - local LLMs on the box
curl -fsSL https://ollama.com/install.sh | sh
ollama --version
ollama pull llama3.2
ollama list
ollama ps
ollama run llama3.2 # interactive prompt
Ollama exposes a local HTTP API on 127.0.0.1:11434 for app integration (Chapters VII & VIII go deeper).
12. Daily command reference
# System
hostname -I # this box's IPs
free -h # memory, human-readable
df -h # disk usage by mount
du -sh * # sizes of items in cwd
uptime # how long we've been up + load
journalctl -u <unit> -f # follow a service's logs
# Networking
ss -tlnp # what's listening (TCP, numeric, PIDs)
ip a # interfaces + IPs
ip r # routing table
curl -I https://example.com # check a URL's headers
# Processes
ps aux | grep nginx
top # or htop
kill -9 1234 # SIGKILL pid 1234
# Packages (Ubuntu)
sudo apt update
sudo apt install -y <pkg>
sudo apt remove <pkg>
sudo apt autoremove
dpkg -l | grep <pattern> # list installed packages
# .deb / .sh installers
sudo dpkg -i package.deb
sh installer.sh
# Firewall
sudo ufw status verbose
sudo ufw allow <port>/tcp
sudo ufw enable | disable | reload
Hands-on Checkpoints
- Stand up a fresh Ubuntu Server LTS VM (UTM on Mac, Multipass, a $5 droplet, or a spare laptop).
- Run sections 1–4 end-to-end. Confirm you can
sshin with a key from your laptop and thatufwis enforcing. - Install Apache, serve a single HTML page, hit it from your laptop's browser.
- Install Cockpit. Reach it only via Tailscale.
- Install Ollama, pull
llama3.2, run a one-line chat. - Wire a systemd unit that auto-starts a Python
http.serveron boot. Verify withjournalctl -u <unit> -f. - Audit: run
sudo ss -tlnpand confirm nothing unexpected is listening. If something is, find out what and why.
Further reading
- Ubuntu Server documentation
- How to install LAMP on Ubuntu - DigitalOcean
- Cockpit administration - Linode guide
- Install WordPress on Ubuntu 24.04 - official tutorial
- Tailscale Learn
- Ollama documentation
Welzin opinion: A clean server is a curated server. Anything you install that you didn't decide to install is a future tail. Every six months, walk the
apt list --installedandsystemctl list-units --type=servicelists and prune. Treat the box like a codebase.