nox.im · All Snippets · All in Ubuntu · All in Server
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
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
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.'"
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
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
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.
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.
If you want to expose a service, see the snippet on how to expose multiple services behind an Nginx reverse proxy with authentication.