GoToSocial Migration By Prompt

This serves as an example of how to use agentic AI to assist with all those projects you’ve been putting off. I captured my conversation history to show how it works, in case anyone out there in a development role still isn’t doing this every day.

The goal here was to migrate my private GoToSocial instance and a Postgresql database from my local network Unraid server to a VPS host that I’ve been renting for years but never used much.

I wrote the prompts, Gemini wrote the plans and scripts, and I ran the commands. This is Gemini 3.5 Flash (Medium) in Antigravity IDE.

I didn’t include every Gemini answer because that would have turned this into a novel. Imagine there was an exhaustive technical answer to every prompt.

Part 1: Feasibility

User Prompts

I want to make a plan to migrate a gotosocial instance and postgres server installed on my local Unraid server to an Internet virtual server of some kind. Can you give me some options? Is it even possible?
1 - I would like to use a low-cost, self-managed VPS, I may already have one actually 2 - it uses local storage I think, 3 - the script connected to the postgres database directly
Will I be able to migrate the existing gts.endgameviable.com domain to the new server so that federation continues to work seamlessly
I have a VPS plan at Vultr with these specs, will that be enough to run a low-traffic gotosocial and postgres containers? AMD High Performance
1 vCPU, 1024 MB RAM, 25 GB NVMe, 2.00 TB Transfer

Gemini’s Plan

GoToSocial & Postgres VPS Migration Plan

This plan details the migration of your GoToSocial instance (gts.endgameviable.com) from your local Unraid server to a self-managed, low-cost Linux VPS.


🏗️ Target Architecture (VPS)

For a self-managed VPS (e.g., Hetzner, DigitalOcean, Linode), we will run a clean Docker Compose stack that includes PostgreSQL, GoToSocial, and Caddy (which automatically handles Let’s Encrypt SSL certificates).

1. docker-compose.yml (On VPS)

Create a directory (e.g., ~/gotosocial) on the VPS and define the services:

services:
  postgres:
    image: postgres:15-alpine
    container_name: gts-postgres
    restart: always
    environment:
      POSTGRES_DB: gotosocial
      POSTGRES_USER: gotosocial
      POSTGRES_PASSWORD: "YOUR_SECURE_PASSWORD" # Match or update from Unraid
    volumes:
      - ./postgres_data:/var/lib/postgresql/data
    # Note: We do NOT expose port 5432 to the public internet for security.

  gotosocial:
    image: superseriousbusiness/gotosocial:latest
    container_name: gotosocial
    restart: always
    environment:
      GTS_HOST: gts.endgameviable.com
      GTS_DB_TYPE: postgres
      GTS_DB_ADDRESS: postgres
      GTS_DB_PORT: 5432
      GTS_DB_USER: gotosocial
      GTS_DB_PASSWORD: "YOUR_SECURE_PASSWORD"
      GTS_DB_DATABASE: gotosocial
      GTS_STORAGE_BACKEND: local
      GTS_STORAGE_LOCAL_DIR: /gotosocial/storage
      GTS_ACCOUNTS_REGISTRATION_OPEN: "false"
      GTS_PORT: 8080
    volumes:
      - ./gts_storage:/gotosocial/storage
    depends_on:
      - postgres

  caddy:
    image: caddy:2-alpine
    container_name: caddy-proxy
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - gotosocial

volumes:
  caddy_data:
  caddy_config:
2. Caddyfile (On VPS)

Caddy will serve as the reverse proxy and handle SSL handshakes automatically:

gts.endgameviable.com {
    reverse_proxy gotosocial:8080
}

🔄 Updating the Ingestion Script (gts-archiver)

Since your python scripts currently use psql to query tower.local directly, moving Postgres to the VPS will break this connection. Here are two ways to solve this:

Option A: Query via SSH Tunnel (Minimum Change)

To keep using SQL queries without exposing Postgres port 5432 to the public internet, you can create a secure SSH tunnel right before running the query.

  1. Establish a tunnel in your terminal:
    ssh -N -L 5432:localhost:5432 user@YOUR_VPS_IP
    
  2. Update the python scripts: Change the psql_command host target from tower.local to localhost:
    psql_command = [
        "psql",
        "-h", "localhost",  # Changed from tower.local
        "-d", "gotosocial",
        "-U", "gotosocial",
        "-c", sql,
        "-o", "output.csv",
        "--csv"
    ]
    

GoToSocial provides a Mastodon-compatible API. Rewriting the script to fetch posts via HTTP makes the database completely private, removes the need for SSH tunnels, removes hardcoded passwords, and allows you to automate the script (e.g. via GitHub Actions or AWS CodeBuild) from anywhere.

Here is how the query portion of fetchposts.py can be refactored using Python’s standard libraries:

import urllib.request
import json

# Your GoToSocial account ID
account_id = "redacted"
api_url = f"https://gts.endgameviable.com/api/v1/accounts/{account_id}/statuses?limit=40"

# Fetch statuses from the public API
req = urllib.request.Request(api_url, headers={'User-Agent': 'GTS-Archiver'})
try:
    with urllib.request.urlopen(req) as response:
        statuses = json.loads(response.read().decode())
except Exception as e:
    print(f"Error fetching from API: {e}")
    sys.exit(1)

# Filter and format statuses in-memory (replacing the SQL conditions)
result_rows = []
for status in statuses:
    created_at_dt = datetime.strptime(status["created_at"], "%Y-%m-%dT%H:%M:%S.%fZ")
    
    # Apply date filters (start/end)
    if not (yesterday <= created_at_dt < today):
        continue
        
    # Apply content filters
    if "https://gts.endgameviable.com/tags/notification" in status["content"]:
        continue
        
    # Map API response fields to match the rest of your script
    result_rows.append({
        "id": status["id"],
        "in_reply_to_id": status["in_reply_to_id"],
        "url": status["url"],
        "visibility": status["visibility"],
        "content_warning": status["spoiler_text"],
        "content": status["content"],
        "created_at": status["created_at"],
        "dayname": created_at_dt.strftime("%A"),
        "day": created_at_dt.strftime("%m/%d"),
        "hour": created_at_dt.strftime("%H:%M")
    })

🗺️ Step-by-Step Migration Blueprint

Follow these steps to perform the migration:

Phase 1: Prepare the VPS
  1. Provision VPS: Deploy an Ubuntu instance and install Docker.
  2. Configure Security Rules: Allow incoming traffic on ports 80, 443 (for web/SSL), and 22 (SSH).
  3. Write Configuration Files: Set up the docker-compose.yml and Caddyfile on the VPS as shown above. Do not run docker compose up yet.
Phase 2: Lower DNS TTL (24 hours prior)
  1. Go to AWS Route 53.
  2. Select the hosted zone for endgameviable.com.
  3. Locate the A record for gts.endgameviable.com and set the TTL to 300 seconds (5 minutes) so the IP switch takes effect quickly.
Phase 3: Backup and Export (On Unraid)
  1. Stop GoToSocial: Shut down the GoToSocial container in Unraid to prevent new database entries or files during migration.
  2. Dump PostgreSQL Database: Run pg_dump on your Unraid Postgres container:
    docker exec -t [postgres-container-name] pg_dump -U gotosocial -F c -d gotosocial > gotosocial_backup.dump
    
  3. Archive Storage Directory: Compress your GoToSocial local media/appdata storage:
    tar -czvf gotosocial_storage.tar.gz -C /mnt/user/appdata/gotosocial/ storage/
    
Phase 4: Transfer and Restore (On VPS)
  1. Transfer Files: Securely copy the backup files to your new VPS:
    scp gotosocial_backup.dump gotosocial_storage.tar.gz user@your-vps-ip:~/gotosocial/
    
  2. Restore Media Storage: Extract the storage archive on the VPS:
    tar -xzvf ~/gotosocial/gotosocial_storage.tar.gz -C ~/gotosocial/
    # This creates ~/gotosocial/storage/ which maps to `./gts_storage`
    mv ~/gotosocial/storage ~/gotosocial/gts_storage
    
  3. Restore Database:
    • Start only the Postgres container on the VPS:
      docker compose up -d postgres
      
    • Restore the database dump:
      docker exec -i gts-postgres pg_restore -U gotosocial -C -d gotosocial < ~/gotosocial/gotosocial_backup.dump
      
Phase 5: Point DNS and Start
  1. Update Route 53: Update the A record for gts.endgameviable.com to point to the VPS’s public IP address.
  2. Start All Services:
    docker compose up -d
    
  3. Verify:
    • Run docker compose logs caddy and verify it successfully obtained SSL certificates for gts.endgameviable.com.
    • Open https://gts.endgameviable.com in your browser, log in, and make a test post.
    • Search for your test post from an external Mastodon account to ensure bidirectional federation is functioning.
  4. Restore TTL: Change the Route 53 A record TTL back to 3600 (1 hour).
  5. Update local script: Apply either Option A (SSH tunnel) or Option B (API-based query) to your local gts-archiver scripts.

Part 2: Local Test

User Prompts

I've created a new directory gts-orchestration and would like to use that directory to setup a local test instance of the gotosocial stack
Is there a way to export Unraid docker container vars or do I have to type them by hand from the UI?
Here are my existing gotosocial vars, update the plan to use the same vars

oot@Tower:~# docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' gotosocial
GTS_ACCOUNTS_REGISTRATION_OPEN=false
GTS_STATUSES_MAX_CHARS=2000
GTS_LETSENCRYPT_EMAIL_ADDRESS=[REDACTED_EMAIL]
GTS_DB_TYPE=postgres
HOST_HOSTNAME=Tower
GTS_HOST=gts.endgameviable.com
GTS_DB_USER=gotosocial
GTS_DB_PASSWORD=[REDACTED_PASSWORD]
GTS_LANDING_PAGE_USER=ultrviolet
GTS_INSTANCE_EXPOSE_PUBLIC_TIMELINE=true
HOST_OS=Unraid
HOST_CONTAINERNAME=gotosocial
GTS_DB_ADDRESS=[REDACTED_IP]
GTS_LETSENCRYPT_ENABLED=false
GTS_LOG_LEVEL=debug
TZ=America/New_York
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Update the plan to configure env vars for docker compose for postgres password and GTS_HOST
Update the plan so the postgres password is only in the .env file
Update the plan to remove "ultrviolet" from docker-compose.yml and .env.sample
Can you update the plan to add Makefile targets to add and remove gts.endgameviable.com to /etc/hosts - is that possible?
I have docker desktop installed but I get this

tkrehbiel@MacBook-Pro gts-orchestration % make up
docker compose up -d
make: docker: No such file or directory
make: *** [up] Error 1
It seems like it's already installed... it doesn't let me apply
Now I get this

tkrehbiel@MacBook-Pro gts-orchestration % make up
/usr/local/bin/docker compose up -d
[+] up 0/3
 ⠋ Image postgres:15-alpine                     Pulling                                                    0.0s
 ⠋ Image superseriousbusiness/gotosocial:latest Pulling                                                    0.0s
 ⠋ Image caddy:2-alpine                         Pulling                                                    0.0s
error getting credentials - err: exec: "docker-credential-desktop": executable file not found in $PATH, out: ``
make: *** [up] Error 1
It started up, now what?
Can I open anything in a browser without setting up gts.endagmeviable.com?
For now I don't want to restore a database, I just want to see if it runs before I work on migrating data
Okay it works, now I want to start making a plan to migrate data from the Unraid postgres database. How do I export data from postgres?
I've never ssh'ed to the unraid server, is it just "ssh tower.local"?
tkrehbiel@MacBook-Pro gts-orchestration % ssh root@tower.local
ssh: connect to host tower.local port 22: Connection refused
I don't see appdata in the list of shares.. it's got a warning icon next to it in the unraid UI
The postgres backup only has 54 bytes, I don't think it worked
pg_dump: error: invalid output format "-c" specified
I've imported the backup files into the project dir, make a plan to import them into my local test setup
I got these warnings

root@Tower:~# docker exec -i postgresql15 pg_dump -U gotosocial -F c -d gotosocial > /mnt/user/LocalBackups/gotosocial_backup.dumpWARNING:  database "gotosocial" has a collation version mismatch
DETAIL:  The database was created using collation version 2.36, but the operating system provides version 2.41.
HINT:  Rebuild all objects in this database that use the default collation and run ALTER DATABASE gotosocial REFRESH COLLATION VERSION, or build PostgreSQL with the right library version.
I updated the dump file, continue the plan
The site said this, how do I see logs

An error occured:
Internal Server Error
Request ID: 000037ya5a58p0000010
I see this warning at the top of the gts page

Warning! It looks like either trusted-proxies is not set correctly in this instance's configuration, or the upstream reverse proxy is not passing an appropriate X-Forwarded-For or X-Real-IP header.

This may cause rate-limiting issues and, by extension, federation issues.

If you are the instance admin, you may be able to fix this by adding [REDACTED_IP]/16 to your trusted-proxies.

For more information and other possible fixes for this issue, see the documentation.
So that was an issue in the local environment only, and wouldn't have happened in a live environment?

Gemini’s Plan

Implementation Plan - Import Unraid Backups into Local Test Setup

This plan details the steps to import your Unraid PostgreSQL dump (gotosocial_backup.dump) and GoToSocial media storage (gotosocial_storage.tar.gz) into the local docker-compose environment inside gts-orchestration.

Because the restored database contains internal ActivityPub links bound to gts.endgameviable.com, we must revert the local configuration from localhost back to gts.endgameviable.com and use the local Caddy reverse proxy with hosts-file mapping.


Proposed Configuration Changes
1. Update .env

We must change GTS_HOST back to the production domain so that GoToSocial matches the restored database records:

POSTGRES_PASSWORD=[REDACTED_PASSWORD]
GTS_HOST=gts.endgameviable.com
GTS_STATUSES_MAX_CHARS=2000
GTS_LANDING_PAGE_USER=ultrviolet
GTS_INSTANCE_EXPOSE_PUBLIC_TIMELINE=true
GTS_LOG_LEVEL=debug
TZ=America/New_York

Import Execution Steps

Follow these steps in your terminal to clear the dummy local data and import your Unraid production data:

Step 1: Wipe Clean and Restart the Stack

We must blow away the temporary local database and media storage to avoid any conflicts:

# Stop containers and delete local docker volumes
make clean

Now, start the stack back up. This will pull in the updated .env configuration and initialize a fresh, empty PostgreSQL database:

make up
Step 2: Extract Media Storage

Extract the compressed media files and place them in the local folder mapped by docker-compose (gts_storage):

# Extract the tarball (creates a "storage" directory)
tar -xzvf gotosocial_storage.tar.gz

# Rename the folder to match the docker volume mount path
mv storage gts_storage
Step 3: Restore the PostgreSQL Database

Restore the custom-format database dump using the make restore-db helper:

make restore-db DUMP=gotosocial_backup.dump

[!NOTE] Since we ran make clean first, this will restore to a clean database without conflicts.

Step 4: Configure Local DNS

Route requests for gts.endgameviable.com to your local machine:

make hosts-add

Verification Plan
  1. Open your browser and navigate to: https://gts.endgameviable.com
  2. Proceed past the browser’s local HTTPS/self-signed certificate warning.
  3. Verify that your posts, profiles, and media attachments load correctly (indicating that the database and files were successfully restored).
  4. Run make logs to verify that GoToSocial is not printing any database connection or filesystem access errors.

Part 3: VPS Deployment & Verification

User Prompts

Now that I've tested locally, how can I install this setup on my Vultr VPS server?
Make a plan, don't change things
Vult instance os is Ubuntu 22.04 x64
I currently have gts.endgameviable.com pointed to a CloudFront distribution... should I continue to use the CloudFront distribution and point it to the Vultr server? Or is that a waste?
Is there any risk of breaking federation during the time period when I switch gts.endgameviable.com but haven't yet started the gotosocial server?
If a server is declared dead in federation, and it comes back months later, will federation resume or is it dead forever?
How do I verify that the domain name is pointing to the Vultr server if there's no web server on the VPS server
tkrehbiel@MacBook-Pro gts-orchestration % tar -czvf gts-deploy.tar.gz docker-compose.prod.yml Caddyfile.prod .e
nv gotosocial_backup.dump gotosocial_storage.tar.gz
tar: docker-compose.prod.yml: Cannot stat: No such file or directory
tar: Caddyfile.prod: Cannot stat: No such file or directory
a .env
a gotosocial_backup.dump
a gotosocial_storage.tar.gz
tar: Error exit delayed from previous errors.
What next?
I've just updated the OS on the VPS to ubuntu 24.04
As part of the plan, create a shell script to run all the necessary ubuntu setup from a fresh os install, and make it idempotent
The setup script should also setup starting docker compose on rebooting
Add a make target to package files for transfer
I packaged the files and scp'ed them to the VPS, now what?
Is this an issue?

root@development:~# tar -xzf /tmp/gts-deploy.tar.gz -C ~/gts-orchestration
tar: Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.com.apple.provenance'
tar: Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.com.apple.provenance'
tar: Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.com.apple.provenance'
tar: Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.com.apple.provenance'
tar: Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.com.apple.provenance'
tar: Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.com.apple.provenance'
I got a ton of errors restoring the pg backup

root@development:~/gts-orchestration# docker compose exec -T postgres pg_restore -U gotosocial -d gotosocial --clean < gotosocial_backup.dump
pg_restore: error: could not execute query: ERROR:  index "web_push_subscriptions_account_id_idx" does not exist
(snip)
Command was: DROP TABLE public.account_notes;
pg_restore: warning: errors ignored on restore: 256
I don't see a page loading at gts.endgameviable.com yet
root@development:~/gts-orchestration# docker compose ps
NAME           IMAGE                                    COMMAND                  SERVICE      CREATED              STATUS              PORTS
caddy          caddy:2-alpine                           "caddy run --config …"   caddy        About a minute ago   Up About a minute   0.0.0.0:80->80/tcp, [::]:80->80/tcp, 0.0.0.0:443->443/tcp, [::]:443->443/tcp, 443/udp, 2019/tcp
gotosocial     superseriousbusiness/gotosocial:latest   "/gotosocial/gotosoc…"   gotosocial   About a minute ago   Up About a minute   
gts-postgres   postgres:15-alpine                       "docker-entrypoint.s…"   postgres     About a minute ago   Up About a minute   5432/tcp
root@development:~/gts-orchestration# docker compose logs caddy
caddy  | {"level":"info","ts":1785877892.8641872,"msg":"maxprocs: Leaving GOMAXPROCS=1: CPU quota undefined"}
(snip)
caddy  | {"level":"info","ts":1785877898.657089,"logger":"tls.obtain","msg":"releasing lock","identifier":"gts.endgameviable.com"}
The gotosocial log is enormous but I don't see any errors
root@development:~/gts-orchestration# curl -Iv https://gts.endgameviable.com
* Host gts.endgameviable.com:443 was resolved.
(snip)
< 
* Connection #0 to host gts.endgameviable.com left intact
https://gts.endgameviable.com/@ultrviolet seems to work though
Chrome is saying my connection to gts.endgamevaible.com is not secure

(Those connectivity problems turned out to be a problem with Chrome caching. The server itself worked on the first try.)

Gemini’s Plan

Deploy GotoSocial to Vultr VPS (Ubuntu 24.04 x64)

Outline the steps, configuration changes, and verification required to migrate the local GotoSocial, Postgres, and Caddy orchestration setup to a remote Vultr virtual private server (VPS) running Ubuntu 24.04 x64.

User Review Required

[!IMPORTANT] Postgres and Port Security: In the local development docker-compose.yml, the PostgreSQL port 5432 and GotoSocial port 8080 are exposed to the host machine. In production, we must disable these mappings so they are only accessible through the internal Docker bridge network and not reachable from the public internet. Caddy will be the sole public gateway on ports 80 and 443.

[!WARNING] SSL Certificate Transition: The local setup uses tls internal in the Caddyfile. For production, this directive must be removed so Caddy can automatically request and manage Let’s Encrypt or ZeroSSL certificates for gts.endgameviable.com.

Open Questions

[!IMPORTANT]

  • Domain DNS: Has gts.endgameviable.com been pointed to your Vultr VPS’s public IP address yet? This is required before Caddy can successfully provision its SSL certificate.
  • Docker Status: Are Docker and Docker Compose already installed on your VPS, or do they need to be installed as part of this process? (Installation steps for Ubuntu 22.04 are provided below if needed).

Proposed Changes
Docker Orchestration Setup
[MODIFY] Makefile
  • Add target pack-deploy to package production settings, DB dump, storage archive, and setup script into a single gts-deploy.tar.gz archive.
[NEW] docker-compose.prod.yml
  • Security hardening: Remove exposed ports for postgres (5432) and gotosocial (8080).
  • Use production-appropriate container names (gts-postgres, gotosocial, caddy) instead of -local suffixes.
  • Adjust GTS_LOG_LEVEL to warn to avoid disk space bloat.
[NEW] Caddyfile.prod
  • Remove tls internal to enable Let’s Encrypt / ZeroSSL certificate management automatically.
[NEW] setup_vps.sh
  • Idempotent Bash script to automate UFW firewall, package updates, Docker installation, folder creation, and enabling Docker and Containerd on boot (supporting the compose container restart policies on system reboot).

Step-by-Step Deployment Guide

Here is the plan for how you will run the installation on your VPS:

Step 1: Package and Transfer Local Setup
  1. On your local machine, run the following command in the orchestration directory to pack the deployment files:
    tar -czvf gts-deploy.tar.gz docker-compose.prod.yml Caddyfile.prod setup_vps.sh .env gotosocial_backup.dump gotosocial_storage.tar.gz
    
    This generates gts-deploy.tar.gz containing the production settings and setup script.
  2. Transfer this archive to your Vultr VPS (replace USER and VPS_IP with your actual VPS ssh details):
    scp gts-deploy.tar.gz USER@VPS_IP:/tmp/
    
Step 2: Set up and Restore on the VPS
  1. SSH into your VPS:
    ssh USER@VPS_IP
    
  2. Create the orchestration directory and extract the files:
    mkdir -p ~/gts-orchestration
    tar -xzf /tmp/gts-deploy.tar.gz -C ~/gts-orchestration
    cd ~/gts-orchestration
    
  3. Run the idempotent setup script to prepare the OS, firewall, Docker, and directories:
    chmod +x setup_vps.sh
    sudo ./setup_vps.sh
    
  4. Prepare configuration filenames for docker compose:
    mv docker-compose.prod.yml docker-compose.yml
    mv Caddyfile.prod Caddyfile
    
  5. Extract the existing storage backup into the gts_storage directory:
    tar -xzf gotosocial_storage.tar.gz -C gts_storage/ --strip-components=1
    
  6. Start the PostgreSQL service first to restore the database:
    docker compose up -d postgres
    
  7. Restore the PostgreSQL database dump:
    # Wait a few seconds for postgres to be ready, then:
    docker compose exec -T postgres pg_restore -U gotosocial -d gotosocial --clean < gotosocial_backup.dump
    
  8. Start the rest of the services (GotoSocial and Caddy):
    docker compose up -d
    

Verification Plan
Automated Tests
  • Check status of all containers:
    docker compose ps
    
  • Check logs for Caddy to ensure SSL certificates were obtained:
    docker compose logs caddy
    
  • Check logs for GotoSocial to ensure database connection and start:
    docker compose logs gotosocial
    
Manual Verification
  • Open https://gts.endgameviable.com in a browser and verify that the instance is loading with a valid Let’s Encrypt / ZeroSSL HTTPS certificate.
  • Verify that your user accounts and posts are fully restored and media images display correctly.

And that was it. Easy peasy.

P.S. I painstakingly cut and paste those prompts and plans into a document, and only then did I realize I could just ask Gemini to read its conversation history and create the document for me in seconds. Live and learn.

Leave a Comment

This is a homegrown DIY comment system I've been working on. It works but technically you're beta testing it. Good luck. Go here to enter a comment on this post without Javascript.