
In this article, I will be showing you how to deploy the GoTTH stack (Go Templ htmx tailwind) to production.
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:
-
An API that interacts with exchanges. Written in go and uses gin.
-
The Web UI 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 houses 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 its 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:
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 executables:
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 set up 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 set up certbot to have an SSL cert.
You can set 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:
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 build the WebUI, and then send 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.
Related Articles
Simple Rate Limiting in Go (Gin)
How to build a URL shortener in Go