-- Leo's gemini proxy

-- Connecting to nox.im:1965...

-- Connected

-- Sending request

-- Meta line: 20 text/gemini; charset=utf-8

Ubuntu Deploy


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


1: Vultr


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[1] 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:


1: 12 factor


#!/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[1] package:


1: autorestart


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[1] with authentication.


1: how to expose multiple services behind an Nginx reverse proxy


-- Response ended

-- Page fetched on Fri May 10 03:07:58 2024