I recently created my very own cryptocurrency exchange aggregator called cyphergoat it finds you the best rate to swap your crypto from different partnered exchanges.

It has two parts:

  1. An API that interacts with exchanges. Written in go and uses gin
  2. The Web UI that is written in go and uses a combination of HTML, HTMX, tailwindcss, CSS and Javascript in templ templates. Aka the GoTTH stack. It interacts with the api in order to find rates etc.

What is extremely cool with this stack and setup is that we are able to produce a single binary with everything included for each part and ship it to the server. On the webui side this is possible since the html is compiled into go code using templ and then shipped with the binary.

In this article I will be going through my setup to make it easier for you to make something like this.

Setup

I am using a debian 12 server which will expose my application via cloudflare tunnels. All of the static files are being served via nginx and the api and website binaries are ran as systemd services.

In this guide I will show you how I set this up.

The setup

I have a single folder on my dev machine called cyphergoat: It contains

api/
web/
builds/

The api folder houses the api source code. The web the website source code.

And the builds houses all of the builds that are deployed to the server.

Tailwind

The first real challenge comes with setting up tailwindcss correctly.

In my web project I have a static folder specifically for static files. Inside of it I have two files

/web
	styles.css
	tailwind.css

The styles.css simply contains:

@import "tailwindcss";

The tailwind.css file is where tailwind-cli will save it’s stuff.

To build the tailwind stuff I simply run

npx @tailwindcss/cli -i ./static/styles.css -o ./static/tailwind.css --watch

(assuming you have tailwind-cli installed)

In my header.templ file (the header of all the pages) at the top I have

<link href="/static/tailwind.css" rel="stylesheet">

<link href="/static/styles.css" rel="stylesheet">

And the files are being served using echo’s e.Static (in my main.go file )

func main(){
e := echo.New()

e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.Secure())


e.Static("/static", "static") // Serves content from static folder.

// Rest of the handlers
}

Server

On my server side I have a debian 12 vm running on proxmox.

In my users home directory I have a folder with the following contents:

cyphergoat/
├── api
├── static/
└── web

The static folder contains all of the static files (including tailwind.css and styles.css) and the web and api are the binaries.

I then have two systemd services for these exectuables:

The cg-api.service /etc/systemd/system/cg-api.service

[Unit]
Description=CypherGoat API
After=network.target

[Service]
User=arkal
Group=www-data
WorkingDirectory=/home/arkal/cyphergoat
ExecStart=/home/arkal/cyphergoat/api
Restart=always
RestartSec=1

[Install]
WantedBy=multi-user.target

And cg-web.service /etc/systemd/system/cg-web.service

[Unit]
Description=CypherGoat Web
After=network.target

[Service]
User=arkal
Group=www-data
WorkingDirectory=/home/arkal/cyphergoat
ExecStart=/home/arkal/cyphergoat/web


[Install]
WantedBy=multi-user.target

Both are owned by the group www-data (this is probably not necessary for the api), in order to make it easier to serve them via nginx.

Nginx

The website is communicating with the api but I still need to make the web-ui accessible.

I have setup an nginx site with the following configuration: /etc/nginx/sites-available/cg

server {
    server_name cyphergoat.com;

    location / {
        proxy_pass http://127.0.0.1:4200;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /static/ {
        alias /var/www/static/;
        expires 30d;
    }
    
    # Optional robots.txt
    location = /robots.txt {
        root /var/www/static;
        access_log off;
        log_not_found off;
    }

    listen 80;
}

I have also setup certbot to have an ssl cert.

You can setup certbot by running:

sudo apt install certbot python3-certbot-nginx -y

Generate the ssl cert:

sudo certbot --nginx -d cyphergoat.com

Read Self host your own website for a more in depth nginx setup.

Cloudflare Tunnels

I am currently making my website accessible using cloudflare pages. It is an extremely easy to use port forwarding solution

To do this you will need a cloudflare account and a domain pointed to cloudflare.

First head to the Zero Trust Dashboard

Under Networks click on Tunnels and then Create a tunnel

Once created you should Install and run a connector, follow the instructions on the page for your specific setup.

After the connector is running you should click on the Public Hostname tab and Add a public hostname.

Now you should see something like this: Cloudflare dashboard

Fill in the info as I have. The service type should be HTTP and the url should be 127.0.0.1:80 or localhost:80

Obviously there is no reason to make your api publicly accessible when deploying your website.

Deployment

In order to deploy my binaries I went ahead and created a quick bash script:

cd api
go build -o ../builds/ .

cd ../web

templ generate && go build -o ../builds/web cmd/main.go

cd ..
rsync -urvP ./builds/ user@SERVER:/home/user/cyphergoat
rsync -urvP ./web/static user@SERVER:/home/user/cyphergoat/
rsync -urvP ./api/coins.json user@SERVER:/user/user/cyphergoat/

The script will build the api, generate the templ files and build the webui and then sends everything over to my server (including the static folder)

I then ssh into my server

ssh user@ip

and then restart the services

sudo systemctl restart cg-api cg-web

And that’s it.

Join my free newsletter!

Subscribe

Where I share what I’ve been up to that week, including articles I’ve published, cool finds, tips and tricks, and more! Receive an email every time I post something new on my blog

No spam, no ads. Unsubscribe at any time.

Simple Rate Limiting in Go (Gin)

How to build a URL shortener in Go

How to deploy django to production