Upload your website files, organize directories, configure server settings, and manage live site updates like a professional.
Before uploading a single file, every developer needs a clear mental map of where their website actually lives on the hosting server. When you log into a shared hosting account, a VPS, or a dedicated server through FTP, you are typically dropped into your home directory — but your home directory is not where visitors see your website. Instead, there is a specific folder, called the web root, that the web server (Apache, Nginx, or LiteSpeed) is configured to serve publicly on your domain. Everything you place in the web root becomes reachable via your URL. Everything outside of it stays private.
The name of this web root folder varies by hosting provider and server stack. On cPanel-based shared hosting, you'll almost always see a directory called public_html. On Plesk servers, Windows IIS hosts, and some older Linux installations, it is called httpdocs or wwwroot. XAMPP and self-hosted Apache stacks default to htdocs, while Nginx virtual hosts commonly map to /var/www/html or a custom path like /srv/sitename/public. If you're unsure, your hosting welcome email or control panel will list the exact absolute path. When in doubt, upload a tiny test file like hello.txt containing the word works and try reaching it at https://yourdomain.com/hello.txt — if it loads, you found the correct folder.
Once inside the web root, the hierarchy is up to you. A well-organized project separates concerns so that assets, pages, and configuration never collide. Here is a typical production-ready structure for a static or lightly-dynamic website:
Notice how assets/ acts as a container for everything the pages depend on, keeping the root directory clean. Subdomains, when offered by your host, usually appear as sibling folders to public_html (e.g. subdomain.yourdomain.com maps to /home/user/public_html/subdomain/ or a dedicated subdomain/ folder). Treat each subdomain as its own mini-site with its own web root rules.
Deployment goes smoother when the local project is already production-shaped before you open FileZilla or WinSCP. A messy local folder — with build artifacts, half-finished pages, hidden OS files, and uncompressed images — leads to bloated uploads, long transfer times, and often broken paths once files land on the server. The preparation phase is the quiet 80% of a professional deployment.
Start by auditing what should not ship. Operating systems leave invisible breadcrumbs: macOS creates .DS_Store files in every folder it opens, Windows generates Thumbs.db, and most editors scatter .vscode/, .idea/, or *.swp files. Source maps, node_modules, Git folders, build logs, and local .env files with secrets must stay off the server. If your FTP client supports an ignore list or filename filter, configure it once and reuse it on every project. FileZilla's filename filters live under View → Filename filters; WinSCP has a File mask in the transfer options.
.env, config.local.php, database dumps, or any file containing credentials, API keys, or private tokens. If one slips through, rotate every exposed secret immediately — assume the file was indexed by bots within minutes.
Next, think about file naming. Web servers running on Linux are case-sensitive: Logo.PNG and logo.png are two different files. Windows developers who build locally with any-case filenames routinely watch images 404 after a Linux upload. Adopt a strict convention: all lowercase, hyphens instead of spaces, no special characters. my file (final).html becomes my-file-final.html — easier to link, easier to type, and encoding-safe in URLs.
If your site uses modern tooling (Sass, TypeScript, bundlers), run the production build locally and upload only the compiled output. Minify CSS and JavaScript to shrink transfer size and improve first-paint performance. Compress raster images with tools like pngquant, mozjpeg, or online services like TinyPNG; convert large PNGs to WebP where supported. A hero image dropping from 1.8 MB to 220 KB uploads 8× faster and loads 8× faster for every visitor.
# Clean junk files before deployment (run inside project folder) find . -name ".DS_Store" -delete find . -name "Thumbs.db" -delete find . -name "*.swp" -delete # Preview the upload size du -sh --exclude=node_modules --exclude=.git .
Finally, zip up a backup of your current local project before every deployment. Version control (Git) is the real safety net, but a dated zip gives you a second insurance policy and takes five seconds. The habit will save you the day a chmod goes wrong on a live file.
With the server structure understood and the local files cleaned, the actual upload is the easy part — if you avoid a few classic pitfalls. In FileZilla, WinSCP, Cyberduck, and every other mainstream FTP client, the interface is split into two panes: the local site on the left, the remote site on the right. To deploy, navigate the right pane into public_html (or your web root), navigate the left pane into your project folder, select the files, and drag them across. The transfer queue at the bottom shows every file in flight, its current speed, and its ETA.
Drag-and-drop is intuitive but not the only option. Right-clicking a file or folder exposes an Upload command, and most clients let you double-click a file in the local pane to send it to the currently open remote directory. For large projects, selecting the entire project root and dragging it into the remote pane triggers a recursive upload: subfolders are created automatically on the server, mirroring your local tree exactly. Keep an eye on the queue — if a file fails (red icon), the client will usually auto-retry, but repeated failures hint at a stability or permission issue worth stopping to investigate.
Transfer modes matter more than most beginners realize. FTP supports three modes: ASCII, Binary, and Auto. ASCII mode performs line-ending conversions — useful decades ago when moving text files between Unix (LF) and Windows (CRLF) hosts, but catastrophic for images, archives, PDFs, or any binary blob. Binary mode copies bytes verbatim and is safe for everything. Auto mode (the default in modern clients) picks per-extension based on a configurable list. For a trouble-free life, set Default transfer type to Binary and leave it there — the only files that genuinely needed ASCII conversion stopped being relevant twenty years ago.
Also pay attention to passive vs active mode. Nearly all shared hosts and firewalled corporate networks require passive (PASV). Active mode asks the server to open a connection back to your IP, which almost never works through NAT or router firewalls. In FileZilla, this lives under Edit → Settings → Connection → FTP → Transfer Mode → Passive. If uploads hang at 0% or directory listings time out, flipping to passive mode is the first thing to try.
ftp> open ftp.example.com Name: webmaster Password: ******** 230 User webmaster logged in. ftp> binary 200 Switching to Binary mode. ftp> cd public_html ftp> put index.html 226 Transfer complete. 24,580 bytes sent in 0.12 secs.
Finally, verify every upload. After the queue empties, refresh the remote pane and spot-check file sizes against the local copies. A size mismatch almost always means the file was truncated or saved in the wrong mode. Then open the site in a private/incognito window to bypass your browser cache and confirm each page renders exactly as expected.
When a visitor types https://yourdomain.com/ into their browser, the web server has to decide which file to send back. There is no magic involved — the server simply walks a configured list of default documents (also called DirectoryIndex files in Apache or index files in Nginx) and serves the first one it finds. The classical list is index.html, then index.htm, then index.php, then default.html. If none exist in the requested folder, the server either shows a directory listing (if enabled) or returns a 403 Forbidden.
For a static site, your top-level entry point is almost always index.html placed directly in the web root. The same rule applies to every subfolder: a request for /blog/ looks for /blog/index.html, a request for /products/shoes/ looks for /products/shoes/index.html, and so on. This folder-plus-index pattern is why clean URLs like example.com/about/ work without a .html extension — the server quietly serves about/index.html behind the scenes. Rename a file from about.html to about/index.html and your URLs instantly look more professional, with no server-side rewrites required.
Index.html will not be served as the default document. The filename must be exactly index.html in lowercase, matching the server's configuration.
If you need a different filename to act as the default (say, home.html or landing.html), you can override the server's list via .htaccess:
# Use home.html as the default page for this directory DirectoryIndex home.html index.html index.php
The order matters: Apache serves the first listed file that exists. Place home.html first and the server will prefer it over any index.html sitting in the same folder.
You should also think carefully about directory listing. If no default document is present and listing is enabled, Apache or Nginx will show a plain, bot-indexable tree of every file in the folder — including configuration files and backups you forgot to delete. This is a well-known information-disclosure vector. Disable it globally in .htaccess with Options -Indexes, and additionally drop an empty index.html into every folder that is not meant to be browsable (such as assets/, uploads/, or includes/).
Finally, create friendly error pages. The two most common ones to customize are 404 Not Found (when a URL does not match any file) and 500 Internal Server Error (when something goes wrong server-side). A branded error page keeps visitors on your site instead of bouncing to Google. Upload 404.html and 500.html to the web root, then register them in .htaccess:
ErrorDocument 404 /404.html ErrorDocument 500 /500.html ErrorDocument 403 /403.html
Keep the error pages lightweight — no heavy frameworks or external dependencies — because they may be requested when the server is already stressed.
The humble .htaccess file is one of Apache's most powerful features. Dropped into any directory, it lets you reconfigure the server for that folder (and all its subfolders) without touching the main server config and without a restart. Redirects, URL rewrites, authentication, compression, caching, security headers, hotlink protection, MIME-type overrides — all of it lives in .htaccess. On shared hosting where you have no shell access, it is often your only lever for server behavior.
The file is plain text, starts with a dot (which hides it on Unix by default — make sure your FTP client shows hidden files; in FileZilla that's Server → Force showing hidden files), and uses Apache's directive syntax. Here is a well-rounded production starter you can adapt:
# Force HTTPS
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# Redirect www to non-www
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^ https://%1%{REQUEST_URI} [L,R=301]
# Remove .html extension from URLs
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}\.html -f
RewriteRule ^(.+?)/?$ $1.html [L]
# Disable directory listing
Options -Indexes
# Custom error pages
ErrorDocument 404 /404.html
ErrorDocument 500 /500.html
# Security headers
Header set X-Content-Type-Options "nosniff"
Header set X-Frame-Options "SAMEORIGIN"
Header set Referrer-Policy "strict-origin-when-cross-origin"
Header set Strict-Transport-Security "max-age=31536000; includeSubDomains"The first block forces every incoming request to HTTPS using a 301 permanent redirect — essential now that modern browsers warn users on any non-encrypted form. The second block normalizes the hostname so that both www.example.com and example.com settle on a single canonical URL (critical for SEO and analytics accuracy). The third block hides .html extensions, giving every page a clean URL. The security headers at the bottom add a thin but meaningful layer of browser-side protections against clickjacking, MIME-sniffing, and leaking referer information to third parties.
.htaccess can take the entire site offline with a 500 error. After editing, immediately reload the homepage in a private window. If it breaks, upload the previous version (you kept a backup, right?) to recover.
301 redirects deserve special attention when migrating URLs. Suppose you renamed /old-article.html to /blog/new-article/ — without a redirect, every backlink and search-engine entry pointing to the old URL will hit a 404 and the SEO equity evaporates. Add one line per legacy URL to preserve link juice:
Redirect 301 /old-article.html /blog/new-article/ Redirect 301 /products.php /products/
Upload .htaccess in ASCII mode (or let your FTP client auto-detect by extension), because stray Windows CRLF line endings have been known to confuse older Apache builds. After upload, set permissions to 644 — readable by the server, writable only by you.
The initial deployment is the fun one. Every deployment after that happens on a live site with real visitors reading pages, submitting forms, and triggering analytics while you're uploading. A sloppy update can serve a half-rendered page to a prospective customer or, worse, overwrite a critical config file. Professional deployments plan for this moment.
The simplest rule: upload non-destructively. Add new files first, then replace changed files, then — only after verifying the new pages work — delete anything that's no longer needed. If you reverse that order and delete first, there will be a window during which visitors can hit missing resources. For CSS and JavaScript, use cache-busting query strings (style.css?v=20261020) or hashed filenames (style.3f8a2c.css) so that updated assets bypass browser and CDN caches instantly.
index.new.html), test it by visiting the temporary URL, and only when it's confirmed working, rename it to index.html. The rename is atomic, so visitors never see a half-written file.
Caches live in surprising places. A browser cache is the obvious one, but there is almost always another layer or two: a CDN (Cloudflare, Fastly), a server-side cache (Varnish, LiteSpeed Cache, WP Super Cache), and sometimes an upstream reverse proxy. Updating a file via FTP does not clear any of these. After deployment, log in to each cache layer and purge the specific files you changed, or do a full purge if the update was broad.
For anything more complex than a one-page tweak, adopt a staging environment. A staging site is a near-identical copy of production — same server, same database schema, same PHP version — but hosted on a subdomain like staging.example.com with its own public_html folder (or on a completely separate host). Deploy every change there first, click through the critical user flows, run Lighthouse, check mobile layouts, and only then promote the files to production. Many hosts offer one-click staging, but you can also build your own with a second FTP account and a few DNS records.
A simple staging-then-production workflow with FTP looks like this:
Before any live update, pull a backup. On shared hosting, that may mean using the control panel's backup tool. On a VPS, a quick tar -czf backup-$(date +%F).tar.gz public_html over SSH takes seconds. Keep the last 7–14 backups rotated. The one time you need to roll back a deployment at 2 a.m., you'll be grateful.
Finally, communicate. If you're part of a team, announce the deployment window in Slack or your project management tool. If the site has heavy traffic, deploy during a low-traffic window (check Analytics for your actual quietest hour — it's rarely midnight in your own timezone).
For a ten-file brochure site, drag-and-drop in a GUI client is perfectly fine. For an e-commerce catalog, a blog with hundreds of posts, or an image gallery with thousands of thumbnails, you need batch tooling. Transferring 5,000 small files one at a time is painfully slow because each file pays the full round-trip cost of a new FTP data connection. Batch techniques — the mput command, directory mirroring, and syncing — move the same data in a fraction of the time.
The oldest batch tool is mput (multi-put), available in the classic command-line ftp client and in lftp. It accepts a wildcard and uploads every matching file in one go. The companion mget does the same in reverse. By default, mput prompts for confirmation on each file — kill the prompt with the prompt toggle:
ftp> open ftp.example.com ftp> binary ftp> cd public_html/assets/images ftp> lcd ./dist/images ftp> prompt off # stop asking "mput file? y/n" for each file ftp> mput *.jpg ftp> mput *.png ftp> mput *.webp
For truly large transfers, graduate to lftp and its mirror command. lftp mirror compares a local tree against a remote tree and transfers only the differences — the FTP equivalent of rsync. It can run in parallel (multiple files at once), skip files by pattern, exclude directories, and resume after a dropped connection. It is the single most useful tool for maintaining a non-trivial site over FTP:
lftp -u webmaster,PASSWORD ftp.example.com << 'EOF'
set ftp:ssl-allow true
set mirror:parallel-transfer-count 4
mirror --reverse \
--delete \
--exclude-glob .git/ \
--exclude-glob node_modules/ \
--exclude-glob *.swp \
./public ./public_html
EOFBreaking that down: --reverse turns download into upload. --delete removes files on the server that no longer exist locally — a true mirror. --exclude-glob skips patterns you never want uploaded. parallel-transfer-count 4 opens four simultaneous connections, dramatically speeding up transfers of many small files. On a decent connection, mirroring a 10,000-file site that has only 50 changes takes seconds instead of hours.
WinSCP users get the same power through its Synchronize command (Commands → Synchronize or Ctrl+S). It opens a side-by-side diff of local vs remote, highlights every file that would be added, updated, or deleted, and only acts when you confirm. It's a safer first step than firing a destructive command-line sync blindly.
--delete: A mirror with --delete pointed at the wrong remote folder will wipe files that aren't in your local copy. Always dry-run first: mirror --reverse --dry-run prints what would happen without touching anything.
Finally, think about scheduling. Once your sync command works manually, drop it into a shell script and run it from cron (Linux/macOS) or Task Scheduler (Windows) so nightly deploys happen automatically. For CI/CD, the same lftp one-liner runs beautifully from GitHub Actions, GitLab CI, or Jenkins — the automation guide on this site walks through end-to-end examples. Combine batch uploads with cache-busting filenames and staged promotion, and even a 50,000-file production site deploys reliably in minutes.
Next up: lock down every connection with SSH keys and encrypted transfers.
🔒 SFTP & Security → ⚙️ Automate Deployments →