I appreciate AWS and Terraform and use it regularly, but find it can be over-kill for small side projects as simple as a single Go binary.
With some low traffic side projects, having a second or so of downtime is acceptable. With others, it is not. In those cases, I now have a bash script for zero downtime deployments that can be used on any server or low-end box.
First your program has to be able to accept a port to bind to on startup in the form of ./myservice 8082
.
We now need a way to push the Go binary up to the server and kick off the whole zero downtime deployment process.
For that, as this is a side project, I’m happy using a Makefile
on my laptop. This could easily be converted GitHub Actions if you were working in a small team setting.
deploy:
cd service && GOOS=linux GOARCH=amd64 go build && mv myservice tmp
cd service; scp boot.sh tmp user@ip:~/myservice
ssh -t user@ip 'cd myservice; bash boot.sh'
cd service; rm tmp
Here the bash script on the server checks if there is already a service bound to 8082, and if not we use that port. However, if there is already a service bound to that port we make use of 8083 and get ready to terminate the process on 8082.
We then make an http request to localhost:$PORT
and if that returns as expected, we then update the nginx, reload it, then start killing off
and cleaning up the old process.
SERVICENAME="myservice"
NGINXCONFIG="mysite.com"
SERVERSTARTEDHTTPCODE=404 # Go default index is a "404 page not found"
# See which version is running
EXISTING=$(lsof -t -i :8082)
if [ "$EXISTING" != "" ]; then
NEWPORT=8083
OLDPORT=8082
else
NEWPORT=8082
OLDPORT=8083
fi
NEWNAME=$SERVICENAME$NEWPORT
mv tmp $NEWNAME
# Spin up new instance
nohup ./$NEWNAME $NEWPORT > server.log 2>&1 &
# Allow service time to spin up
sleep 2
# Get the HTTP status code to make sure it's started up correctly
HTTPSTATUS=$(curl -s -o /dev/null -w "%{http_code}" "localhost:$NEWPORT")
if [ "$HTTPSTATUS" -eq "$SERVERSTARTEDHTTPCODE" ]; then
# Swap out the Nginx config for the new port
sed -i "s#.*proxy_pass.*# proxy_pass \"http://127.0.0.1:$NEWPORT\";#" /etc/nginx/sites-enabled/$NGINXCONFIG
sleep 1
# Gracefully restart Nginx
sudo nginx -s reload
# Kill the old service
PID=$(lsof -t -i :$OLDPORT)
if [ -n "$PID" ]; then
# Send the SIGTERM signal to gracefully terminate the process
kill -15 "$PID"
rm $SERVICENAME$OLDPORT
echo "Gracefully terminated $SERVICENAME with PID: $PID" >> server.log
else
echo "No process found for $SERVICENAME" >> server.log
fi
else
echo "Server did not start. Deployment aborted. (HTTP status code: $HTTPSTATUS)" >> server.log
exit 1
fi
One nice improvement would be to use systemctl
so that its managed, easy to control, has a watchdog, and the logging is consistent with other services.
I can also think of ways of cleaning this up and making it more defensive, though as a quick and scrappy script it does the job nicely for now. Maybe if this side project takes off I’ll graduate it to the cloud, but for now a low end box does the trick.