I'm not sure how this will go but this will be my running documentation of setting up a macbook pro as a server. The immediate goal is to get nginx serving out a web page to the public internet. This will be the base case as I would like to move my blog to the macbook.
This is going to involve a couple of things from what I can tell:
Apple Silicon specific stuff
Mac needs to run with the lid closed
It will need ssh
Need some security
Need nginx installed
Set up dynamic dns
letsencrypt for SSL certificate
A domain name
These things aren't in a specific order but get the point across.
I'm going to label the below as steps but they aren't really a path. It's just the order that I took when doing this.
The first thing I did was upgrade my macOS Ventura (13) to macOS Sonoma (14).
The Sonoma background is quite stunning and the moving screensaver is slick.
The second thing I did was reset my macbook. I don't want my apple account tied to what will be a public server. This involved having to log in to my apple account which is always worrying as I never rememeber the password. Luckily I got it after a couple of errors.
I set up the macbook and skipped the Apple ID stuff.
I don't want to have to work directly on the mac so this is the most useful step.
Click the Apple icon -> Search for Remote Login -> Toggle this on and make sure to give full disk access.
I already gave my macbook a static IP on the router.
I want to keep the macbook close while staying on, this was straightforward to do from the command line.
sudo pmset -a disablesleep 1
I wonder if the screen is turning off as I don't think it is.
At this point I can close the macbook and ssh in to configure everything else!
I want to use the brew package manager to manage the various other things I'm going to be installing so the next step is to install brew. Luckily this is really straightforward.
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Follow the installer and make sure to update the profile for your shell so brew is available.
I tested brew by installing wget.
brew install wget
I like the fish shell for the suggestions and auto complete.
brew install fish
We need to add fish to the list of available shells, to do this, update /etc/shells:
sudo vim /etc/shells
and add fish after the rest:
...
/bin/zsh
/opt/homebrew/bin/fish
We also need to update fish to have access to brew. Open ~/.config/fish/config.fish and add the following:
if status is-interactive
fish_add_path /opt/homebrew/bin
end
Now we can use chsh to change the shell we log in to:
Shell: /opt/homebrew/bin/fish
Now we can log out and back in and we should be in Fish.
I need my usual vim stuff.
I try to keep my vim configuration simple. The biggest thing is that I use the Seuol256 colorscheme, undo directory and vim-polygot to get syntax highlighting.
Now to finally install nginx!
brew install nginx
brew services start nginx
Let's see if nginx is running:
brew services list
Name Status User File
nginx started server ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist
Now we should be able to navigate to our server on port 8080:
192.168.13.79:8080
This is the standard nginx web page. The configuration for this page can be found in /opt/homebrew/etc/nginx.
You can add more applications by adding to the /opt/homebrew/etc/nginx/servers folder.
I created a simple test directory and test page and set it up to be served on 8081.
I needed to add an extra option to nginx so that going to urls didn't result in the port number getting added in:
server {
listen 8081;
listen [::]:8081;
server_name example.com;
port_in_redirect off;
autoindex on;
root /Users/server/example.com/;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
Time to secure ssh. Mostly this is going to changing the default port and switching to using ssh keys.
ssh-keygen -f ~/.ssh/servername
Once the key is generated we need to push it to the server:
ssh-copy-id -i ~/.ssh/servername 192.168.13.79
Now we should be able to use ssh without giving a password.
Once that is working we can disable password authentication. We first need to update /etc/ssh/sshd_config.
Add/Update the following:
PermitRootLogin no
AllowUsers username
PasswordAuthentication no
KbdInteractiveAuthentication no
UsePAM no
Now restart sshd:
sudo launchctl unload /System/Library/LaunchDaemons/ssh.plist
sudo launchctl load -w /System/Library/LaunchDaemons/ssh.plist
The -w let's the service start up on boot.
Now we should have ssh secured so that only connections made with the valid key can get it.
Time to make the server public! I'm running this server from home so I need to set the DNS directly and ideally make it automatic so that if my IP changes, my DNS record will also change.
I use namecheap and so the URL that needs to be hit to set a dynamic IP is:
https://dynamicdns.park-your-domain.com/update?host={@,www}&domain={example.com}&password={password}
This will set the Dynamic DNS A record in namecheap to the IP address the request is from.
This will set the DNS record to point to my home's IP address. I'll need to do some port forwarding on the router to actually get that traffic sent to the mac server.
This is going to be specific to a router. You will need to go into something like the firewall or port forwarding and add a rule to take any traffic coming in for port 80 and port 443 and pass it to the server.
Once this step is done, we should be able to go to the URL and see something from our server. In my case I forwarded port 8081 and was able to see my test application from the public internet!
brew install inadyn
I added a custom configuration for namecheap to /opt/homebrew/etc/inadyn.conf:
custom namecheap {
username = example.com
password = password
ddns-server = dynamicdns.park-your-domain.com
ddns-path = "/update?domain=%u&password=%p&host=%h&ip=%i"
hostname = { "@", "www" }
}
Now I need to get SSL certificates set up.
brew install certbot
Official Instructions for nginx on macos
I had to create the .well-known file with full permissions to get certbot to successfully generate certificates.
sudo certbot certonly --webroot
This will prompt you for the domain name and the path to the web folder.
The certificates will be saved to /etc/letsencrypt/live/.
I had to change the permissions of letsencrypt as I'm running nginx as a non-root user.
sudo chmod -R 755 /etc/letsencrypt/archive
sudo chmod -R 755 /etc/letsencrypt/live
Now we should be able to do update the nginx configuration with the ssl certificates:
server {
listen 8081 ssl;
listen [::]:8081 ssl;
server_name example.com;
port_in_redirect off;
autoindex on;
root /Users/server/example.com/;
index index.html;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / {
try_files $uri $uri/ =404;
}
}
Check the configuration:
nginx -t
Restart nginx:
brew services restart nginx
If everything went well we should now be able to go to the domain and have everything work!
https://example.com
The final step is to add the certbot renewal to the root crontab.
sudo crontab -e
Add the following:
0 */12 * * * certbot renew --nginx
This will try doing a renewal every 12 hours.
At this point I have everything set up and my mac is functioning as a server.
The most difficult part was once again dealing with the SSL stuff. Hopefully it gets easier next time. I also learned quite a bit about the mac now and how it works. The differences between it and Linux look to be small but are important.
Brew has been fantastic as a package manager
I still need to figure how to get nginx to start up automatically along with inadyn to do the dynamic dns. I was also surprised to learn how simple dynamic dns looks to be as a user. I need to hit a namecheap url to trigger a dns record update. I wonder how fast it is, probably it depends on the caches.
The next thing is to set up ScarletDME so I can build out the real application that I want to self host. This part is probably going to be a bit more involved but I'm looking forward to it.