Skip to content
All articles
Oct 5, 20267 min read

Deploy Next.js 16 on a VPS: PM2, Nginx, Zero Downtime

Deploy Next.js 16 on a VPS: PM2, Nginx, Zero Downtime

Deploy Next.js 16 on a VPS by building on the server, running PM2 behind Nginx with HTTPS, and swapping release folders for zero-downtime deploys and rollbacks.

This exact setup runs the website you are reading right now: a Next.js 16 App Router site with React 19, served by PM2 on an Ubuntu VPS behind Nginx, deployed automatically from GitHub. It costs a fraction of a managed platform, handles traffic spikes well, and I can roll back a bad release in seconds. Below is the full playbook, including the small details that tutorials usually skip.

Key takeaways

  • Next.js 16 needs Node.js 20.9 or newer. Use the current LTS.
  • PM2 keeps the app alive, restarts it on crashes and on reboot, and can run several instances.
  • Nginx terminates HTTPS, compresses responses and caches /_next/static files for a year.
  • Release folders + a current symlink give you zero-downtime deploys and one-command rollbacks.
  • NEXT_PUBLIC_* variables are baked in at build time. Changing them means rebuilding, not just restarting.

What you need

  • A VPS with at least 2 GB RAM (builds are memory hungry; 4 GB is comfortable). Ubuntu 24.04 LTS is my default.
  • A domain pointing at the server (an A record for www and the apex).
  • A non-root user with sudo, SSH keys, and a firewall. If you have not done this yet, follow my Ubuntu 24.04 hardening checklist first.

Step 1: Install Node.js, PM2 and Nginx

sudo apt update && sudo apt install -y nginx git
# Node.js LTS from NodeSource (or use nvm / fnm if you prefer)
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt install -y nodejs
sudo npm install -g pm2
node -v   # must be 20.9+ for Next.js 16

If your project uses bun or pnpm, install it now too. This site builds with bun; the steps are identical.

Step 2: Use a release folder layout

The single biggest upgrade over "git pull and restart" is building each deploy in its own folder. The live app never sees a half-built .next directory.

/srv/myapp/
├── releases/
│   ├── 20261005-0912-a1b2c3d/
│   └── 20261005-1430-e4f5a6b/
├── shared/
│   └── .env.local        # secrets live here, symlinked into each release
└── current -> releases/20261005-1430-e4f5a6b

A deploy becomes: clone the commit into a new release folder, link the shared env file, install, build, then point current at it. Rolling back is pointing current at the previous folder.

Step 3: Build the app

REL=/srv/myapp/releases/$(date +%Y%m%d-%H%M)-$(git rev-parse --short HEAD)
git clone --depth 1 https://github.com/you/myapp.git "$REL"
ln -s /srv/myapp/shared/.env.local "$REL/.env.local"
cd "$REL" && npm ci && npm run build

Two production details:

  • Environment variables. Anything starting with NEXT_PUBLIC_ is inlined into the JavaScript bundle during next build. The env file must exist before the build.
  • Image optimization. next/image uses sharp on the server. Recent Next.js versions install it automatically; if you see slow or failing image requests, check that it built for your platform.

Optional: set output: "standalone" in next.config.ts to get a minimal server.js with only the dependencies you need. It is great for Docker images. For a plain VPS, next start is simpler and works fine.

Step 4: Run it with PM2

Create ecosystem.config.js outside the releases folder so it survives deploys:

module.exports = {
  apps: [{
    name: "myapp",
    cwd: "/srv/myapp/current",
    script: "node_modules/next/dist/bin/next",
    args: "start -p 3000",
    instances: 2,            // or "max" for one per CPU core
    exec_mode: "cluster",
    max_memory_restart: "800M",
    env: { NODE_ENV: "production" },
  }],
};
pm2 start /srv/myapp/ecosystem.config.js
pm2 save
pm2 startup   # prints a command; run it so PM2 starts on boot

Cluster mode with two or more instances lets pm2 reload myapp restart workers one at a time, so there is always a process answering requests.

PM2 reloads one worker at a time while Nginx keeps sending requests to the other visitors Nginx worker 1 reloading new release…serving requests worker 2 reloading new release…serving requests
pm2 reload restarts workers one at a time. While one loads the new release, Nginx keeps sending traffic to the other, so visitors never see downtime.

One caveat with several instances: each one keeps its own in-memory and on-disk cache for ISR and revalidateTag. On a single VPS this is usually fine. If you see stale pages after revalidation, run one instance or configure a shared cache handler.

Step 5: Put Nginx in front

server {
    listen 80;
    server_name www.example.com example.com;

    location /_next/static/ {
        proxy_pass http://127.0.0.1:3000;
        add_header Cache-Control "public, max-age=31536000, immutable";
    }

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        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;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}
sudo nginx -t && sudo systemctl reload nginx
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

Certbot adds the HTTPS server block and renews certificates automatically. For SEO, pick one canonical host (I use www) and 301-redirect the other one in a single hop. Mixed hosts split your ranking signals.

Keep port 3000 closed in the firewall. Only Nginx should talk to Next.js.

Step 6: Zero-downtime deploys and instant rollback

Put the steps into one script so every deploy is identical:

#!/usr/bin/env bash
set -euo pipefail
APP=/srv/myapp
SHA=$1
REL=$APP/releases/$(date +%Y%m%d-%H%M)-${SHA:0:7}
PREV=$(readlink -f $APP/current || true)

git clone --quiet https://github.com/you/myapp.git "$REL"
git -C "$REL" checkout --quiet "$SHA"
ln -s $APP/shared/.env.local "$REL/.env.local"
(cd "$REL" && npm ci && npm run build)

ln -sfn "$REL" $APP/current
pm2 reload myapp --update-env

# health check, roll back on failure
sleep 5
if ! curl -fsS --max-time 10 http://127.0.0.1:3000/ >/dev/null; then
  echo "health check failed, rolling back"
  ln -sfn "$PREV" $APP/current
  pm2 reload myapp --update-env
  exit 1
fi

# keep the 5 newest releases
ls -1dt $APP/releases/* | tail -n +6 | xargs -r rm -rf

Because the build happens in a fresh folder while the old release keeps serving traffic, a failing build never takes the site down. The only switch is an atomic symlink change plus a rolling reload.

The next step is triggering this script from GitHub on every push to main. I explain the full pipeline, including a locked-down deploy key, in GitHub Actions CI/CD to a VPS with automatic rollback.

Performance checklist after go-live

  • Run Lighthouse on the homepage and a blog post. Your hero heading or image is usually the LCP element; do not hide it behind a fade-in animation.
  • Confirm /_next/static responses return the long cache header.
  • Check pm2 monit memory over a day and tune max_memory_restart.
  • Set up uptime monitoring (UptimeRobot, Better Stack or a simple cron + curl) so you hear about downtime before customers do.

Frequently asked questions

Can Next.js 16 run without Vercel?

Yes. next start is a full Node.js server that supports the App Router, server components, server actions, ISR, middleware and image optimization. Some platform extras, like Vercel's edge network and preview URLs, you replace with your own tools.

How much RAM does a Next.js VPS need?

Running a typical site uses a few hundred MB per instance. Building is the heavy part; 2 GB works for small apps, 4 GB is comfortable. Add swap on small servers so builds do not get killed.

Should I use Docker or PM2?

Both are good. PM2 on the host is simpler for one or two apps on a single server. Docker (often with output: "standalone") is better when you run many services or want identical environments everywhere.

Is self-hosting Next.js cheaper than Vercel?

Usually, yes, once you have real traffic or several projects. I broke down the numbers in Vercel vs self-hosting in 2026.

How do I roll back a bad deploy?

With the release-folder layout, point the current symlink at the previous release and run pm2 reload. It takes seconds and needs no rebuild.

Want this set up for your app?

I deploy Next.js, Node.js and NestJS apps to VPS servers with HTTPS, CI/CD, monitoring and backups, and hand over clear documentation. Browse my web development services, see recent projects, or tell me about your app.

  • Next.js 16
  • deploy Next.js
  • VPS
  • PM2
  • Nginx
  • self-hosting
  • zero downtime

Keep reading

Vercel vs Self-Hosting in 2026: Real Costs Compared
DevOpsOct 5, 2026

Vercel vs Self-Hosting in 2026: Real Costs Compared

Vercel is cheapest for one small app. With several apps, steady traffic or long-running jobs, a $10–40 VPS running Coolify, Dokploy or Kamal usually costs less. Every week I talk to founders and agencies who are surprised by a hosting bill. The app did not change much, but traffic grew, the team grew, or a few background jobs got added, and the platform invoice tripled. Other teams go the opposite way: they move to a cheap VPS, skip backups and monitoring, and pay for it later in downtime. This article compares the real cost of Vercel and self-hosting in 2026, including the hidden costs that pricing pages do not show. I run my own sites on a self-managed VPS and also help clients who are happy on Vercel, so this is not a sales pitch for either side. Key takeaways Vercel's real price is per seat plus usage. Pro plans are billed per team member, and bandwidth, function execution and build minutes above the included quota are charged on top. A VPS is a flat fee. Roughly $5–40 per month buys enough CPU and RAM to host many small apps, a database and a cache. Self-hosting adds work, not just savings: updates, backups, monitoring and security are now your job (or your DevOps partner's job). Open-source PaaS tools like Coolify and Dokploy give you a Vercel-style dashboard on your own server. The break-even point usually arrives when you have more than two or three production apps , a growing team, or workloads that do not fit serverless. How Vercel pricing really works Vercel's free Hobby plan is for personal, non-commercial projects. Commercial projects need Pro, which is priced per team member per month and includes a usage allowance. Beyond the allowance you pay for what you use: data transfer, serverless and edge function execution, image optimization, build minutes and some add-ons. Enterprise plans are custom-priced. This model is excellent when you are small. You pay almost nothing to start, and you get preview deployments, a global CDN and zero server maintenance. The bill grows with three things: Team size. Five developers cost five seats, even if only one person deploys. Traffic and media. Image-heavy sites and large downloads push bandwidth and optimization usage up. Server work. Heavy API routes, AI calls that wait on slow model responses, and frequent revalidation all add function time. Always check the current numbers on Vercel's pricing page; they change, and your usage dashboard is the only reliable forecast. What self-hosting costs The server A VPS from Hetzner, DigitalOcean, Vultr, Linode or Contabo with 2–4 vCPUs and 4–8 GB RAM costs roughly $5–40 per month depending on the provider and region. That single server can run several Next.js or Node.js apps, PostgreSQL, Redis and a reverse proxy. Bandwidth is usually included in generous amounts. The deployment layer Coolify: the most popular open-source, self-hosted PaaS. Git push deploys, preview deployments, databases and a polished dashboard. It needs some RAM for itself (plan on a 4 GB server or larger). Dokploy: a newer alternative with a similar Docker-based approach and a clean UI. Lighter and growing fast. Kamal: the CLI tool from 37signals (makers of Basecamp and HEY). No dashboard; it SSHes into your servers, deploys Docker images and switches traffic with zero downtime. Plain PM2 + Nginx + GitHub Actions: the leanest option with the fewest moving parts. This is what I use for this website. Full guide: deploy Next.js 16 on a VPS . The hidden costs This is where honest comparisons differ from marketing posts. When you self-host, you also take on: Security updates for the OS, runtime and database. Backups that you have actually tested restoring. Monitoring and alerts for uptime, disk space and memory. Incident time. When the server has a problem at 2 a.m., nobody else will fix it. Budget a few hours per month for this, or a monthly maintenance retainer with someone who does it for a living. Even with that included, multi-app teams usually come out ahead. Illustrative shape, not real prices: usage-based bills climb with seats and traffic, while a VPS stays flat until you outgrow it. Three realistic scenarios Scenario 1: Solo founder, one marketing site One developer, a Next.js landing page and blog, modest traffic. Winner: Vercel. You will likely stay inside the Pro allowance, and your time is better spent on the product than on servers. Scenario 2: Agency with 10 client sites Three developers, ten small production sites, each with light traffic. On a per-seat, per-project platform the bill adds up quickly. Two well-managed VPS servers with Coolify or a scripted PM2 setup can host all ten for a flat monthly fee. Winner: self-hosting , often by a wide margin. Scenario 3: SaaS with background jobs and websockets A product with a NestJS API, scheduled jobs, a queue worker and real-time chat. Serverless functions are not designed for long-running processes or persistent websocket connections, so you would need extra services anyway. Winner: self-hosting or a hybrid , where the frontend stays on Vercel and the API, workers and database run on a VPS. A simple decision framework Stay on Vercel if most of these are true: You have one or two apps and a small team. Preview URLs for every pull request are central to your workflow. Your backend is light, or lives elsewhere already. Nobody on the team wants to own servers. Move to self-hosting (or hybrid) if most of these are true: Your monthly platform bill is growing faster than your revenue. You run long tasks, workers, cron jobs, websockets or self-hosted AI models. You need data to stay in a specific country or on hardware you control. You host many small sites for clients. How to migrate without drama Audit Vercel-specific features you rely on: edge middleware, image optimization, cron, KV or Blob storage, analytics. Pick replacements: Next.js middleware and image optimization run fine on next start ; cron becomes systemd timers or a worker; Blob storage becomes S3-compatible storage (see my guide to self-hosted S3 after MinIO ). Build a CI/CD pipeline with automatic rollback so deploys stay boring. Here is the pipeline I use . Harden the server before DNS points at it. Lower DNS TTL , switch, watch logs, and keep the Vercel project for a week as a fallback. Frequently asked questions What is the best Vercel alternative in 2026? For a self-hosted dashboard experience, Coolify is the most popular choice, with Dokploy close behind. For CLI-driven teams, Kamal is excellent. For the simplest setup on one server, PM2 with Nginx and a deploy script is hard to beat. Is a VPS fast enough compared with a CDN? For most audiences, yes, especially with static assets cached for a long time. If your users are spread worldwide, put a CDN like Cloudflare in front of the VPS and you get global caching for free or at low cost. Can I self-host Next.js with all features? Yes. Server components, server actions, ISR, middleware and image optimization all work with next start . Some multi-instance caching needs extra configuration. How much time does self-hosting take each month? With automated updates, backups and monitoring in place, a few hours per month for a small fleet. Without automation it can be much more, which is why setup quality matters. Can I use both Vercel and a VPS? Absolutely. A hybrid setup is common: the marketing site and frontend on Vercel, the API, database and workers on a VPS. Get a hosting cost review Send me your current hosting bill and a short description of your stack, and I will tell you honestly whether moving makes sense, what it would cost, and what the risks are. I handle the migration, CI/CD, hardening and ongoing maintenance. See my DevOps services or contact me .

Read article →
Ubuntu 24.04 Server Hardening Checklist for 2026
Linux System AdminOct 5, 2026

Ubuntu 24.04 Server Hardening Checklist for 2026

Harden Ubuntu 24.04 with automatic security updates, SSH keys only, no root login, a UFW firewall, fail2ban, and offsite backups you have actually tested. A fresh VPS gets its first automated login attempts within minutes of going online. Bots scan the whole internet for open SSH ports, default passwords and outdated software all day long. The good news is that a small set of well-chosen settings stops almost all of it. This is the checklist I apply to every Ubuntu 24.04 LTS server I set up for clients, in the order I apply it, with the commands and the gotchas that are specific to 24.04. Key takeaways Patch automatically. Most real-world compromises use known, already-fixed vulnerabilities. SSH keys only, no root login. On 24.04, put your settings in /etc/ssh/sshd_config.d/ and watch out for the cloud-init file that can re-enable passwords. Default-deny firewall with UFW, and remember that Docker-published ports bypass UFW. fail2ban slows down brute-force attempts and keeps logs clean. Backups you have restored at least once are part of security, not a separate topic. 1. Update everything and enable automatic security updates sudo apt update && sudo apt full-upgrade -y sudo apt install -y unattended-upgrades sudo dpkg-reconfigure -plow unattended-upgrades Ubuntu installs security updates automatically once this is enabled. Kernel updates still need a reboot. Either schedule one in /etc/apt/apt.conf.d/50unattended-upgrades : Unattended-Upgrade::Automatic-Reboot "true"; Unattended-Upgrade::Automatic-Reboot-Time "04:00"; or enable Livepatch through Ubuntu Pro (free for personal use on a small number of machines) to apply many kernel fixes without rebooting. 2. Create a non-root user with sudo sudo adduser deploy sudo usermod -aG sudo deploy # copy your public key sudo mkdir -p /home/deploy/.ssh sudo cp ~/.ssh/authorized_keys /home/deploy/.ssh/ sudo chown -R deploy:deploy /home/deploy/.ssh sudo chmod 700 /home/deploy/.ssh && sudo chmod 600 /home/deploy/.ssh/authorized_keys Open a second terminal and confirm you can log in as the new user and run sudo before you change any SSH settings. Keep your current session open until everything works. 3. Harden SSH (the 24.04 way) Ubuntu 24.04 reads drop-in files from /etc/ssh/sshd_config.d/ . Two details trip people up: The first value wins. OpenSSH uses the first value it reads for each setting, and drop-ins are read in alphabetical order before the main file. Cloud images often ship 50-cloud-init.conf with PasswordAuthentication yes . If your file sorts after it, your setting is ignored. So name your file so it sorts first: # /etc/ssh/sshd_config.d/00-hardening.conf PermitRootLogin no PasswordAuthentication no KbdInteractiveAuthentication no PubkeyAuthentication yes AllowUsers deploy MaxAuthTries 3 LoginGraceTime 30 X11Forwarding no sudo sshd -t # syntax check, no output means OK sudo sshd -T | grep -E 'passwordauthentication|permitrootlogin' sudo systemctl restart ssh The sshd -T line prints the effective configuration, so you can see that passwords are really off. Another 24.04 detail: SSH is socket-activated . If you change the port, the listening port comes from ssh.socket , so run sudo systemctl daemon-reload && sudo systemctl restart ssh.socket afterwards. Changing the port reduces log noise but is not real security on its own; keys-only login is what matters. 4. Turn on the firewall (UFW) sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow OpenSSH sudo ufw allow 80,443/tcp sudo ufw enable sudo ufw status verbose Allow SSH before enabling UFW, or you will lock yourself out. If you have a static office IP, restrict SSH to it: sudo ufw allow from 203.0.113.10 to any port 22 proto tcp . Docker warning: ports published with -p 5432:5432 are opened through iptables rules that UFW does not control. Bind internal services to localhost instead ( -p 127.0.0.1:5432:5432 ) and let Nginx be the only public entry point. I see exposed databases caused by this on client servers more often than any other issue. Default deny: only SSH, HTTP and HTTPS reach the server. MySQL, Redis and Postgres stop at the firewall. Remember that Docker-published ports skip UFW. 5. Install fail2ban sudo apt install -y fail2ban sudo tee /etc/fail2ban/jail.local >/dev/null <<'EOF' [DEFAULT] bantime = 1h findtime = 10m maxretry = 5 backend = systemd [sshd] enabled = true EOF sudo systemctl enable --now fail2ban sudo fail2ban-client status sshd With password login disabled, attackers cannot guess their way in anyway, but fail2ban cuts the noise and protects other services you add later, such as Nginx basic-auth or mail. 6. Check what is listening sudo ss -tulpn Every line should be something you expect. Databases, Redis, app servers on ports like 3000 or 5000, and admin panels should listen on 127.0.0.1 , not 0.0.0.0 . Remove packages you do not use with sudo apt purge . 7. Secure the web layer Use HTTPS everywhere with Let's Encrypt ( certbot --nginx ) and redirect HTTP to HTTPS. Hide version numbers in Nginx with server_tokens off; . Add security headers: Strict-Transport-Security , X-Content-Type-Options: nosniff , Referrer-Policy and a sensible Content-Security-Policy . Never expose .env , .git or backup files from the web root. Running Next.js or Node.js behind Nginx? My guide to deploying Next.js 16 on a VPS shows the full reverse proxy config. 8. Kernel and system settings Ubuntu's defaults are already reasonable, and AppArmor is enabled out of the box. A few extra network settings are worth adding in /etc/sysctl.d/99-hardening.conf : net.ipv4.conf.all.rp_filter = 1 net.ipv4.conf.all.accept_redirects = 0 net.ipv6.conf.all.accept_redirects = 0 net.ipv4.conf.all.send_redirects = 0 net.ipv4.conf.all.accept_source_route = 0 net.ipv4.tcp_syncookies = 1 sudo sysctl --system 9. Logs, monitoring and alerts Keep journald logs persistent and review journalctl -u ssh once in a while. Set up uptime monitoring and disk-space alerts; a full disk causes more outages than hackers do. Run sudo apt install lynis && sudo lynis audit system for a free security score and a list of suggestions. 10. Backups you have actually tested Security is also about recovery. Back up databases and important files every day to a different provider or region, keep several versions, and do a test restore at least once per quarter. My complete guide to Linux backups covers scripts for files and databases, and if you store backups in self-hosted S3, read my notes on MinIO alternatives . The quick checklist Full upgrade and unattended security updates Non-root sudo user with SSH key SSH: keys only, no root, drop-in sorted first, verified with sshd -T UFW default deny, only 22, 80, 443 open Docker ports bound to 127.0.0.1 fail2ban with the sshd jail ss -tulpn shows only expected services HTTPS, security headers, server_tokens off sysctl network hardening Monitoring, alerts and a Lynis audit Offsite backups with a tested restore Frequently asked questions Is Ubuntu 24.04 secure by default? It is a solid base: AppArmor is enabled, packages are signed and security updates are fast. But a fresh server still allows password SSH on many cloud images, has no firewall rules and does not reboot for kernel updates. The steps above close those gaps. Should I change the SSH port? It reduces log noise from bots but does not stop a targeted attacker. Disabling passwords and root login matters far more. If you change it, update UFW and restart ssh.socket on 24.04. Do I need fail2ban if passwords are disabled? It is not strictly required for SSH once passwords are off, but it is cheap, reduces noise, and protects other services you may expose later. How often should I audit a server? Review updates and alerts weekly (automatically if possible), and do a full audit with Lynis and a manual check of open ports every quarter or after any big change. Can you harden my existing server without downtime? Usually yes. Most steps apply live. SSH and firewall changes are done carefully with a second session open, and a kernel reboot is scheduled in a quiet window. Get your server hardened I secure and maintain Ubuntu and Debian servers for businesses: hardening, firewalls, backups, monitoring, and a short written report of everything I changed. See my Linux system admin services or contact me for a security check of your server. Shipping code to that server next? Read GitHub Actions CI/CD with automatic rollback .

Read article →
MinIO Is Archived: Self-Hosted S3 Alternatives in 2026
DevOpsOct 5, 2026

MinIO Is Archived: Self-Hosted S3 Alternatives in 2026

MinIO's open-source repo is archived. For self-hosted S3 in 2026, use RustFS as a drop-in, SeaweedFS for proven scale, or Garage for small multi-site clusters. For years, MinIO was the answer to "how do I run S3 on my own server?" That is no longer true. Over 2025 the company removed most of the web console from the community edition, stopped publishing free binaries and Docker images, and moved the open-source project into maintenance mode. In April 2026 the GitHub repository was archived. If your app still stores uploads, backups or build artifacts in a community MinIO instance, you are running unmaintained storage software, and it is time to plan a move. I went through this migration myself. The uploads for this website (blog covers, project screenshots, chat attachments) used to live on local disk and in MinIO-style storage. Today they live in RustFS , with a local-disk fallback for older files. This guide covers what I learned: which alternatives are worth your time, how to choose, and how to migrate without breaking a single image URL. Key takeaways MinIO community edition is no longer a safe default. No new security fixes means every exposed instance becomes riskier over time. RustFS is the closest MinIO look-alike: same single-binary model, same ports (9000 API, 9001 console), Apache 2.0 license. SeaweedFS is the mature, battle-tested choice when you have lots of files or need to grow to many servers. Garage is great for small clusters spread across locations, but implements only the core S3 API. Migration is mostly boring if your app talks plain S3: copy buckets with rclone , switch the endpoint, keep the same object keys. Why the MinIO change matters for your business Object storage is rarely the exciting part of a product, which is exactly why it gets forgotten. It quietly holds customer uploads, invoices, database backups and media. When the software underneath stops getting patches, three things happen: Security debt grows. Any vulnerability found after the archive date stays open in the community build. Docker images go stale. If you pull minio/minio:latest in CI, you may already be pinned to an old build without noticing. Hiring and support get harder. New engineers will expect a maintained tool, and documentation will slowly drift away from the version you run. The good news: S3 is a protocol, not a product. Your application code almost never needs to change. You swap the server behind the endpoint. The best self-hosted S3 alternatives in 2026 1. RustFS: the drop-in replacement RustFS is written in Rust, licensed under Apache 2.0, and openly aims to fill the space MinIO left. It ships as a single binary or Docker image, exposes the S3 API on port 9000 and a web console on port 9001, and the console will feel familiar to anyone who used the old MinIO UI. docker run -d --name rustfs \ -p 127.0.0.1:9000:9000 -p 127.0.0.1:9001:9001 \ -e RUSTFS_ACCESS_KEY=change-me \ -e RUSTFS_SECRET_KEY=a-long-random-secret \ -v /srv/rustfs/data:/data \ -v /srv/rustfs/logs:/logs \ --restart unless-stopped \ rustfs/rustfs:latest Notice the 127.0.0.1 bindings. Docker publishes ports straight through iptables and bypasses UFW, so I never expose storage ports publicly. Put Nginx in front with TLS instead (for example s3.yourdomain.com ), and never keep the default rustfsadmin credentials. Choose RustFS when: you ran a single MinIO node, you want the shortest migration, and you like having a web console. Be careful: it is a younger project, so pin a version tag in production and test upgrades on staging first. 2. SeaweedFS: proven at scale SeaweedFS has been in development for over a decade. It is built for very large numbers of files, stores small objects efficiently, and offers an S3 gateway on top of its own filer. It is Apache 2.0 licensed and has a long production track record. Choose SeaweedFS when: you store millions of files, you expect to grow beyond one server, or you need features like tiering to cloud storage. Trade-off: more moving parts (master, volume, filer, S3 gateway) and a steeper learning curve than a single binary. 3. Garage: lightweight and geo-distributed Garage was designed by the Deuxfleurs collective to run on modest hardware across several physical locations, with replication built in. It is light on RAM and simple to operate. It is licensed under AGPL-3.0, which is fine for running it as infrastructure, but worth a quick check with your legal team if you plan to modify and distribute it. Choose Garage when: you want three small nodes in three places (home lab, office, VPS) instead of one big server. Trade-off: it implements the core S3 API, not every advanced feature, so check that your SDK calls (versioning, object lock, some ACL operations) are supported before you commit. 4. Ceph RGW: enterprise only Ceph's RADOS Gateway is extremely capable, but it is a full distributed storage platform. Unless you already run Ceph or have a dedicated storage team, it is too heavy for a typical web app or SaaS. 5. Managed S3-compatible storage Sometimes the right answer is to stop self-hosting storage. Providers such as Cloudflare R2, Backblaze B2, Hetzner Object Storage or Wasabi give you an S3 endpoint for a low monthly price. You trade control for zero maintenance. For backups, I often recommend both : self-hosted for the app, plus an offsite managed bucket for disaster recovery. How to choose in 60 seconds One server, a few hundred GB, want it working today: RustFS. Millions of files or multi-server growth: SeaweedFS. Several small nodes in different locations: Garage. No time to run storage at all: a managed S3-compatible provider. Already running Ceph: Ceph RGW. Step-by-step: migrating from MinIO without downtime Step 1: Inventory what you have List buckets, sizes, and which apps use which credentials. Check for bucket policies, lifecycle rules and public-read buckets, because these are the settings people forget to recreate. Step 2: Start the new server next to the old one Run RustFS (or your choice) on different ports or a different host. Create the same bucket names and a dedicated access key per application. Step 3: Copy the data with rclone rclone speaks S3 to both sides and can be re-run safely. Configure two remotes, then sync: # ~/.config/rclone/rclone.conf [old] type = s3 provider = Minio endpoint = http://127.0.0.1:9000 access_key_id = OLD_KEY secret_access_key = OLD_SECRET [new] type = s3 provider = Other endpoint = http://127.0.0.1:9100 access_key_id = NEW_KEY secret_access_key = NEW_SECRET # copy everything, then re-run right before the switch to catch new uploads rclone sync old:uploads new:uploads --progress --checksum rclone copies every object with the same key, so your URLs never change. Run it again right before the switch to catch new uploads. Step 4: Point the app at the new endpoint In most SDKs the change is two environment variables. One setting matters for every MinIO-style server: path-style addressing . Here is the Node.js client config this website uses: import { S3Client } from "@aws-sdk/client-s3"; const s3 = new S3Client({ endpoint: process.env.S3_ENDPOINT, // https://s3.yourdomain.com region: "us-east-1", // any value works for self-hosted forcePathStyle: true, // http://host/<bucket>/<key> credentials: { accessKeyId: process.env.S3_ACCESS_KEY!, secretAccessKey: process.env.S3_SECRET_KEY!, }, }); Keep object keys identical and your public URLs never change. On this site, every upload is served through the API as /files/<key> , and if a key is not found in the bucket it falls back to local disk. That one fallback let me migrate gradually with zero broken images. Step 5: Final sync, switch, watch Run rclone sync one last time, deploy the new endpoint, and watch logs for 403 or 404 errors for a day. Keep the old MinIO data read-only for a week before deleting anything. Don't forget backups Moving storage is the perfect moment to fix backups. A bucket on the same disk as your app is not a backup. Schedule rclone sync to an offsite provider every night and test a restore. If you want a deeper walkthrough of server and database backups, read my complete guide to Linux backups . Frequently asked questions Can I keep using MinIO in 2026? The software still runs, but the community repository is archived and no longer receives fixes. For internal test environments that is acceptable for a while. For anything exposed to the internet or holding customer data, plan a migration. Is RustFS production ready? RustFS has a stable 1.x release line and covers the core S3 API well. I run it in production for this website. As with any young project, pin a version, monitor it, and keep offsite backups. Do I need to change my application code? Usually not. If your app uses an AWS S3 SDK, you change the endpoint, credentials and possibly enable path-style addressing. Object keys and bucket names stay the same. What is the cheapest way to get S3 storage? For small projects, a self-hosted RustFS or Garage node on a VPS you already pay for costs nothing extra. For large or critical data, a managed S3-compatible provider is often cheaper than the engineering time to run storage yourself. How long does a migration take? For a typical web app with tens of gigabytes, a careful migration takes a few hours of work plus the copy time. Most of the effort is inventory and testing, not the copy itself. Need help moving off MinIO? I plan and run storage migrations, set up self-hosted S3 with TLS and backups, and make sure nothing breaks along the way. See my DevOps services or contact me with your current setup and I will reply with a clear plan and a fixed price. If you are also rethinking where your app runs, my comparison of Vercel vs self-hosting costs is a good next read.

Read article →