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: <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 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. Join my free newsletter Related Articles Simple Rate Limiting in Go (Gin) How to build a URL shortener in Go How to deploy Django to production © 2025 4rkal CC BY-SA SUBSCRIBE 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. cyphergoat 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. An API that interacts with exchanges. Written in go and uses gin. 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. 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. a single 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/ 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 /web styles.css tailwind.css The styles.css simply contains: styles.css @import "tailwindcss"; @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 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"> <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 } 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 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. web API I then have two systemd services for these executables: The cg-api.service /etc/systemd/system/cg-api.service 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 [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 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 [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. www-data 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 /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; } 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 sudo apt install certbot python3-certbot-nginx -y Generate the SSL cert: sudo certbot --nginx -d cyphergoat.com sudo certbot --nginx -d cyphergoat.com Read Self host your own website for a more in-depth nginx setup. Self host your own website 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 . Zero Trust Dashboard Under Networks click on Tunnels, and then Create a tunnel. Networks Tunnels, Create a tunnel. Once created, you should Install and run a connector ; follow the instructions on the page for your specific setup. Install and run a connector After the connector is running, you should click on the Public Hostname tab and Add a public hostname . Public Hostname 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. HTTP 127.0.0.1:80 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/ 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 ssh user@ip And then restart the services. sudo systemctl restart cg-api cg-web sudo systemctl restart cg-api cg-web And that’s it. Join my free newsletter Join my free newsletter Related Articles Simple Rate Limiting in Go (Gin) Simple Rate Limiting in Go (Gin) How to build a URL shortener in Go How to build a URL shortener in Go How to deploy Django to production How to deploy Django to production © 2025 4rkal CC BY-SA SUBSCRIBE 4rkal CC BY-SA SUBSCRIBE