nox.im · All Snippets · All in Ubuntu · All in Server

Ubuntu Deploy, Build and Run Go Services

This article is meant for rapid development without CI/CD pipelines and small teams sharing a VPS or otherwise private server on Vultr, Digital Ocean or Linode.

Go is outdated in the Debian repositories, install a more up to date version through snap:

sudo apt-get install -y snapd
sudo snap install go --classic

for dependency management, make Go packages available via the vendor directory using go mod:

go mod vendor

Deploy with SSH scp

Archive both the tree at HEAD (note that you have to have changes committed) and the vendor directory

git archive --format=tar --prefix=myproject/ HEAD | gzip > myproject.tar.gz
tar cvzf myproject-vendor.tar vendor

NOTE: make sure the first git archive command has a trailing slash on the *prefix! Otherwise it won’t extract into a subdirectory.

Ship it to the remote server

scp myproject.tar.gz root@10.3.141.135:/root
scp myproject-vendor.tar.gz root@10.3.141.135:/root

Build on the target machine

On the machine

tar xzf ../myproject-vendor.tar.gz
go build -mod=vendor .

Which can be scripted with ssh too and added to a Makefile for example:

ssh root@10.3.141.135 "tar xzf myproject.tar.gz && cd myproject && tar xzf ../myproject-vendor.tar.gz && go build -mod=vendor ."
PROJECT_SHORTNAME:=myproject
REMOTE_USER:=root
REMOTE_HOST:=10.3.141.135
REMOTE_PATH:=/root

upload:
	go mod vendor
	git archive --format=tar --prefix=$(PROJECT_SHORTNAME)/ HEAD | gzip > $(PROJECT_SHORTNAME).tar.gz
	tar czf $(PROJECT_SHORTNAME)-vendor.tar.gz vendor
	scp $(PROJECT_SHORTNAME).tar.gz $(REMOTE_USER)@$(REMOTE_HOST):$(REMOTE_PATH)
	scp $(PROJECT_SHORTNAME)-vendor.tar.gz $(REMOTE_USER)@$(REMOTE_HOST):$(REMOTE_PATH)
	scp .env.remote $(REMOTE_USER)@$(REMOTE_HOST):$(REMOTE_PATH)/$(PROJECT_SHORTNAME)/.env

remote: upload
	ssh $(REMOTE_USER)@$(REMOTE_HOST) "tar xzf $(PROJECT_SHORTNAME).tar.gz && cd $(PROJECT_SHORTNAME) && tar xzf ../$(PROJECT_SHORTNAME)-vendor.tar.gz && go build -mod=vendor . && echo 'success.'"

Dotenv configuration through the environment

Most services abide the 12 factor appraoch and store configuration in the environment. To load a .env file into the environment prior to the service binary we can add a small shell script that we execute instead of the Go binary directly:

#!/bin/sh
export $(grep -v '^#' .env | xargs) && ./myproject

Our .env in this case would look as follows:

ENV_VAR_1=foo
ENV_VAR_2=bar

Databases

I’d recommend to support multiple storage layers and instead of any heavy setup like PostgreSQL and MySQL, revert to Sqlite for quick iterations and tests. On Ubuntu install the sqlite command line tools:

apt-get install sqlite3

Running Services in the background with Tmux

A brief example of how we can cheaply and easily run background processes on Linux using a terminal multiplexer. Create a new session with a name, start a process:

tmux new -s myproject-a
./myproject-a

To detach without closing the process, first press CTRL + b then press d. You can safely close the SSH connection and return at a later time. To reattach to the session at a later time, you can list all tmux sessions

tmux ls
myproject-b: 1 windows (created Thu Mar 10 14:58:06 2022)
myproject-a: 1 windows (created Thu Mar 10 14:56:18 2022)

and reattach yourself to see the logs of the process:

tmux attach -t myproject-a

For browsing/searching logs, we can set Tmux to vim mode via ~/.tmux.conf. Also set the history-limit as the default value is 2000 and possibly insufficient for your logs:

set-window-option -g mode-keys vi
set-option -g history-limit 10000

We can enter scroll mode with CTRL+b followed by [. J and K (capital with shift) for scrolling, / for search.

Auto restart on exit

To auto restart the process if it crashes or exits we can run it with a script:

#!/bin/sh
while true; do
    export $(grep -v '^#' .env | xargs) && ./myproject
    echo "Exited with code $?. respawning ..." >&2
    sleep 1
done

Exit this script with a double CTRL+c.

In Go, we can auto detect if the binary is updated after a Makefile run and restart ourselves with the autorestart package:

import "github.com/slayer/autorestart"

func main() {
    // ...
    stop := make(chan os.Signal, 1)
	signal.Notify(stop, os.Interrupt)

	go watcher()
    // run app main loop
	<-stop
    // exit handlers
}

func watcher() {
	autorestart.WatchPeriod = 3 * time.Second

	autorestart.RestartFunc = func() {
		if proc, err := os.FindProcess(os.Getpid()); err == nil {
			proc.Signal(syscall.SIGINT)
		}
	}

	restart := autorestart.GetNotifier()
	go func() {
		<-restart
		log.Printf("binary updated, sending interrupt ...")
	}()

	autorestart.StartWatcher()
}

This is also handy for local development and “live reload”. You can test this by finding your process with ps aux | grep <myprogram> and sending an interrupt with kill -SIGINT 1234 where 1234 is its PID.

Exposing Services

If you want to expose a service, see the snippet on how to expose multiple services behind an Nginx reverse proxy with authentication.


Last modified on Sunday, Mar 20, 2022.
Go back