Log in
Progress0 / 30 pages0%
1.
Linux · Sub-chapter 1 · 5 min read

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

  1. First boot - Ubuntu base setup, packages, hostname
  2. SSH access - server side + your ssh config
  3. Firewall - UFW recipes
  4. Users, sudo, permissions
  5. Editor + Git - make the box pleasant to live in
  6. Web servers - Apache vs Nginx setup
  7. LAMP stack - MySQL + PHP + phpMyAdmin
  8. WordPress - install, configure, change the domain in DB
  9. Cockpit - web-based server admin
  10. Tailscale - joining the private mesh
  11. Ollama - local LLMs on the box
  12. Daily command reference

CYGWIN - the reference box

For context. The recipes below were validated on this hardware:

SpecValue
ModelDell desktop, repurposed as a server
CPUIntel Core i5-6500 @ 3.20 GHz, 2 cores
RAM16 GB DDR3
Disk512 GB HDD
NetworkGigabit Ethernet + UPS
OSUbuntu 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:

bash
# 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:

bash
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:

bash
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:

bash
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:

bash
ssh-keygen -o -t ed25519 -C "you@welzin.ai"
ssh-copy-id <username>@<server-ip>

Add an entry to ~/.ssh/config for ergonomics:

sshconfig
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:

text
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.

bash
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:

bash
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:

bash
sudo ufw reload

To rate-limit SSH (slows brute-force):

bash
sudo ufw limit OpenSSH

4. Users, sudo, permissions

Create a non-root user with sudo:

bash
sudo adduser welzin                   # interactive: set password, etc.
sudo usermod -aG sudo welzin
sudo -u welzin -i                     # become that user

Permission cheat-sheet:

bash
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

bash
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):

bash
ssh-keygen -t ed25519 -C "cygwin-github"
cat ~/.ssh/id_ed25519.pub       # paste into GitHub → SSH keys

6. Web servers - Apache or Nginx

Apache

bash
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:

apache
<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:

bash
sudo a2ensite <name>
sudo a2enmod rewrite
sudo systemctl restart apache2

Nginx (alternative)

bash
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

bash
# 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>:

sql
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:

bash
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
<?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:

bash
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:

bash
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):

apache
<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:

bash
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):

php
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:

php
define( 'WP_HOME',    'https://newdomain.com' );
define( 'WP_SITEURL', 'https://newdomain.com' );

In MySQL / phpMyAdmin (replace oldurl.com and newurl.com):

sql
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.

bash
. /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:

bash
curl -fsSL https://tailscale.com/install.sh | sh

Bring it up (this prints a one-time login URL):

bash
sudo tailscale up

Authenticate in your browser with the team's Welzin account. Then from any other node on the same tailnet:

bash
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

bash
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

bash
# 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 ssh in with a key from your laptop and that ufw is 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.server on boot. Verify with journalctl -u <unit> -f.
  • Audit: run sudo ss -tlnp and confirm nothing unexpected is listening. If something is, find out what and why.

Further reading

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 --installed and systemctl list-units --type=service lists and prune. Treat the box like a codebase.

Knowledge check

Pass 80% to unlock
0/1 answered
Why capture server setup as a runbook of exact commands?