In this article, we will show you how to deploy the GoTTH stack (Go, Templ, HTMX, Tailwind) in production.
Recently, we developed an aggregation platform for cryptocurrency exchange that finds the best exchange rates among different partner providers.
The project consists of two parts:
An API written in Go using the Gin framework to interact with the exchanges.
A web interface developed in Go that uses HTML, HTMX, TailwindCSS, CSS and JavaScript in Templ templates, which makes up the GoTTH stack.
One of the main advantages of this setup is that we can generate a single compiled binary with all the necessary elements for each part of the system and easily deploy it on the server. In the case of the web interface, this is possible because the HTML templates are converted into Go code and integrated into the final binary.
In this article, we detail our configuration to facilitate its implementation.
Server Configuration
We use a Debian 12 server to host our application, which is exposed through Cloudflare Tunnels. Static files are served via Nginx and both the API and the web application run as services managed by systemd.
The project structure in the development environment is as follows:
api/
web/
builds/
The `api/` directory contains the API source code, `web/` houses the web interface source code, and `builds/` stores the binaries ready for deployment on the server.
Tailwind Configuration
To manage TailwindCSS, we have a `static/` folder inside the `web/` directory, where two files are located:
/web/static/
styles.css
tailwind.css
The `styles.css` file simply imports TailwindCSS:
@import 'tailwindcss';
While `tailwind.css` is generated by TailwindCLI using the following command:
npx @tailwindcss/cli -i ./static/styles.css -o ./static/tailwind.css --watch
Both files are referenced in the main HTML template using:
To serve these static files on the server, we use Echo in our `main.go`:
func main(){
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.Secure())
e.Static('/static', 'static') // Serve static content.
// Rest of handlers
}
Service Configuration on the Server
On the server, we have a folder structure similar to:
cyphergoat/
+-- api
+-- static/
+-- web
The `api` and `web` binaries are managed by systemd through the following services:
cg-api.service:
[Unit]
Description=CypherGoat API
After=network.target
[Service]
User=user
Group=www-data
WorkingDirectory=/home/user/cyphergoat
ExecStart=/home/user/cyphergoat/api
Restart=always
RestartSec=1
[Install]
WantedBy=multi-user.target
cg-web.service:
[Unit]
Description=CypherGoat Web Interface
After=network.target
[Service]
User=user
Group=www-data
WorkingDirectory=/home/user/cyphergoat
ExecStart=/home/user/cyphergoat/web
[Install]
WantedBy=multi-user.target
Nginx Configuration
To serve the user interface via Nginx, we use the following configuration:
server {
server_name domain.com;
location / {
proxy_pass https://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;
}
listen 80;
}
To enable HTTPS, we use Certbot with:
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d domain.com
Deployment
To automate the generation and transfer of binaries, we created a 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:/home/user/cyphergoat/
Then, we access the server and restart the services:
ssh user@ip
sudo systemctl restart cg-api cg-web
At Q2BSTUDIO we specialize in the development and deployment of technological solutions, offering custom services to implement efficient and scalable infrastructures. If you are looking for experts in modern application development and deployment, contact us.





