Skip to Content

Deployment

Gofasta’s built-in deployment story targets one concrete environment: a Linux VPS running Docker or systemd, reached over SSH. The gofasta deploy command handles the full pipeline — build, ship, migrate, health-check, swap symlink, rollback — without requiring any cloud vendor account or third-party platform.

This page covers:

  • The fastest path: gofasta deploy end-to-end.
  • How the generated Dockerfile, systemd unit, nginx config, and CI workflows fit together.
  • Activating the CI workflows (they ship inert; you opt into the ones you want).

Cloud-managed deployment (ECS, Cloud Run, App Service, etc.) is not currently supported. See the whitepaper roadmap if that’s on the horizon for your project — until then, this page documents what’s actually shipped.

The short version

# One-time — prepare a fresh Ubuntu/Debian server. gofasta deploy setup --host deploy@api.example.com # Deploy the current working copy. gofasta deploy # Observe, debug, roll back. gofasta deploy status gofasta deploy logs gofasta deploy rollback

Configure the target in config.yaml:

deploy: host: deploy@api.example.com method: docker # "docker" (default) or "binary" port: 22 path: /opt/myapp arch: amd64 # "amd64" or "arm64" keep_releases: 3 # old releases retained for rollback

Full flag and subcommand reference: gofasta deploy CLI.

Deployment methods

gofasta deploy supports two methods. Both target the same VPS layout; only the packaging differs.

Docker method (default)

  1. Builds a Docker image locally using the project’s Dockerfile.
  2. Transfers it to the server via docker save | ssh docker load — no registry required.
  3. Copies .env, config.yaml, and the production compose.yaml into a timestamped release directory.
  4. Runs docker compose up -d on the server.
  5. Runs pending database migrations inside the container.
  6. Polls /health until the app responds.
  7. Atomically repoints the current symlink to the new release.
  8. Prunes releases older than keep_releases.

Best for: teams that want parity between local (gofasta dev --all-in-docker) and production, or that already use Docker Compose locally.

Binary method

  1. Cross-compiles a static binary with CGO_ENABLED=0 GOOS=linux.
  2. Transfers the binary, migrations, templates, and configs over SCP.
  3. Installs the binary to /usr/local/bin/<appname> and the systemd service unit.
  4. Runs pending migrations.
  5. Restarts the systemd service.
  6. Polls /health until the app responds.
  7. Atomically repoints current and prunes old releases.

Best for: teams that prefer no Docker daemon on the server, or deploy to a low-resource VPS where every MB matters.

Release directory layout

Both methods use the same Capistrano-style layout on the remote host:

/opt/myapp/ ├── current -> releases/20260417-153000/ # symlink flipped atomically ├── releases/ │ ├── 20260417-153000/ # active release │ └── 20260417-120000/ # previous (available for rollback) └── shared/ ├── .env # shared across all releases └── config.yaml

Each deploy creates a new timestamped directory. The current symlink is only flipped after the health check passes, so a failed deploy leaves the previous release serving traffic.

Deployment files in the scaffold

A gofasta new project ships deployment-adjacent files under deployments/ plus a Dockerfile and compose.yaml at the project root:

. ├── Dockerfile # multi-stage build for the `docker` deploy method ├── compose.yaml # local dev compose file └── deployments/ ├── ci/ # GitHub Actions workflow templates (opt-in — see below) │ ├── github-actions-test.yml │ ├── github-actions-release.yml │ └── github-actions-deploy-vps.yml ├── docker/ │ ├── compose.production.yaml # production compose file used by `gofasta deploy` │ └── dev.dockerfile # dev image with Air for hot reload ├── nginx/ │ └── app.conf # reverse-proxy snippet for TLS termination └── systemd/ ├── app.service # service unit for the `binary` deploy method └── deploy.sh # reference script if you want to deploy without the CLI

Everything under deployments/ is standard Go code the developer owns — feel free to edit, delete, or replace.

Dockerfile

The root Dockerfile is a two-stage build optimized for a minimal Alpine runtime image:

FROM golang:1.25.0-alpine AS builder WORKDIR /build COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app ./app/main FROM alpine:3.20 RUN apk add --no-cache ca-certificates tzdata COPY --from=builder /app /app COPY db/migrations /migrations COPY config.yaml /config.yaml COPY templates /templates ENV PORT=8080 EXPOSE 8080 ENTRYPOINT ["/app"] CMD ["serve"]

gofasta deploy --method docker invokes docker build against this file, then ships the resulting image to the server over SSH.

systemd (binary method)

The deployments/systemd/app.service unit file manages the binary as a long-running service:

[Unit] Description=MyApp API Server After=network.target postgresql.service [Service] Type=simple User=myapp Group=myapp WorkingDirectory=/opt/myapp/current ExecStart=/opt/myapp/current/myapp serve Restart=always RestartSec=5 Environment=APP_ENV=production EnvironmentFile=/opt/myapp/shared/.env [Install] WantedBy=multi-user.target

gofasta deploy setup installs this unit automatically. If you ever need to re-install or edit it:

sudo cp deployments/systemd/app.service /etc/systemd/system/myapp.service sudo systemctl daemon-reload sudo systemctl enable --now myapp sudo systemctl status myapp sudo journalctl -u myapp -f

nginx reverse proxy

deployments/nginx/app.conf is a reference reverse-proxy config with TLS termination:

upstream myapp { server 127.0.0.1:8080; } server { listen 80; server_name api.myapp.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name api.myapp.com; ssl_certificate /etc/letsencrypt/live/api.myapp.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.myapp.com/privkey.pem; location / { proxy_pass http://myapp; 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 /health { proxy_pass http://myapp; access_log off; } }

gofasta deploy setup installs nginx and copies this config into /etc/nginx/sites-available/. Point it at your domain, provision a Let’s Encrypt certificate with certbot --nginx -d api.myapp.com, and reload.

GitHub Actions workflows

How activation works

The scaffold ships three workflow templates under deployments/ci/not under .github/workflows/. That’s deliberate: GitHub Actions only picks up workflow files from .github/workflows/*.yml. Files anywhere else are inert.

The scaffold stores them as inert templates so a first git push doesn’t immediately fire a deploy with unset secrets. Each template’s top comment documents its required secrets. Read the header, configure the secrets under Settings → Secrets and variables → Actions, then copy the file in:

mkdir -p .github/workflows cp deployments/ci/github-actions-test.yml .github/workflows/test.yml git add .github/workflows/test.yml git commit -m "ci: enable test workflow" git push

Available templates

TemplatePurpose
github-actions-test.ymlRun go test -race on every push + PR against main, spin up a Postgres service container, upload coverage to Codecov. No secrets required unless you enable Codecov.
github-actions-release.ymlOn v* tag push, build a Docker image and push it to GHCR using the default GITHUB_TOKEN.
github-actions-deploy-vps.ymlOn push to main (or manual dispatch), run gofasta deploy against the configured VPS. Requires DEPLOY_HOST, DEPLOY_SSH_KEY, and DEPLOY_PORT secrets.

Example: test workflow

# .github/workflows/test.yml name: Test on: push: branches: [main] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest services: postgres: image: postgres:16-alpine env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: myapp_test ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: "1.25" - name: Run tests run: go test -race -coverprofile=coverage.out ./... env: GOFASTA_DATABASE_HOST: localhost GOFASTA_DATABASE_PORT: 5432 GOFASTA_DATABASE_USER: postgres GOFASTA_DATABASE_PASSWORD: postgres GOFASTA_DATABASE_NAME: myapp_test - uses: codecov/codecov-action@v4 with: file: coverage.out

Example: VPS deploy workflow

# .github/workflows/deploy.yml name: Deploy on: push: branches: [main] workflow_dispatch: jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: "1.25" - name: Install gofasta CLI run: go install github.com/gofastadev/cli/cmd/gofasta@latest - name: Configure SSH run: | mkdir -p ~/.ssh echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519 ssh-keyscan -p ${{ secrets.DEPLOY_PORT }} ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts - name: Deploy run: gofasta deploy --host ${{ secrets.DEPLOY_HOST }} --port ${{ secrets.DEPLOY_PORT }}

First-time server setup

gofasta deploy setup automates everything below on an Ubuntu/Debian VPS. Use this section as reference for what it’s doing under the hood or when running on a distribution it doesn’t cover.

# On the server (one-time) sudo apt update && sudo apt install -y curl nginx curl -fsSL https://get.docker.com | sudo sh # only if using docker method sudo useradd -r -s /bin/false myapp # only if using binary method sudo mkdir -p /opt/myapp/{releases,shared} sudo chown -R myapp:myapp /opt/myapp # Copy your .env and config.yaml into /opt/myapp/shared/ # Run `gofasta deploy` from your local machine.

Troubleshooting

SymptomLikely cause
deploy host is requireddeploy.host not set in config.yaml and --host not passed.
SSH connection refusedHost/port wrong, or your SSH key isn’t in the server’s ~/.ssh/authorized_keys. Test: ssh -p <port> user@server echo ok.
Health check failedServer started but the app didn’t come up on /health within the timeout. The previous release stays active — run gofasta deploy logs to inspect.
docker: command not found on remoteRun gofasta deploy setup first, or install Docker manually and re-deploy.
Workflow doesn’t run after git pushYou likely forgot to copy the file from deployments/ci/ into .github/workflows/. GitHub Actions only reads the latter.

Next Steps

Last updated on