HomeDocumentationc_002_deployment
c_002_deployment
15 min read

Flutter Web Deployment: From Zero to Production in 5 Minutes

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:

javascript
flutter build web --release

This creates a build/web/ directory containing:

javascript
  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)
Pro Tip
The Beautiful Truth: No Server Required!

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)

javascript
scp -r build/web/* user@your-server-ip:/var/www/bookstore/
javascript
# 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)

javascript
# 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:

javascript
#!/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:

javascript
rsync -avz --update build/web/ user@server:/var/www/bookstore/

Method 3: Git (Professional & Trackable)

javascript
# 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
Pro Tip
Use Git for code, rsync for builds!

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)

javascript
# 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)

Pro Tip
Nginx is the gold standard for serving static files. Fast, reliable, and battle-tested.

Installation

javascript
sudo apt update
sudo apt install nginx

Configuration

Create /etc/nginx/sites-available/my_site:

javascript
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:

javascript
# 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:

javascript
[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:

javascript
[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:

javascript
# 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:

javascript
[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!

javascript
# 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

javascript
# 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)

javascript
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:

javascript
# 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)

javascript
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:

javascript
#!/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:

javascript
chmod +x deploy.sh

#Now deploying is one command:
./deploy.sh

Troubleshooting Common Issues

Issue 1: Port Already in Use

javascript
# Find what's using port 8080
  sudo lsof -i :8080

  # Kill the process
  sudo kill -9 <PID>

Issue 2: Permission Denied

javascript
# 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

javascript
# 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

javascript
# 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

javascript
# 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

javascript
# 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:

javascript
[Service]
  StandardOutput=append:/var/log/bookstore/access.log
  StandardError=append:/var/log/bookstore/error.log

Create log directory:

javascript
sudo mkdir -p /var/log/bookstore
sudo chown www-data:www-data /var/log/bookstore

Even More Advanced: Monitoring with Grafana

javascript
# 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:

javascript
# 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

Related Topics

deploy flutter web appsystemd flutter servicedeploy flutter to VPSflutter web nginx configurationhow to deploy flutter web app to VPSflutter web deployment without dockerflutter web deployment checklist

Ready to build your app?

Turn your ideas into reality with our expert mobile app development services.