My Redmine was old. Not “a bit behind” old — Redmine 3.3.2, installed in 2017, running on Amazon Linux AMI 2018.03, an operating system that reached End of Life at the end of 2023. The EC2 instance itself was launched in 2015. Every SSH login greeted me with a nice ASCII-art banner warning that the OS had been dead for almost three years.
I kept postponing the upgrade because everyone who has ever touched Redmine knows the truth: upgrading across major versions is where weekends go to die. Ruby version conflicts, gem dependency hell, database migrations failing halfway, plugins that no longer exist. And this box also ran a forgotten PHP site, two Bitbucket-mirroring cronjobs, and a nightly backup to S3 that — luckily — actually worked.
So I asked an AI agent to do it.
The audit (that I didn’t have to do)
Instead of writing a plan from assumptions, the agent SSH’d into the box and did a real inventory. Within minutes it reported things I’d half-forgotten:
- MySQL 5.5.62 — a database version old enough to attend school
- Ruby 2.3.4 via rbenv, Thin app server behind Apache
- ~90 MB database: 8,664 issues, 21 projects, 61 users, 10,426 attachments
- Over a gigabyte of bare git repos synced from Bitbucket every 5 minutes
- A free plugin long abandoned
- Nightly mysqldump + tarball to S3 with 6-day retention
- And a surprise: the box also hosted a second, obsolete site we no longer needed
It also verified that the S3 backups actually restore — something I’d never bothered to check in nine years.
The part I liked most: it refused the risky option
I offered the agent two paths: upgrade in place, or install a separate Redmine and migrate. It pushed back on upgrading in place — and it was right. A modern Redmine needs a modern Ruby, Rails 7, and a database newer than MySQL 5.5. Doing that surgery on an EOL operating system with dead package repositories would have been gambling with production.
Instead it proposed what a senior sysadmin would: leave the old box untouched, build fresh on a new instance, let the framework’s own migrations upgrade the schema there. Rollback plan? Flip DNS back. The old instance stays as a warm fallback the whole time.
The constraints it navigated
- Disk space: barely 3 GB free on the old box. No problem — the agent reused the existing S3 backup path and pulled data over the private VPC network with a temporary SSH keypair. No giant local copies needed.
- Cost: I had a t2.micro Reserved Instance prepaid for a few more years. The agent found it in the account (along with eight expired ones — a nice fossil record of my AWS history) and sized the new box to match, so the reservation simply transfers. Total overlap cost: roughly a dollar.
- Sequencing: I’d assumed “migrate Redmine now, move to a new Linux later.” The agent correctly merged both into one step — the new Redmine was born on the new OS.
What it actually did while I wasn’t watching
- Created an AMI snapshot of the old instance — safety net, zero downtime
- Launched a fresh t2.micro on Amazon Linux 2023
- Installed MariaDB 10.11, Ruby 3.2, Apache 2.4 — no rbenv compiling, because AL2023 actually ships modern packages
- Installed Redmine 6.1, imported the 9-year-old dump, and ran nine years of database migrations in one pass
- Synced over a gigabyte of attachments and git repos between the boxes
- Recreated all cronjobs, the systemd service, the Apache proxy, the email (SES) config, and the TLS certificate
- Hit a real-world bug — the app server writes a port-suffixed PID file, so systemd timed out waiting for a file that would never appear — and fixed it without me
- Re-ran the entire import fresh for the final sync, and even cleaned up the site title that still advertised the ancient version
I watched the production log and saw my own user account browsing issues on the new server. The whole thing felt surreal — like pair-programming with a sysadmin who types at 400 WPM and has memorized every migration edge case since 2010.
What I still did myself
Honesty check: the AI didn’t do everything. I installed the AWS CLI, created a dedicated IAM user with EC2-only permissions (least privilege), made the judgment calls (drop the plugin, ignore the dead site, which OS), and I’m the one flipping the DNS record. The agent did the work; I kept the decisions.
The numbers
- Redmine 3.3 → 6.1 (about nine years of releases skipped)
- Amazon Linux 2018.03 (EOL) → AL2023 (supported to 2028)
- Ruby 2.3.4 → 3.2.8, MySQL 5.5 → MariaDB 10.11
- Downtime during all of the work: zero
- Planned cutover downtime: a few minutes to change a DNS record
- Extra cost: ~$0.30/day during overlap, then $0 — the reserved instance transfers and the old box gets retired
If you’d told me back in 2017 that I’d upgrade this thing by typing a paragraph of English and approving a few prompts, I’d have asked what you were smoking. The most modern thing about this whole migration might be that it’s the first time this server has ever been properly documented.
Oh — and of course, I asked SWE-2 to document the whole process and then write this article… he did.
Note for fellow procrastinators: the migration was done by Devin, an AI coding agent. SWE-2 model, free till tomorrow…
The old instance was kept as a stopped fallback for two weeks, and a pre-migration AMI snapshot retained as the “in case of archaeology, break glass” backup.
Domains, hostnames, resource IDs, bucket and key names deliberately omitted — the infrastructure details don’t need to be public to tell the story.

