Go apps compile to a single binary with no runtime dependencies, start in milliseconds, and handle thousands of requests per second with minimal memory. Fiber brings the ergonomics of Express to Go — fast routing, middleware, and JSON responses without the ceremony. Pairing it with a managed MySQL database should be straightforward: connect via DATABASE_URL, run migrations, done. But most platforms make you provision the database separately, copy credentials by hand, and restart the app to pick up the connection string.
PandaStack wires the database to the app automatically: create a MySQL instance, link it to your Fiber project, and DATABASE_URL appears in the environment before your app starts. This guide builds a Fiber API with database-backed routes, shows how to run migrations on deploy, and explains connection pooling so you don't exhaust the free tier's 50-connection limit.
The Go Fiber API
Create a new Go module:
mkdir fiber-mysql-demo && cd fiber-mysql-demo
go mod init github.com/yourname/fiber-mysql-demoInstall Fiber and a MySQL driver:
go get github.com/gofiber/fiber/v2
go get github.com/go-sql-driver/mysqlCreate main.go:
package main
import (
"database/sql"
"log"
"os"
"time"
"github.com/gofiber/fiber/v2"
_ "github.com/go-sql-driver/mysql"
)
var db *sql.DB
func main() {
var err error
dsn := os.Getenv("DATABASE_URL")
if dsn == "" {
log.Fatal("DATABASE_URL not set")
}
db, err = sql.Open("mysql", dsn)
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
defer db.Close()
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
app := fiber.New()
app.Get("/health", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"status": "ok"})
})
app.Get("/users", getUsers)
app.Post("/users", createUser)
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Fatal(app.Listen("0.0.0.0:" + port))
}
func getUsers(c *fiber.Ctx) error {
rows, err := db.Query("SELECT id, name, email FROM users")
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
defer rows.Close()
var users []fiber.Map
for rows.Next() {
var id int
var name, email string
if err := rows.Scan(&id, &name, &email); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
users = append(users, fiber.Map{"id": id, "name": name, "email": email})
}
return c.JSON(users)
}
func createUser(c *fiber.Ctx) error {
var user struct {
Name string `json:"name"`
Email string `json:"email"`
}
if err := c.BodyParser(&user); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Invalid request"})
}
result, err := db.Exec("INSERT INTO users (name, email) VALUES (?, ?)", user.Name, user.Email)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
id, _ := result.LastInsertId()
return c.Status(201).JSON(fiber.Map{"id": id, "name": user.Name, "email": user.Email})
}The critical bits:
DATABASE_URL: PandaStack injects this when you link a MySQL database. Format:user:password@tcp(host:port)/dbname?tls=true.- Connection pooling:
SetMaxOpenConns(10)limits concurrent connections. Free-tier databases allow 50 connections; if you deploy 5 instances of the app, each can safely open 10. - Port binding: Reads
PORTfrom the environment (defaults to8080in containers).
Create a migration script in migrate.sh:
#!/bin/bash
set -e
if [ -z "$DATABASE_URL" ]; then
echo "DATABASE_URL not set, skipping migration"
exit 0
fi
mysql "$DATABASE_URL" <<EOF
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
EOF
echo "Migration complete"Make it executable:
chmod +x migrate.shTest locally with a local MySQL instance or a temporary cloud database:
export DATABASE_URL="user:pass@tcp(localhost:3306)/testdb"
./migrate.sh
go run main.gocurl http://localhost:8080/health
curl -X POST http://localhost:8080/users -H "Content-Type: application/json" -d '{"name":"Alice","email":"alice@example.com"}'
curl http://localhost:8080/usersBoth routes should work. Push to GitHub.
Provision a managed MySQL database
Go to the PandaStack dashboard, navigate to Databases, and click New Database. Choose:
- Engine: MySQL 8.x
- Plan: Free (1 database allowed on the free tier)
- Name:
fiber-demo-db
PandaStack provisions a MySQL instance on Kubernetes via KubeBlocks, generates credentials, and shows the connection details. You'll see:
- Host (internal Kubernetes DNS name)
- Port (3306)
- Username / password
- Database name
Do not copy these manually. The next step wires them automatically.
Deploy the Fiber app and link the database
Create pandastack.json in the repo root:
{
"type": "container",
"name": "fiber-mysql-api",
"language": "go",
"buildCommand": "go build -o app",
"startCommand": "./app",
"healthCheckPath": "/health"
}Push to GitHub, then deploy via the dashboard:
git add .
git commit -m "Add Fiber API with MySQL"
git pushGo to https://dashboard.pandastack.io/deploy?repo=yourname/fiber-mysql-demo. The deploy screen auto-detects Go and fills in the build/start commands. Click Deploy.
The build runs, the container starts, and the app crashes with DATABASE_URL not set. This is expected — you haven't linked the database yet. In the project's Settings tab, find the Databases section and click Link Database. Select fiber-demo-db from the dropdown.
PandaStack injects DATABASE_URL as an environment variable and triggers a redeploy. The new container picks up the connection string, connects to MySQL, and the /health endpoint goes live.
Now run the migration. SSH into the running container (via the dashboard's Console tab) and execute:
./migrate.shOr, better, add a custom build step that runs migrations automatically. Update pandastack.json:
{
"type": "container",
"name": "fiber-mysql-api",
"language": "docker",
"dockerfilePath": "Dockerfile"
}Create a Dockerfile that builds the app and includes the migration script:
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o app
FROM alpine:3.19
RUN apk add --no-cache mysql-client
WORKDIR /app
COPY --from=builder /app/app .
COPY migrate.sh .
RUN chmod +x migrate.sh
# Run migration before starting the app
CMD ./migrate.sh && ./appCommit and push. The next deploy runs the migration before starting the app, so the users table is always created. Test it:
curl https://fiber-mysql-api-abc123.pandastack.io/users
# []
curl -X POST https://fiber-mysql-api-abc123.pandastack.io/users \
-H "Content-Type: application/json" \
-d '{"name":"Bob","email":"bob@example.com"}'
curl https://fiber-mysql-api-abc123.pandastack.io/users
# [{"id":1,"name":"Bob","email":"bob@example.com"}]The database is live, the API is live, and the connection string was injected automatically.
Deploy via the API with database linking
For CI pipelines, you can create the project and link the database in one API call. First, get the database ID from the dashboard or via:
curl -H "Authorization: Bearer psk_live_your_token_here" \
https://api.pandastack.io/v1/databasesCopy the id of your MySQL instance. Then create the project with the database linked:
curl -X POST https://api.pandastack.io/v1/projects \
-H "Authorization: Bearer psk_live_your_token_here" \
-H "Content-Type: application/json" \
-d '{
"slug": "container",
"name": "fiber-mysql-api",
"repositoryName": "yourname/fiber-mysql-demo",
"branch": "main",
"databaseId": "db-abc123",
"autoDeploy": true
}'The databaseId field links the database before the first deploy, so DATABASE_URL is set from the start. No manual linking step, no crash-on-boot.
Connection pooling and the 50-connection limit
The free-tier MySQL database allows 50 concurrent connections. If you deploy 3 instances of your app (via horizontal scaling) and each opens 20 connections, you've hit the limit and new connections fail. The SetMaxOpenConns(10) setting in the Go code prevents this by capping each app instance at 10 connections.
The paid plans increase the limit:
- Free: 50 connections
- Pro ($15/mo): 300 connections
- Premium ($25/mo): 1000 connections
For high-traffic APIs, upgrade the plan or add a connection pooler (PgBouncer for Postgres, ProxySQL for MySQL) between the app and the database. PandaStack's managed databases don't include a built-in pooler, so you'd deploy it as a separate container.
Backup and restore
The free tier includes 7 days of backup retention with daily automatic backups. To restore from a backup:
- 1Go to the database's Backups tab in the dashboard
- 2Select a backup and click Restore
- 3Choose in-place restore (overwrites the current data) or restore to a new database
Manual backups (via the Backup Now button) count against the same retention period. For longer retention or more frequent backups, upgrade to Pro (15 days) or Premium (30 days).
What breaks and how to fix it
Connection refused at runtime: The DATABASE_URL format is wrong. PandaStack injects user:password@tcp(host:port)/dbname?tls=true, but some MySQL drivers expect mysql://user:password@host:port/dbname. Check the driver's docs and parse accordingly.
Too many connections error: Your app opened more connections than the database allows. Lower SetMaxOpenConns or upgrade the plan. The free tier's 50-connection limit is shared across all apps connected to the database.
Migration runs on every deploy and fails on duplicate table: The migration script lacks CREATE TABLE IF NOT EXISTS. Without the IF NOT EXISTS clause, the script crashes on the second deploy. Use idempotent migrations (tools like golang-migrate or goose) for production.
App starts but queries fail with "table doesn't exist": The migration didn't run. If you're using the Dockerfile CMD approach, check the build logs to verify ./migrate.sh executed before ./app started. If the migration failed silently, the app starts anyway.
Choosing MySQL vs PostgreSQL
PandaStack offers both MySQL and PostgreSQL (versions 14 and 16 for Postgres, 8.x for MySQL). Choose based on your workload:
- MySQL: Better for read-heavy workloads, simpler replication, smaller storage footprint.
- PostgreSQL: Better for complex queries, JSON columns, full-text search, PostGIS for geo data.
Both are managed identically (same backup retention, same connection limits, same auto-wiring to apps). You can switch engines by provisioning a new database and migrating data via mysqldump or pg_dump.
Next steps
You've deployed a Go Fiber API with a managed MySQL database that wires itself to the app automatically. The same pattern works for any Go framework (Gin, Echo, Chi) and any language that reads DATABASE_URL from the environment.
For production apps, consider:
- Adding indexes to the
userstable for faster queries - Implementing a read replica for read-heavy workloads (available on paid plans)
- Setting up monitoring and alerts for slow queries via the database's Metrics tab
PandaStack handles the infrastructure; you write the SQL.
References
- [Go Fiber documentation](https://docs.gofiber.io/)
- [MySQL best practices](https://dev.mysql.com/doc/refman/8.0/en/optimization.html)
- [PandaStack database guide](https://docs.pandastack.io/databases)
- [Connection pooling in Go](https://www.alexedwards.net/blog/configuring-sqldb)