Remote Dev Server Guide Part 2: Initial Server Setup
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
- Choosing Your Server
- Initial Server Setup (you are here)
- Security with Tailscale VPN
- Claude Code & Development Workflow
- Production Deployment with GitHub Actions & PM2