Database Migrations in Postgres Using Goose

How we manage database migrations and seed sample data in a PostgreSQL using Goose.

By Aprim Regmi -

Database Migrations

What are database migrations?

Database migrations are like Git commits, but for database structure, simply versioned SQL file that tracks the change to database schema. Instead of manually running SQL queries or altering tables directly on a production server which is risky and hard to track, migrations let us control every change to our schema. We can run migrations to make our database schema up to date and rollback to safe state if something goes wrong.

Why Goose?

goose

There are several tools available for database migrations, but Goose is one of the most popular, lightweight, and flexible database migration tools. It allows us to write migrations in raw SQL (or even native Go code) and can be used as a CLI tool during development or run directly inside your go server.

Managing database migrations using Goose

Let's walk through how to set up Goose and run first database migration with PostgreSQL.

1. Installing Goose

First, let's install the Goose CLI globally on our machine:

go install github.com/pressly/goose/v3/cmd/goose@latest

2. Setup Env

Before we write migrations we need to setup some environment variables

.env

GOOSE_DRIVER=postgres
GOOSE_DBSTRING="host=localhost user=postgres password=secret dbname=mydb sslmode=disable"
GOOSE_MIGRATION_DIR=db/migrations

3. Creating your first migration

Create a folder for your migrations (db/migrations). Then, use the goose create command to generate a new SQL migration file:

goose create create_users_table sql

This will generate a sql file inside your folder, such as 20260824120000_create_users_table.sql.

4. Writing the migration SQL

Open your migration file. Goose uses special comment (-- +goose Up and -- +goose Down) to split your file into up migration and rollbacks:

20260824120000_create_users_table.sql

-- +goose Up
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL,
    password TEXT,
    created_at TIMESTAMP DEFAULT now()
);

-- +goose Down
DROP TABLE IF EXISTS users;

Up: Applied when moving your database structure forward (e.g., creating tables).

Down: Applied if something went wrong and we ever need to undo or roll back the change.

5. Running migrations

To apply our migrations to the PostgreSQL database, run the following command:

goose up

Under the hood, Goose automatically creates a tracking table named goose_db_version in our database. This table keeps track of which migration files have already been run.

We can also check our migration status anytime by running:

goose status

Output:

Applied At                  Migration
=======================================
Mon Aug 24 10:19:32 2026 -- 20260823162916_create_users_table.sql
Mon Aug 24 10:24:17 2026 -- 20260824102030_create_admin_table.sql
Mon Aug 24 10:27:36 2026 -- 20260824102636_update_admin_table.sql
Mon Aug 24 10:33:43 2026 -- 20260824103048_alter_user_table.sql
Mon Aug 24 11:18:32 2026 -- 20260824103406_alter_admin_table.sql

6. Rollbacks

If anything went wrong and we need to roll back to the previous db state, we can run this command:

goose down

Or, if we want to go back in specific point in history, we can use this command:

goose down-to <version>

This will start rolling back the database from the latest to the desired version. We can make some changes and migrate up the database schema.

Data Seeding

While migrations handle database schema, we often need an automated way to insert default or demo data for development, testing or demo. That's where data seeding comes in.

Data seeding is the process of populating a database with initial sample data and Goose helps us handle this easily. Since, Goose migration files are just sql files we can use them to seed our database with default values by writing INSERT queries.

Create a migration file for seeding users table.

goose create seed_users sql

Now write a query to seed users table:

20260825104622_seed_users.sql

-- +goose Up
INSERT INTO users (name, email, password) VALUES
('John', 'john@gmail.com', 'john123'),
('Alice', 'alice@gmail.com', 'Alice123'),
('Jack', 'jack@gmail.com', 'jack123');

-- +goose Down
DELETE FROM users WHERE email IN ('john@gmail.com', 'alice@gmail.com', 'jack@gmail.com');

Never store plain text password in a real app. Always hash sensitive data before storing them in the database - this is just for demo purposes.

Now we can simply do goose up to seed our table with default test data.

Conclusion

And that's it! We've just implemented database migrations and seeded initial sample data cleanly using Goose.