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 deployend-to-end. - How the generated
Dockerfile,systemdunit,nginxconfig, 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 rollbackConfigure 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 rollbackFull 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)
- Builds a Docker image locally using the project’s
Dockerfile. - Transfers it to the server via
docker save | ssh docker load— no registry required. - Copies
.env,config.yaml, and the productioncompose.yamlinto a timestamped release directory. - Runs
docker compose up -don the server. - Runs pending database migrations inside the container.
- Polls
/healthuntil the app responds. - Atomically repoints the
currentsymlink to the new release. - 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
- Cross-compiles a static binary with
CGO_ENABLED=0 GOOS=linux. - Transfers the binary, migrations, templates, and configs over SCP.
- Installs the binary to
/usr/local/bin/<appname>and the systemd service unit. - Runs pending migrations.
- Restarts the systemd service.
- Polls
/healthuntil the app responds. - Atomically repoints
currentand 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.yamlEach 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 CLIEverything 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.targetgofasta 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 -fnginx 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 pushAvailable templates
| Template | Purpose |
|---|---|
github-actions-test.yml | Run 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.yml | On v* tag push, build a Docker image and push it to GHCR using the default GITHUB_TOKEN. |
github-actions-deploy-vps.yml | On 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.outExample: 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
| Symptom | Likely cause |
|---|---|
deploy host is required | deploy.host not set in config.yaml and --host not passed. |
| SSH connection refused | Host/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 failed | Server 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 remote | Run gofasta deploy setup first, or install Docker manually and re-deploy. |
Workflow doesn’t run after git push | You likely forgot to copy the file from deployments/ci/ into .github/workflows/. GitHub Actions only reads the latter. |
Next Steps
- gofasta deploy CLI reference — every flag and subcommand.
- Configure your application — what goes in
config.yamlvs.env. - Health check API reference — liveness and readiness endpoints.
- Observability API reference — metrics and tracing.