{"id":35747,"date":"2026-01-11T17:00:02","date_gmt":"2026-01-11T10:00:02","guid":{"rendered":"https:\/\/dps.media\/huong-dan-di-doi-migrate-supabase-self-hosted-sang-vps-moi-bao-mat-2026\/"},"modified":"2026-01-11T17:34:21","modified_gmt":"2026-01-11T10:34:21","slug":"guide-to-migrate-supabase-self-hosted-to-new-vps-security-2026","status":"publish","type":"post","link":"https:\/\/dps.media\/en\/guide-to-migrate-supabase-self-hosted-to-new-vps-security-2026\/","title":{"rendered":"Guide to Migrating Supabase Self-Hosted to New VPS &amp; Security 2026"},"content":{"rendered":"<p class=\"wp-block-paragraph\">Operating a system <strong>Supabase Self-Hosted<\/strong> gives you absolute control over data and costs. However, at some point, your current VPS may become overloaded, or you find another VPS provider with better performance at a bargain price. That's when you need to think about <strong>Migrate (Move)<\/strong> your system to a new home.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Many of you worry that this migration will be complicated, prone to data loss or damaging stable configurations. Especially when you already have a significant amount of user data (Users), Database, and Storage. Additionally, there's the concern about prolonged downtime (service interruption) during the transition.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Don't worry! This article will be the most detailed guide, instructing you step by step on how to migrate Supabase Docker from the old VPS to the new VPS in the safest way. In particular, we will use the method <strong>VPS-to-VPS transfer<\/strong> to ensure the fastest speed, and set up <strong>Nginx Proxy Manager<\/strong> to manage domains as well as secure your Dashboard (Studio).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Understand Clearly the \u201cHeart\u201d of Supabase Self-Hosted<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Before getting started, you need to understand the file structure of Supabase to know what to copy and what can be skipped. When you install Supabase with Docker, you can see in the folder <code data-no-translation=\"\">\/opt\/supabase<\/code> there are many subfolders like <code data-no-translation=\"\">apps<\/code>, <code data-no-translation=\"\">packages<\/code>, <code data-no-translation=\"\">examples<\/code>{\"translations\":[]}<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><img decoding=\"async\" src=\"https:\/\/dps.media\/wp-content\/uploads\/mcp\/2026\/01\/folder_structure_diagram_1768125348402.jpg\" alt=\"Supabase Docker Folder Structure\" title=\"\"><figcaption>Only the \u201cdocker\u201d folder is truly important<\/figcaption><\/figure>\n<\/div>\n\n\n<p class=\"wp-block-paragraph\">In fact: <strong>99% the most important things are compactly contained in the folder <code data-no-translation=\"\">docker<\/code><\/strong>. The remaining folders are mostly the source code of the Supabase project (Next.js apps, libraries\u2026) that Docker Container <strong>does not use directly<\/strong> to run. Docker runs based on pre-built \u201cImages\u201d (for example) <code data-no-translation=\"\">supabase\/studio:latest<\/code>, <code data-no-translation=\"\">supabase\/postgres:15<\/code>, so copying the source code is unnecessary.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In the folder <code data-no-translation=\"\">docker<\/code>, we have 3 \u201cimmutable\u201d components:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong><code data-no-translation=\"\">docker-compose.yml<\/code><\/strong>: This is the system design. It specifies which services will run (Database, Auth, Storage, Realtime\u2026), which ports are open, and which volumes are mounted.<\/li>\n<li><strong><code data-no-translation=\"\">.env<\/code><\/strong>: This is where all your secrets are stored. Database password (<code data-no-translation=\"\">POSTGRES_PASSWORD<\/code>), JWT Secret (to generate tokens), API Keys (Anon Key, Service Role Key). Losing this file means losing control of the system.<\/li>\n<li><strong><code data-no-translation=\"\">volumes\/<\/code><\/strong>: The most important is <code data-no-translation=\"\">volumes\/db\/data<\/code>. This is where the live PostgreSQL data is stored. If you only copy the config files and forget this folder, you'll have a brand new Supabase but... empty.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Prepare the \u201cNew Home\u201d (Target VPS)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">You need to prepare a clean new VPS (Fresh Install). The recommended operating system is <strong>Ubuntu 20.04 LTS<\/strong> ho\u1eb7c <strong>22.04 LTS<\/strong> to ensure stability and best compatibility with Docker.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">First, SSH into the new VPS and update the system:<\/p>\n\n\n\n<pre class=\"wp-block-code\" data-no-translation=\"\"><code data-no-translation=\"\">sudo apt update &amp;&amp; sudo apt upgrade -y<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Next, install Docker and Docker Compose plugin. The fastest and most standard way is to use Docker's official automatic installation script:<\/p>\n\n\n\n<pre class=\"wp-block-code\" data-no-translation=\"\"><code data-no-translation=\"\">curl -fsSL https:\/\/get.docker.com -o get-docker.sh\nsudo sh get-docker.sh<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">After installation, check if Docker is running:<\/p>\n\n\n\n<pre class=\"wp-block-code\" data-no-translation=\"\"><code data-no-translation=\"\">sudo docker --version\nsudo docker compose version<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If you see the full version, you're ready to receive the data.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: Transfer Data (\u201cShoot\u201d Data via Rsync)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of the traditional method: <em>Download 10GB of data to your personal computer (slow network) -&gt; Upload 10GB to the new VPS (slow network)<\/em>, we will use <strong>Rsync<\/strong> to transfer data directly between 2 VPS. The speed will be extremely fast by utilizing datacenter bandwidth.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><img decoding=\"async\" src=\"https:\/\/dps.media\/wp-content\/uploads\/mcp\/2026\/01\/rsync_terminal_command_1768125330398.jpg\" alt=\"Rsync Terminal Transfer Command\" title=\"\"><figcaption>Rsync transfers data safely, supports resume when network is lost<\/figcaption><\/figure>\n<\/div>\n\n\n<p class=\"wp-block-paragraph\"><strong>Why use Rsync instead of SCP?<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Resume:<\/strong> If you're copying 90% and the network drops, running the Rsync command again will continue from 90%. SCP will have to start over.<\/li>\n<li><strong>Preserves attributes:<\/strong> Rsync preserves permissions and timestamps of files, which is very important for Database files.<\/li>\n<li><strong>Data compression:<\/strong> Flag <code data-no-translation=\"\">-z<\/code> helps compress data during transfer, saving bandwidth.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Implementation:<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">1. <strong>On New VPS<\/strong>: Create the parent directory in advance to avoid copying the wrong path:<\/p>\n\n\n\n<pre class=\"wp-block-code\" data-no-translation=\"\"><code data-no-translation=\"\"># T\u1ea1o th\u01b0 m\u1ee5c \/opt\/supabase\nmkdir -p \/opt\/supabase<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">2. <strong>On Old VPS<\/strong>: Run the Rsync command below. This command will prompt for the root password of the new VPS.<\/p>\n\n\n\n<pre class=\"wp-block-code\" data-no-translation=\"\"><code data-no-translation=\"\"># Ch\u1ea1y l\u1ec7nh n\u00e0y T\u1eea VPS C\u0168\n# IP_VPS_MOI: Thay b\u1eb1ng IP c\u1ee7a server m\u1edbi (VD: 116.118.x.x)\n\nrsync -avzP \/opt\/supabase\/docker\/ root@IP_VPS_MOI:\/opt\/supabase\/docker\/<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Note the trailing <code data-no-translation=\"\">\/<\/code> in the source path is very important. If you write it wrong, the folder <code data-no-translation=\"\">docker<\/code> may be nested (for example <code data-no-translation=\"\">docker\/docker<\/code>). The above command is the most standard.<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Launch Supabase in the New Home<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">After the Rsync progress bar completes (reaches 100%), congratulations, the entire soul of the old system has moved to the new home. Now wake it up.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On <strong>New VPS<\/strong>:<\/p>\n\n\n\n<pre class=\"wp-block-code\" data-no-translation=\"\"><code data-no-translation=\"\">cd \/opt\/supabase\/docker\n\n# (T\u00f9y ch\u1ecdn) Ki\u1ec3m tra l\u1ea1i file .env xem c\u00f3 c\u1ea7n ch\u1ec9nh s\u1eeda g\u00ec kh\u00f4ng\ncat .env\n\n# Kh\u1edfi ch\u1ea1y c\u00e1c container\ndocker compose up -d<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The first run will take a few minutes for Docker to pull the images. Be patient. After it's done, check the logs to ensure no errors:<\/p>\n\n\n\n<pre class=\"wp-block-code\" data-no-translation=\"\"><code data-no-translation=\"\">docker compose logs -f<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 5: Configure Nginx Proxy Manager &amp; SSL<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Supabase by default only opens local ports (like 8000, 3000) or internal ports. To allow users to access via a nice domain (<code data-no-translation=\"\">https:\/\/api.your-domain.com<\/code>) with SSL security, you need a Reverse Proxy. <strong>Nginx Proxy Manager (NPM)<\/strong> is an excellent choice because it has an intuitive interface.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><img decoding=\"async\" src=\"https:\/\/dps.media\/wp-content\/uploads\/mcp\/2026\/01\/nginx_proxy_manager_ui_1768125366079.jpg\" alt=\"Nginx Proxy Manager Dashboard Interface\" title=\"\"><figcaption>Configure 2 separate Proxy Hosts for API and Studio<\/figcaption><\/figure>\n<\/div>\n\n\n<p class=\"wp-block-paragraph\">You need to create 2 <strong>Proxy Hosts<\/strong> separately:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1. API Gateway (Most Important)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Domain Names:<\/strong> <code data-no-translation=\"\">api.your-domain.com<\/code> (or your API domain)<\/li>\n<li><strong>Forward Hostname \/ IP:<\/strong> <code data-no-translation=\"\">172.17.0.1<\/code> (This is Docker's default Gateway IP, helping NPM point back to the container on the same host).<\/li>\n<li><strong>Forward Port:<\/strong> <code data-no-translation=\"\">8008<\/code> (This is the port of Kong\/API Gateway in Supabase).<\/li>\n<li><strong>Websockets Support:<\/strong> MUST BE ENABLED (Supabase Realtime uses this).<\/li>\n<li><strong>SSL Tab:<\/strong> Request a new Let\u2019s Encrypt Certificate, Force SSL, HTTP\/2 Support.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">2. Studio Dashboard (Management Interface)<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Domain Names:<\/strong> <code data-no-translation=\"\">studio.your-domain.com<\/code><\/li>\n<li><strong>Forward Hostname \/ IP:<\/strong> <code data-no-translation=\"\">172.17.0.1<\/code><\/li>\n<li><strong>Forward Port:<\/strong> <code data-no-translation=\"\">3003<\/code> (Default port of Studio).<\/li>\n<li><strong>Websockets Support:<\/strong> Enable.<\/li>\n<li><strong>SSL Tab:<\/strong> Configure similarly as above.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">After configuring NPM, the final step is to go to your domain management page (DNS), point the 2 sub-domains above to <strong>IP of the New VPS<\/strong>. Wait about 5-10 minutes for DNS to update.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 6: High-Level Security (Access Lists)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A major vulnerability of Supabase Self-Hosted community edition is the <strong>Studio Dashboard has no login page<\/strong>. Anyone who knows the path <code data-no-translation=\"\">studio.your-domain.com<\/code> can access and edit your data. This is extremely dangerous.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">But with Nginx Proxy Manager, we can easily patch this vulnerability using the <strong>Access Lists<\/strong> (Access Restriction).<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><img decoding=\"async\" src=\"https:\/\/dps.media\/wp-content\/uploads\/mcp\/2026\/01\/security_access_list_1768125382017.jpg\" alt=\"Secure Studio with Access List\" title=\"\"><figcaption>Add password protection layer for Studio<\/figcaption><\/figure>\n<\/div>\n\n\n<p class=\"wp-block-paragraph\"><strong>How to implement:<\/strong><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>In NPM, go to the tab <strong>Access Lists<\/strong>, click <strong>Add Access List<\/strong>.<\/li>\n<li>Name it <code data-no-translation=\"\">Admin-Only<\/code>.<\/li>\n<li>In the <strong>Authorization<\/strong>, tab, add a Username and Password that only you know.<\/li>\n<li>(Advanced tip) In the tab <strong>Access<\/strong>, you can enter the static IP of your company\/home and select <strong>Action: Allow<\/strong>. Then enable the option <strong>Satisfy Any<\/strong>.\n<ul><li>Meaning: If it's the correct \u201chome\u201d IP, access directly without any questions.<\/li><li>If it's an unfamiliar IP (like at a cafe), the system will show a popup asking for password.<\/li><\/ul>\n<\/li>\n<li>Save.<\/li>\n<li>Go back <strong>Proxy Hosts<\/strong>, edit the line <code data-no-translation=\"\">studio.your-domain.com<\/code>. At the Details tab, in the <strong>Access List<\/strong>, select <code data-no-translation=\"\">Admin-Only<\/code>.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Moving the server is not as scary as you think if we understand the nature of Docker and know how to use tools like Rsync. With the guide above, you will not only successfully migrate but also upgrade the security system for your Supabase.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Remember to always backup data periodically (cronjob backup folder <code data-no-translation=\"\">docker<\/code>) to prevent unexpected issues. Wish you success!<\/p>","protected":false},"excerpt":{"rendered":"<p>Running a Supabase Self-Hosted system gives you absolute control over data and costs. However, at some point, your current VPS may become overloaded, or you find another VPS provider with better performance [\u2026]<\/p>","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-35747","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"acf":[],"rankmath_keywords":{"primary":"","secondary":[""]},"yoast_keywords":{"primary":"","secondary":[]},"yoast_focuskw":"","rankmath_focuskw":"","seo_keywords":{"primary":"","secondary":[""]},"_links":{"self":[{"href":"https:\/\/dps.media\/en\/wp-json\/wp\/v2\/posts\/35747","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/dps.media\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/dps.media\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/dps.media\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/dps.media\/en\/wp-json\/wp\/v2\/comments?post=35747"}],"version-history":[{"count":0,"href":"https:\/\/dps.media\/en\/wp-json\/wp\/v2\/posts\/35747\/revisions"}],"wp:attachment":[{"href":"https:\/\/dps.media\/en\/wp-json\/wp\/v2\/media?parent=35747"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/dps.media\/en\/wp-json\/wp\/v2\/categories?post=35747"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/dps.media\/en\/wp-json\/wp\/v2\/tags?post=35747"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}