Introduction: The Flutter Promise
Flutter is an amazing framework! With one codebase, you can create:
- Mobile apps (Android & iOS)
- Websites (Progressive Web Apps)
- Desktop apps (Windows, Mac, Linux)
But here's the million-dollar question: How easy is it to deploy your Flutter app to the web?
Spoiler alert: It's surprisingly simple! No complex build tools, no Docker containers, no Kubernetes clusters. Just a few commands and you're live.
In this guide, we'll show you how to deploy your Flutter web app to any VPS (AWS, Google Cloud, DigitalOcean, Vultr, Linode) in under 5 minutes. Let's dive in!
Step 1: Understanding Flutter Web Build
What Happens During flutter build web?
When you run this command, Flutter's compiler (dart2js) converts your Dart code into optimized JavaScript. Here's what you get:
flutter build web --releaseThis creates a build/web/ directory containing:
build/web/
├── index.html # Entry point (1.2 KB)
├── main.dart.js # Your compiled app (~3 MB, minified)
├── flutter.js # Flutter engine loader (9 KB)
├── flutter_service_worker.js # PWA support (8 KB)
├── canvaskit/ # WebAssembly rendering engine (~2 MB)
│ ├── canvaskit.js
│ ├── canvaskit.wasm
│ └── profiling/
├── assets/ # Your images, fonts, etc.
│ ├── fonts/
│ ├── images/
│ └── ...
├── favicon.png
├── manifest.json # PWA manifest
└── version.json
Total size: ~31 MB (with CanvasKit)Here's what makes Flutter web deployment so simple: It's 100% static files! Unlike traditional web frameworks (PHP, Ruby on Rails, Django), there's:
- No database to configure
- No server-side code to execute
- No environment variables to manage
- No runtime to install
Just HTML, JavaScript, and assets. That's it!
Can You Just Open index.html?
Technically, yes! You can right-click build/web/index.html → Open With → Firefox/Chrome. BUT (there's always a but)...
The file:// protocol has limitations:
- WebAssembly (CanvasKit) won't load
- Some browser APIs are restricted
- CORS issues with external resources
The solution? Serve it through HTTP (even locally). Any web server works!
Step 2: Getting Files to Your Server
You've built your app locally. Now let's get it to your cloud VM. Here are three battle-tested methods:
Method 1: SCP (Simple & Quick)
scp -r build/web/* user@your-server-ip:/var/www/bookstore/# With custom SSH port
scp -P 2222 -r build/web/* user@your-server-ip:/var/www/bookstore/
Pros:
- Simple, works everywhere
- Built into SSH (no extra tools)
Cons:
- Copies all files every time (slow for updates)
- No incremental updates
Method 2: rsync (Smart & Efficient)
# Basic sync
rsync -avz build/web/ user@your-server-ip:/var/www/bookstore/
# With progress and custom SSH port
rsync -avzP -e "ssh -p 2222" build/web/ user@your-server-ip:/var/www/bookstore/
# Delete files on server that don't exist locally
rsync -avz --delete build/web/ user@your-server-ip:/var/www/bookstore/
What makes rsync magical:
- Incremental transfers: Only changed files are copied
- Resume capability: Can resume interrupted transfers
- Bandwidth efficient: Compresses data during transfer (-z)
- Preserves permissions: -a keeps file attributes
Common Questions:
Q: Does rebuilding trigger automatic rsync?
A: No. You need to run rsync after each flutter build web. Consider creating a deployment script:
#!/bin/bash
# deploy.sh
echo "Building Flutter web..."
flutter build web --release
echo "Syncing to server..."
rsync -avz --delete build/web/ user@your-server:/var/www/bookstore/
echo "Deployment complete!"
Q: What if I update build/web/ offline (no network)?
A: rsync only transfers when you run it. Your local changes stay local until you explicitly sync. The server won't know about local changes until you push them.
Q: What if someone else modified files on the server?
A: By default, rsync overwrites server files with your local versions. Use --update to only transfer newer files:
rsync -avz --update build/web/ user@server:/var/www/bookstore/Method 3: Git (Professional & Trackable)
# On local machine:
git add build/web
git commit -m "Deploy v1.0.2"
git push origin main
# On server:
cd /var/www/bookstore
git pull origin main
Pros:
- Version control for deployments
- Easy rollbacks (git checkout v1.0.1)
- Deployment history tracked
Cons:
- Git repo bloat (build files are large)
- Not ideal for frequent deployments
Step 3: Serving Your App (Three Methods)
Now your files are on the server. Time to make them accessible!
Method 1: Python HTTP Server (Quick Start)
# Navigate to your app directory
cd /var/www/my_site
# Start server on port 8080
python3 -m http.server 8080
# Or run in background
nohup python3 -m http.server 8080 > /dev/null 2>&1 &
Access: http://your-domain.com:8080
Pros:
- Zero configuration
- Built into Python (already installed on most servers)
- Perfect for quick demos
Cons:
- Not production-grade
- No automatic restart on crash
- No caching or optimization
Resource usage:
- RAM: ~20 MB idle
- CPU: < 1% idle
- Handles: ~100 concurrent users
Method 2: Nginx (Production-Ready)
Installation
sudo apt update
sudo apt install nginxConfiguration
Create /etc/nginx/sites-available/my_site:
server {
listen 8080;
server_name your-domain.com;
# Root directory
root /var/www/my_site;
index index.html;
# SPA routing: all routes go to index.html
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets aggressively
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|wasm)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml
application/xml application/xml+rss text/javascript;
gzip_min_length 1000;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}
Enable and Start:
# Create symlink to enable site
sudo ln -s /etc/nginx/sites-available/my_site /etc/nginx/sites-enabled/
# Test configuration
sudo nginx -t
# Reload nginx
sudo systemctl reload nginx
Pros:
- Production-grade performance
- Advanced caching
- Load balancing ready
- SSL/TLS support
Resource usage:
- RAM: ~5 MB idle (yes, 5 MB!)
- CPU: < 0.5% idle
- Handles: 10,000+ concurrent users
Method 3: Systemd Service (The Professional Way)
This is where the magic happens! Make your Python server (or any server) run like a professional service:
- Auto-starts on server boot
- Auto-restarts on crash
- Logs everything
- Easy to manage (systemctl start/stop/restart)
Creating the Service
Create /etc/systemd/system/bookstore.service:
[Unit]
Description=Bookstore Flutter Web App
Documentation=https://your-docs-url.com
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/my_site
# The command to run
ExecStart=/usr/bin/python3 -m http.server 8080
# Restart policy
Restart=always
RestartSec=10
# Resource limits (optional but recommended)
MemoryLimit=512M
CPUQuota=50%
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=bookstore
# Security hardening (optional)
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
Understanding the Configuration:
[Unit] Section:
- After=network.target: Wait for network before starting
- Wants=network-online.target: Prefer waiting for full network connectivity
[Service] Section:
- Type=simple: Process runs in foreground (default for most apps)
- User=www-data: Run as low-privilege user (security!)
- WorkingDirectory: Where to run the command
- ExecStart: The actual command to execute
- Restart=always: Restart on any failure
- RestartSec=10: Wait 10 seconds before restarting
[Install] Section:
- WantedBy=multi-user.target: Start during normal boot
Managing Your Service:
# Reload systemd to recognize new service
sudo systemctl daemon-reload
# Enable service (start on boot)
sudo systemctl enable my_site
# Start service now
sudo systemctl start my_site
# Check status
sudo systemctl status my_site
# Output:
# ● my_site.service - Bookstore Flutter Web App
# Loaded: loaded (/etc/systemd/system/my_site.service; enabled)
# Active: active (running) since Sun 2026-02-09 12:00:00 UTC
# Main PID: 12345 (python3)
# Tasks: 1
# Memory: 18.2M
# CPU: 0.1s
# View logs
sudo journalctl -u my_site -f
# -f follows logs in real-time (like tail -f)
# View last 50 lines
sudo journalctl -u my_site -n 50
# Restart service
sudo systemctl restart my_site
# Stop service
sudo systemctl stop my_site
# Disable auto-start on boot
sudo systemctl disable my_site
Advanced: Using Nginx with Systemd
For production, combine nginx with systemd monitoring:
Option A: Nginx is already managed by systemd (nothing extra needed!)
Option B: Custom Node.js backend with systemd:
[Unit]
Description=Bookstore API Backend
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/my_site-api
ExecStart=/usr/bin/node server.js
Restart=always
Environment=NODE_ENV=production
Environment=PORT=3000
[Install]
WantedBy=multi-user.target
Now you have:
- Frontend (Flutter) → Served by nginx on port 8080
- Backend (Node.js) → Managed by systemd on port 3000
Beautiful separation of concerns!
Step 4: Firewall Configuration
Don't forget to open your port!
# UFW (Ubuntu)
sudo ufw allow 8080/tcp
sudo ufw status
# Firewalld (CentOS/Fedora)
sudo firewall-cmd --permanent --add-port=8080/tcp
sudo firewall-cmd --reload
# iptables (manual)
sudo iptables -A INPUT -p tcp --dport 8080 -j ACCEPT
sudo iptables-save > /etc/iptables/rules.v4
Step 5: SSL/HTTPS with Let's Encrypt (The Cherry on Top)
Want https://your-domain.com instead of http://? Let's Encrypt makes it free and automatic!
Prerequisites
- Domain name pointing to your server
- Port 80 open (for certificate verification)
- Nginx installed
Installation
# Install Certbot
sudo apt install certbot python3-certbot-nginx
# Obtain certificate and auto-configure nginx
sudo certbot --nginx -d your-domain.com
# Follow the prompts:
# 1. Enter email for urgent renewal notifications
# 2. Agree to Terms of Service
# 3. Choose: Redirect HTTP to HTTPS (recommended)What Certbot does:
- Verifies you own the domain
- Obtains SSL certificate
- Configures nginx automatically
- Sets up auto-renewal
Updated Nginx Config (Certbot modifies this)
server {
listen 80;
server_name your-domain.com;
# Redirect to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name your-domain.com;
# SSL certificates (added by Certbot)
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Your existing config...
root /var/www/bookstore;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
Auto-Renewal
Certificates expire every 90 days. Certbot sets up auto-renewal:
# Test renewal process (dry run)
sudo certbot renew --dry-run
# Check renewal timer
sudo systemctl status certbot.timer
# Manual renewal (if needed)
sudo certbot renew
Your app is now:
- Encrypted (HTTPS)
- Trusted (Valid SSL certificate)
- More SEO-friendly (to continue, add metadata - title and description)
- Professional (Green padlock in browser)
Resource Usage Reality Check
Let's talk numbers. How much does hosting a Flutter web app actually cost in terms of server resources?
Baseline: Static Site (Your Flutter App)
Idle (no visitors):
├─ CPU: 0.1%
├─ RAM: 5-20 MB (nginx: 5MB, Python: 20MB)
└─ Disk: 31 MB
1 user browsing:
├─ CPU: 0.5%
├─ RAM: +10 MB
└─ Network: ~3 MB initial load, then < 100 KB/min
100 concurrent users:
├─ CPU: 5-10%
├─ RAM: ~500 MB
└─ Network: Depends on bandwidth
1000 concurrent users:
├─ CPU: 50-80% (still manageable!)
├─ RAM: ~2 GB
└─ Network: Saturates 100 Mbps if all loading simultaneously
Comparison: Static vs. Dynamic
Your Flutter App (Static): Request → Read file from disk → Send to browser Time: 0.1-1 ms
Typical PHP/Node.js App (Dynamic): Request → Execute code → Query database → Process data → Render HTML → Send to browser Time: 50-500 ms
Real-World Examples
$5/month Vultr VPS (1 GB RAM, 1 vCPU):
- Can handle: 500-1000 concurrent users
- Monthly traffic: Unlimited (up to bandwidth cap)
- Idle cost: Basically running a screensaver
$10/month DigitalOcean Droplet (2 GB RAM, 1 vCPU):
- Can handle: 2000-5000 concurrent users
- Overkill for most demos/small apps
Why so efficient?
- No server-side processing
- Aggressive caching
- Client does all rendering
- One-time file transfer per session
Complete Deployment Script: Automate everything with a single script!
Create deploy.sh:
#!/bin/bash
# Configuration
REMOTE_USER="your-user"
REMOTE_HOST="your-server.com"
REMOTE_PORT="22"
REMOTE_PATH="/var/www/my_site"
SERVICE_NAME="my_site"
# Colors for output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m' # No Color
echo -e "${BLUE} Building Flutter web app...${NC}"
flutter build web --release || {
echo -e "${RED} Build failed!${NC}"
exit 1
}
echo -e "${BLUE} Syncing to server...${NC}"
rsync -avz --delete \
-e "ssh -p $REMOTE_PORT" \
build/web/ \
$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/ || {
echo -e "${RED} Sync failed!${NC}"
exit 1
}
echo -e "${BLUE} Restarting service...${NC}"
ssh -p $REMOTE_PORT $REMOTE_USER@$REMOTE_HOST \
"sudo systemctl restart $SERVICE_NAME" || {
echo -e "${RED} Service restart failed!${NC}"
exit 1
}
echo -e "${GREEN} Deployment complete!${NC}"
echo -e "${GREEN} Visit: https://your-domain.com${NC}"
Make it executable:
chmod +x deploy.sh
#Now deploying is one command:
./deploy.sh
Troubleshooting Common Issues
Issue 1: Port Already in Use
# Find what's using port 8080
sudo lsof -i :8080
# Kill the process
sudo kill -9 <PID>
Issue 2: Permission Denied
# Fix ownership
sudo chown -R www-data:www-data /var/www/my_site
# Fix permissions
sudo chmod -R 755 /var/www/my_site
Issue 3: Nginx 502 Bad Gateway
# Check nginx error logs
sudo tail -f /var/log/nginx/error.log
# Common cause: service not running
sudo systemctl status my_site
sudo systemctl start my_site
Issue 4: White Screen / App Not Loading
# Check browser console (F12)
# Common causes:
# 1. Wrong base href in index.html
# 2. Assets not loading (check paths)
# 3. JavaScript errors
# Solution: Rebuild with correct base URL
flutter build web --base-href /
Issue 5: Systemd Service Won't Start
# Check service status
sudo systemctl status bookstore
# View detailed logs
sudo journalctl -u bookstore -n 100 --no-pager
# Common fixes:
# 1. Fix file paths in service file
# 2. Check user permissions
# 3. Verify ExecStart command works manually
Bonus: Monitoring Your App
Basic Monitoring with Systemd
# Watch service in real-time
watch -n 1 'systemctl status bookstore'
# Monitor resource usage
systemctl show bookstore --property=MemoryCurrent,CPUUsageNSec
# Get service uptime
systemctl show bookstore --property=ActiveEnterTimestamp
Advanced: Add Logging
Update your systemd service to log to a file:
[Service]
StandardOutput=append:/var/log/bookstore/access.log
StandardError=append:/var/log/bookstore/error.log
Create log directory:
sudo mkdir -p /var/log/bookstore
sudo chown www-data:www-data /var/log/bookstoreEven More Advanced: Monitoring with Grafana
# Install Prometheus Node Exporter
sudo apt install prometheus-node-exporter
# Install nginx-prometheus-exporter
# (tracks requests, response times, etc.)
# Set up Grafana dashboard
# Import dashboard ID: 11074 (Nginx metrics)
Production Checklist
Before going live, make sure you've:
Security:
- SSL/HTTPS enabled (Let's Encrypt)
- Firewall configured (only necessary ports open)
- Service running as non-root user
- Security headers configured in nginx
- Regular updates: sudo apt update && sudo apt upgrade
Performance:
- Gzip compression enabled
- Static asset caching configured
- CDN considered for global users (Cloudflare, AWS CloudFront)
Reliability:
- Systemd service configured with auto-restart
- Monitoring/alerting set up
- Backup strategy for server
- DNS configured with low TTL (easy to switch servers)
Maintenance:
- Deployment script created
- Log rotation configured
- Update process documented
Conclusion: The Flutter Web Advantage
Deploying a Flutter web app is refreshingly simple compared to traditional web stacks:
Traditional Web App: PHP/Node → Database → Session management → Server rendering → Complex deployment → Environment variables → Scalability concerns
Flutter Web App with pre-existent API: Build → Upload static files → Serve → Done!
Key Takeaways:
- One Codebase, Everywhere: Write once, deploy to mobile, web, and desktop
- Zero-Config Deployment: No complex build pipelines or Docker containers
- Minimal Resources: A $5 VPS can handle thousands of users
- Professional Tools: Systemd + Nginx = Production-grade setup in minutes
- Free SSL: Let's Encrypt makes HTTPS trivial
- Future-Proof: Easy to scale from demo to production
Your 5-Minute Deployment Path:
# 1. Build (30 seconds)
flutter build web --release
# 2. Upload (1 minute)
rsync -avz build/web/ user@server:/var/www/app/
# 3. Configure service (2 minutes)
# Create systemd service file
# 4. Enable & start (30 seconds)
sudo systemctl enable myapp && sudo systemctl start myapp
# 5. SSL (1 minute)
sudo certbot --nginx -d yourdomain.com
# Total: ~5 minutes!
The best part? This works for any VPS provider: AWS, Google Cloud, DigitalOcean, Vultr, Linode, Hetzner, or even your Raspberry Pi at home!
Flutter web isn't just about building apps—it's about making deployment so simple that you can focus on what matters: creating amazing user experiences.
Now go deploy something awesome!
About This Guide
This guide was born from real-world experience deploying Flutter apps. It is addressed to anyone who wants to learn and deploy a flutter web app. However, you may find it the more useful if you already have a flutter mobile/desktop project with a working API and you want to easily expand to web. If you found it helpful, consider:
- Sharing it with fellow Flutter developers
- Trying out Flutter for your next project
- Exploring Progressive Web Apps (PWAs) with Flutter
Questions? Improvements? Found a typo? Feel free to reach out or contribute! The Flutter community thrives on shared knowledge. Happy deploying!
Additional Resources
- https://docs.flutter.dev/platform-integration/web
- https://nginx.org/en/docs/
- https://www.freedesktop.org/software/systemd/man/systemd.service.html
- https://letsencrypt.org/docs/
- https://docs.flutter.dev/perf/best-practices