GoVueKit

Deploying a Go + Vue SaaS on one VPS with Traefik and Let's Encrypt

May 27, 2026

A SaaS that compiles into one binary does not need a deployment platform. It needs a server, a proxy that gets certificates, and a backup you have restored at least once.

Here is the complete setup, in the order you would actually do it.

What you are building

        Internet
           │  :80 / :443
      ┌────▼─────┐   Let's Encrypt certificates, HTTP→HTTPS
      │  Traefik │
      └────┬─────┘
           │  docker network "traefik"
      ┌────▼─────┐   one process: API + embedded Vue app + job worker
      │   app    │
      └────┬─────┘
           │
      ┌────▼─────┐   not exposed to the internet
      │ postgres │
      └──────────┘

Two containers you wrote nothing for, one that is your product. A 1 vCPU / 2 GB server runs this comfortably at the start.

Step 1: the server and DNS

Any provider works; for EU data residency, Hetzner, Scaleway and OVHcloud are the usual answers. Point an A record (and AAAA if you have IPv6) at the server, install Docker, and do the boring hardening: SSH keys only, automatic security updates, a firewall that allows 22, 80 and 443.

DNS first, because Let's Encrypt validates over HTTP and will fail politely until the record resolves.

Step 2: Traefik, once per host

docker network create traefik
ACME_EMAIL=you@example.com docker compose -f deploy/traefik/docker-compose.yml up -d

Traefik now owns ports 80 and 443, redirects HTTP to HTTPS, and requests certificates for any container that asks through labels. You do this once; a second application on the same host joins the same network.

Step 3: configure the app

cp .env.example .env

The variables that must be right in production:

Variable Value
APP_ENV production — turns on secure cookie flags
BASE_URL https://app.example.com, used in emails and OAuth callbacks
DOMAIN app.example.com, used by the Traefik labels
CSRF_KEY openssl rand -hex 32
POSTGRES_PASSWORD something random you never type again
DB_DRIVER postgres
MAILER resend (+ RESEND_API_KEY) or smtp
SUPER_ADMIN_EMAIL the address allowed into /admin
TRUSTED_PROXIES the docker network CIDR, so X-Forwarded-For is trusted only from Traefik

Three of them — APP_ENV, DB_DRIVER and TRUSTED_PROXIES — describe this stack's topology and are already set by docker-compose.prod.yml, which wins over .env. They are yours to fill in only if you deploy without that file.

make setup writes most of this interactively if you prefer answering questions to reading a table.

Step 4: ship it

docker compose -f docker-compose.prod.yml up -d --build

The multi-stage build compiles the Vue app, embeds it in the Go binary and produces a distroless image running as a non-root user. Migrations run at startup, so there is no separate migrate step to forget.

Two minutes later https://app.example.com answers with a valid certificate.

Step 5: the webhook and the mail domain

Two things are easy to postpone and painful to discover later.

Payments. Point the Stripe webhook endpoint at https://app.example.com/api/billing/webhook and put the signing secret in STRIPE_WEBHOOK_SECRET. Trigger a test event and check the log line.

Email deliverability. Publish SPF, add the DKIM records your provider generates, and start DMARC at p=none with a reporting address. Send from a subdomain (mail.example.com) so your root domain's reputation is insulated. Mail without DKIM goes to spam at the two providers your customers use.

Step 6: backups you have restored

The part that separates a hobby deployment from a business:

# nightly, to somewhere that is not this server
docker compose -f docker-compose.prod.yml exec -T db \
  pg_dump -U govuekit govuekit | gzip > /backups/db-$(date +%F).sql.gz
tar czf /backups/uploads-$(date +%F).tar.gz /var/lib/docker/volumes/<project>_uploads

Two volumes, not one. Blobs live outside the database, so a pg_dump alone restores accounts with broken avatars and missing attachments. If you set STORAGE=s3, the object store holds them instead and only the database is yours to back up.

Then do the thing nobody does: restore into a scratch container and sign in. An untested backup is a belief.

Step 7: updating

git pull
docker compose -f docker-compose.prod.yml up -d --build

Migrations apply on boot. Rollback is the previous image tag, or the previous binary if you deploy without Docker. Because the frontend is embedded, there is no window where a new API meets an old bundle — they ship as one artifact.

Without Docker, if you prefer

make build              # bin/govuekit, frontend inside
scp bin/govuekit server:/opt/myapp/

Then a systemd unit with Restart=always, an EnvironmentFile, and any reverse proxy in front — Caddy gets certificates with two lines, nginx needs certbot. The application does not care: it is one process listening on a port.

What to watch once it is live

That is the whole operational surface: one binary, one database, one proxy, two backups. If you want the cost side of the argument, read the rented stack bill.