Skip to content
Back to blog

Remote Dev Server Guide Part 2: Initial Server Setup

6 min read
View as Markdown

In Part 1, we picked and ordered a Hetzner dedicated server. Now we have a fresh Ubuntu 24.04 box with root access. Let's turn it into a development machine.

Create Your Dev User

Never develop as root. Create a dedicated user with sudo privileges:

adduser dev
usermod -aG sudo dev

Set up SSH key authentication for the new user. This is a prerequisite — in Part 3 we'll disable password authentication entirely, so keys must work first:

# On your local machine, copy your public key
ssh-copy-id dev@YOUR_SERVER_IP

# Or manually
mkdir -p /home/dev/.ssh
echo "your-public-key-here" >> /home/dev/.ssh/authorized_keys
chmod 700 /home/dev/.ssh
chmod 600 /home/dev/.ssh/authorized_keys
chown -R dev:dev /home/dev/.ssh

Verify key login works before moving on — open a new terminal and confirm ssh dev@YOUR_SERVER_IP connects without asking for a password.

Switch to the dev user for the rest of this guide:

su - dev

System Updates

sudo apt update && sudo apt upgrade -y

Install essential build tools:

sudo apt install -y build-essential curl git wget unzip htop tmux jq lsof

Install Node.js

I use the NodeSource APT repository for Node.js. It installs system-wide, so all users (including the deploy user) get access without extra setup:

curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt install -y nodejs

Verify:

node --version  # v24.x.x
npm --version

Install Bun

Bun is a fast JavaScript runtime and package manager. I use it for building and running Next.js apps:

curl -fsSL https://bun.sh/install | bash
source ~/.bashrc
bun --version

Bun installs to ~/.bun/bin/. It's significantly faster than npm/yarn for installing dependencies and running builds.

Install Docker

Docker is essential for running databases and services in isolation:

# Add Docker's official GPG key and repository
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

# Add your user to the docker group (no sudo needed for docker commands)
sudo usermod -aG docker dev
newgrp docker

Database Containers

I run PostgreSQL and Redis in Docker containers for each project. Here's an example docker-compose.yml:

services:
  postgres:
    image: postgres:18
    restart: always
    environment:
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: dev
      POSTGRES_DB: myapp
    ports:
      - "127.0.0.1:5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data

  redis:
    image: redis:alpine
    restart: always
    ports:
      - "127.0.0.1:6379:6379"

volumes:
  pgdata:

Key detail: bind to 127.0.0.1 so databases are only accessible locally, not from the internet.

Install PostgreSQL Client

You'll want the psql client for connecting to your database containers:

sudo apt install -y postgresql-client-16

Set Up tmux

tmux keeps your sessions alive when you disconnect. This is critical for a remote dev server — you can close your laptop, come back later, and everything is exactly where you left it.

Create a basic .tmux.conf:

cat >> ~/.tmux.conf << 'EOF'
# Better prefix
set -g prefix C-a
unbind C-b
bind C-a send-prefix

# Mouse support
set -g mouse on

# Start window numbering at 1
set -g base-index 1
setw -g pane-base-index 1

# Increase scrollback
set -g history-limit 50000

# Better colors
set -g default-terminal "screen-256color"
set -ga terminal-overrides ",xterm-256color:Tc"
EOF

Start a named session for your work:

tmux new-session -s dev

Inside tmux, you can create windows for different tasks:

  • Window 0: Shell / Git operations
  • Window 1: Dev server (bun run dev)
  • Window 2: Database / Docker logs
  • Window 3: Testing

Detach with Ctrl+A, D. Reattach with tmux attach -t dev.

Configure Git

git config --global user.name "Your Name"
git config --global user.email "[email protected]"
git config --global init.defaultBranch main

# Generate SSH key for GitHub
ssh-keygen -t ed25519 -C "[email protected]"
cat ~/.ssh/id_ed25519.pub
# Add this to GitHub → Settings → SSH Keys

Set Up Zsh with Oh My Zsh

I use Oh My Zsh for a better shell experience — autocompletion, Git integration, and a clean prompt:

# Install zsh
sudo apt install -y zsh

# Set zsh as default shell
chsh -s $(which zsh)

# Install Oh My Zsh
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

I use the robbyrussell theme (the default) and the git plugin. In ~/.zshrc:

ZSH_THEME="robbyrussell"
plugins=(git)
source $ZSH/oh-my-zsh.sh

The git plugin gives you useful aliases like gst (git status), ga (git add), gc (git commit), and tab completion for branch names.

Shell Aliases

I add custom aliases to ~/.zshrc for common operations:

# Claude Code shortcuts
alias cc='claude --dangerously-skip-permissions'
alias ccw='claude --dangerously-skip-permissions --worktree'

# tmux session management
alias tn='tmux new-session -s'     # tn myapp
alias tl='tmux list-sessions'      # list all sessions
alias ta='tmux attach-session -t'  # ta myapp
alias tk='tmux kill-session -t'    # tk myapp

The cc alias launches Claude Code in auto-accept mode — useful when you trust it to make file edits without confirmation prompts. The ccw variant does the same but in a Git worktree for isolated work.

Create a Deploy User

For production apps, use a separate user that only runs PM2 processes:

sudo adduser deploy
sudo mkdir -p /var/www
sudo chown deploy:deploy /var/www

This separation means your dev user's environment is isolated from production. Even if something goes wrong in development, your production apps keep running.

Install Nginx

Nginx acts as a reverse proxy in front of your Node.js apps:

sudo apt install -y nginx
sudo systemctl enable nginx

We'll configure Nginx for specific apps in Part 5.

Directory Structure

Here's how I organize things:

/home/dev/
├── projects/           # All development repos
│   ├── ahmedelywa.com/
│   ├── my-other-app/
│   └── ...
├── .claude/            # Claude Code config
└── .ssh/               # SSH keys

/var/www/               # Production deployments (deploy user)
├── ahmedelywa.com/
├── my-other-app/
└── ecosystem.config.js # PM2 config

Development happens in /home/dev/projects/. Production deployments go to /var/www/ under the deploy user. Clean separation.

What's Next

The server is now ready for development work. But it's currently accessible via password authentication on the default SSH port — a huge security risk. In Part 3, we'll lock it down with Tailscale VPN so it's only accessible through your private network.

Series Navigation

  1. Choosing Your Server
  2. Initial Server Setup (you are here)
  3. Security with Tailscale VPN
  4. Claude Code & Development Workflow
  5. Production Deployment with GitHub Actions & PM2