Deploying a Dart server to a VPS

A step-by-step tutorial

Dart on the server

Published May 2024

Introduction

I've always been a fan of taking control of your own backend infrastructure. While there are many Cloud-based solutions like Firebase and Supabase, the pricing is often unclear. They may start free for a few small projects, but then they get expensive. I suppose if your app is making a lot of money or you have a lot of investment capital, then it's fine, but that hasn't been my situation in the past. The cheapest option I've found with a clear pricing structure is to run your own virtual private server (VPS).

Of course, going the cheap route means you have to do a lot more work. But that work is also a good learning experience. I've set up my own servers quite a few times over the years, both for hosting web pages and for running backend Dart API servers for Flutter apps. In this article, I'll walk you through the steps to do that yourself.

We'll use the following tech stack:

As far as how you create your Dart server, that's up to you. I like to use Shelf, but Serverpod is another great option. This article will focus very little on the Dart server itself. In fact, you can use this tutorial to set up any Docker-based server you like.

Step 0: Preparation

We're going to start from scratch, so I won't assume you already have a VPS or even a domain name. You'll need to prepare both of those now.

Getting a VPS

A VPS is a virtual private server. That means your server will be running on one physical computer at some specific location in the world. The virtual part means that from your OS's perspective, it thinks it has the computer to itself, but it is probably actually sharing the underlying hardware with other users who have purchased VPSs from the hosting company. If you want the hardware all to yourself, then you need to rent or buy a dedicated server, which tends to be more expensive.

I'd recommend getting a VPS that has the following specs at a minimum:

There are many VPS providers. You can use whichever you like, but I currently use DigitalOcean and RackNerd.

Note: My affiliate links are DigitalOcean and RackNerd if you want to help me out.

I like DigitalOcean because of their great tutorials and reliable VPSs. (I've used some smaller VPS providers before that went out of business. And that's a pain to deal with.)

I like RackNerd because they have very inexpensive VPSs. At the time of writing, they have a 1 GB VPS for less than $1/month. And so far they've been pretty reliable. I had some trouble with the VPS being slow once or twice, but when I contacted support, they resolved the problem quickly.

You can also search LowEndBox for deals on VPSs. Just be aware that some of the companies on this list are not very reliable.

For this article, I'm going to purchase a VPS from RackNerd. If you use DigitalOcean, they refer to their VPSs as "Droplets".

I chose a 1 GB KVM VPS with the following specs:

For that VPS, I selected the following configuration options:

In general, it's good to choose a location near your users if possible. However, we'll also put the server behind a Cloudflare CDN, so that will help for static content that can be cached.

You can choose a newer version of Ubuntu if available, but you might need to update some of the commands in this tutorial.

After you make the purchase, you should get an email that includes the IP address of your server and password for the root user. Yours will be different, but my IP is 107.175.2.52.

Next, you should get a domain name, and point it at your IP address.

Registering a domain name

While it's technically possible to hardcode your server's IP address into your frontend Flutter app, what would you do if your VPS provider changed your IP address? (That's happened to me several times with some of the smaller VPS providers.) All of your Flutter apps would break. That means you should get a domain name that points to your server's IP address.

You can use whatever domain registrar you want, but I'll use Cloudflare because they sell the domain names at cost. We also want to use their CDN services. A CDN is a content delivery network. It's a service that takes static content from your server and caches it on servers around the world so that the response time for users is faster.

Cloudflare requires domains that are registered with them to use their CDN. So if you don't want that, you should choose a different domain registrar. If you are serving a lot of large files, you might eventually have to start paying, but even then they're probably the cheapest of the options. From what I hear, they're much cheaper than AWS. Cloudflare doesn't charge for egress (data transfer to the internet) but rather for things like storage and data operations.

I registered the following domain:

That makes this tutorial the first article I'll post on that site. From here on, everywhere you see learndart.dev, you can substitute your own domain name.

Note: If you already own a domain name and you don't want to pay for another one, you can point your server to a subdomain of it. For example, if you own example.com, you can point your server to myapp.example.com.

After getting your domain name, go to your DNS settings and add an A record for the top-level domain. Also add A records for the www and myapp subdomains. For each of these, point them to your IP address. Here's what mine looks like:

Cloudflare DNS settings

Note: If you are only using a subdomain like myapp.example.com instead of a top-level domain, you would only add the A record for myapp and leave example.com and www as they are. It's perfectly fine for one server to serve a subdomain while another server handles the top-level domain.

It should only take a few seconds or at most a few minutes for Cloudflare to resolve the A records for your IP address. If you're using another DNS registrar, though, it could take longer. That's why I'm having you do this step at the beginning of the tutorial.

You can look up the IP address or DNS name with the following tools:

Neither is very helpful at this point, though, because you haven't set up the web server yet. Even if you look up learndart.dev, which should be working by the time you read this tutorial, it will show a different IP address than the one I registered on Cloudflare. That's because Cloudflare masks it for security and performance reasons. (Please don't hack me now.)

If you try to visit your new domain in a browser now, Cloudflare will tell you the server is down. You'll fix that soon.

Where you're headed

To give you a preview of what's to come, these are the domains and subdomain types you'll be hosting:

If you bought a domain that you'll use exclusively for a single app, you could also do the following:

The advantage of using a generic top-level domain and an app-specific subdomain is that you can add as many apps as you want in the future without needing to buy new domains. If they are low-traffic, you can host them on the same server. If they're high traffic, you can point a subdomain to the IP address of a different server.

Much of the process for the remainder of this tutorial I learned from DigitalOcean's tutorials. I'll be following the same general procedure but will streamline the process and give my own abbreviated commentary. If you want more details, refer to the following guides:

Step 1: Ubuntu Server

In this section, you'll go through the process of configuring your new Ubuntu Linux server. That'll involve updating the system and improving on the default security.

Logging in

Find the IP address and root password that you received in the email from your VPS provider.

Then use ssh to log into your server. This command line tool is available by default on Mac and Linux. Windows users can use PuTTY.

ssh [email protected]

Notes:

If everything was successful, you should be logged into your remote machine.

Ubuntu initial login

Do you feel like a real hacker now?

Check the OS version of your system:

lsb_release -a 

It should be Ubuntu 22.04 LTS (or newer). LTS stands for Long-Term Support. This means it receives security updates for 5 years.

Updating the OS software packages

Check for software updates:

apt update

Upgrade the software packages:

apt upgrade

This may take a while. Accept the defaults when prompted.

If you get a message about a newer kernel being available, press the spacebar to select <Ok>.

Pending kernel upgrade

If you get a message about which services to restart, you can accept the defaults. Press tab to highlight <Ok> and then spacebar to confirm.

Which services should be restarted?

Reboot the machine so you can apply your kernal upgrade:

reboot

This will kick you out.

Note: If you leave the terminal connection idle for a long time, it may become unresponsive. In that case, close the terminal and open a new one to log back in.

Log back in:

ssh [email protected]

It takes a few minutes for the server to reboot, so you may be rejected a few times.

Adding a new user

It's more secure not to use the root user for everything. Create a new user account for yourself. You can use your name or a nickname. I'll just use me:

adduser me

You'll need to enter a password. You can leave the other information blank.

Add your new me user to the sudo group:

usermod -aG sudo me

This allows me to perform superuser (root) tasks by prefixing commands with sudo.

Setting up a firewall

Next, let's set up the firewall. Make sure that ssh is allowed so that you don't lock yourself out of your own server:

ufw allow OpenSSH

Check the settings now:

ufw app list

You should see OpenSSH listed as an available application.

Enable the firewall:

ufw enable

Choose y (yes) when prompted.

Check the status:

ufw status

Let's make sure that you can still get it. First, log out of your server:

exit

Then log back in with your new user:

ssh [email protected]

Remember that the cursor won't move as you type in the password you just created.

Good. You're not locked out and your new user is working.

Disabling password login

Next you're going to improve the security even more by not allowing anyone to log in with a password. For that you'll use ssh keys.

Log out of the server again:

exit

If you've never generated SSH keys before, you'll need to do that now. Enter the following command. (If you've previously generated the public/private key pair, you don't need to do it again. Go on to the next command.)

ssh-keygen

When prompted for a password, it's a good idea to use one, but it's not required. The key itself is your password. However, if someone steals your private key and you didn't use a password, they'll be able to log into your server.

Next, copy your public key to your server using ssh-copy-id. (If you don't have ssh-copy-id available on your machine see the DigitalOcean guide for other options.)

ssh-copy-id [email protected]

Log into your server again as me:

ssh [email protected]

You're going to disallow password authentication now.

Edit the sshd_config file:

sudo nano /etc/ssh/sshd_config

When entering a sudo command, you need to enter your me user password. Subsequent uses of sudo will not require it for a while.

Scroll down in the file until you find PasswordAuthentication. Alternatively, you can press Ctrl+w to search. Uncomment the line by removing the # and change the value to no:

PasswordAuthentication no

Then save the file by pressing Ctrl+X to exit, then y to save changes, and enter to confirm the file name.

Now restart ssh:

sudo systemctl restart ssh

Let's test that it is working. Log out again:

exit

And then log back in:

ssh [email protected]

The only password you'll be asked for is your ssh key password, if you set one.

That completes the initial hardening of your server security. Good work!

Step 2: Nginx

Nginx is a web server that will take incoming requests and route them to the appropriate location.

Installing Nginx

We'll have requests coming in for the following primary locations:

learndart.dev
www.learndart.dev
myapp.learndart.dev
myapp.learndart.dev/api

Nginx will help us see that they get to the right place.

First, install Nginx:

sudo apt install nginx

Check what apps are available to the firewall:

sudo ufw app list

You'll see the following:

Available applications:
  Nginx Full
  Nginx HTTP
  Nginx HTTPS
  OpenSSH

We want to allow both HTTP and HTTPS traffic to our server, so run the following command:

sudo ufw allow 'Nginx Full'

Confirm that the change was made:

sudo ufw status

You should see Nginx Full in the list.

Make sure that Nginx is running:

systemctl status nginx

It should be running already, but if not, check the DigitalOcean guide for more help.

Testing the default setup

Visit your server's domain name or IP address in the browser:

You should see a basic web page from Nginx:

Nginx working

The HTML for that page is stored in the /var/www/html folder.

Note: If you have a .dev site and you know that .dev is only visible in a browser if you've set up an SSL certificate, you might be wondering why you can see it. The reason is that Cloudflare provides SSL certificates for all sites that they register. Read more about that here. You'll set up your own SSL certificate later in the tutorial, so if you didn't go with Cloudflare, that's fine.

Hiding the version number

If you go to a page that doesn't exist, you'll see the version number of Nginx. For example, visiting http://107.175.2.52/pinkelephants looks like so:

Nginx version showing

You can make hackers work a little harder by hiding the version number. That way if there's a known vulnerability in some version and you haven't updated Nginx yet, at least you're not advertising it.

Open the main configuration file for Nginx:

sudo nano /etc/nginx/nginx.conf

These settings apply to all sites served by Nginx.

Find the following line in the html block and uncomment it by removing the #:

server_tokens off;

This will hide the version number.

While you're here, you might want to browse the rest of the file. This is where you'll come to make other adjustments. For example, if you wanted to increase the maximum upload size to 20 MB (I believe the default is 1 MB), you'd add the following line to the http block:

client_max_body_size 20M;

When you're finished browsing, the file and exit.

Make sure that you didn't break anything. Run the following command to test the configuration:

sudo nginx -t

Assuming everything is good, restart Nginx:

sudo systemctl restart nginx

Now, if you visit http://learndart.dev/pinkelephants, you won't see the version number.

Nginx version number gone

Take that, hackers!

(Actually, I shouldn't joke like that. I really don't want to challenge anyone to the task.)

Disabling the default site

We don't want the default site interfering with the rest of the tutorial, so disable the config file by unlinking it from Nginx's sites-enabled folder:

sudo unlink /etc/nginx/sites-enabled/default

That config file is still in the /etc/nginx/sites-available folder. You haven't deleted it, but after restarting Nginx, Nginx will no longer serve that default site. You'll create your own content to serve.

Creating new sites

We have two different websites to create:

  1. learndart.dev
  2. myapp.learndart.dev

The first one is our generic site and the second one is our app-specific site.

Note: If you already have a working top-level domain that is served somewhere else, then just follow the directions for myapp.learndart.dev below. Or, if you're domain is solely for MyApp, then follow the directions for learndart.dev.

You'll store the web content in the /var/www directory and create separate folders for each site.

Adding the generic site

Make a folder for the learndart.dev website content:

sudo mkdir -p /var/www/learndart.dev/html

The -p creates the parent folders if they don't exist.

Change the owner to your user, which is me in my case:

sudo chown -R $USER:$USER /var/www/learndart.dev/html

Also apply the appropriate permissions:

sudo chmod -R 755 /var/www/learndart.dev

Add an index.html file:

nano /var/www/learndart.dev/html/index.html

And paste in some temporary content:

<html>
    <head>
        <title>Learn Dart</title>
    </head>
    <body>
        <h1>Welcome to LearnDart.dev!</h1>
    </body>
</html>

Save and exit with Ctrl+X.

Adding the app-specific site

Repeat what you did above, this time for your app site:

sudo mkdir -p /var/www/myapp.learndart.dev/html
sudo chown -R $USER:$USER /var/www/myapp.learndart.dev/html
sudo chmod -R 755 /var/www/myapp.learndart.dev
nano /var/www/myapp.learndart.dev/html/index.html

<html>
    <head>
        <title>MyApp</title>
    </head>
    <body>
        <h1>Welcome to MyApp!</h1>
    </body>
</html>

Eventually you'll want to replace those pages with full websites. If you'd like a Dart-based solution, consider Jaspr or Static Shock, which this site is made with (or will be by the time I finish this tutorial).

Next, you need to route the incoming traffic to the correct locations.

Configuring Nginx to serve the sites

First you'll configure the generic site. Then you'll do the app-specific site.

Configuring the generic site

Create the following Nginx configuration file for learndart.dev:

sudo nano /etc/nginx/sites-available/learndart.dev

Add the following content:

server {
    listen 80;
    server_name learndart.dev www.learndart.dev;

    location / {
        root /var/www/learndart.dev/html;
        index index.html;
    }
}

This is an Nginx server block. Here are a few notes about it:

Save your changes and exit nano.

You've configured the server for learndart.dev by adding that file to the sites-available folder. However, it's not live until you link it to the sites-enabled folder. Do that by creating a symbolic link:

sudo ln -s /etc/nginx/sites-available/learndart.dev /etc/nginx/sites-enabled/

Before restarting Nginx, test that there are no errors in the configuration file:

sudo nginx -t

Note: If you have a server name that's really long, you might need to increase the server_names_hash_bucket_size in /etc/nginx/nginx.conf. See the Nginx docs for details. But unless Nginx tells you too, you don't need to worry about it.

As long as the test passed, restart Nginx:

sudo systemctl restart nginx

Now visit your site in a browser:

Learn Dart site working

Great, it's working!

Configuring the app site

Now repeat the process for your app site.

Note: If you're only using your domain for MyApp, then you can add the location /api block below to the server block in the configuration file you just created for learndart.dev. You can then skip creating a new configuration file for myapp.learndart.dev.

Create the server configuration file:

sudo nano /etc/nginx/sites-available/myapp.learndart.dev

Add the config info to the file:

server {
    listen 80;
    server_name myapp.learndart.dev;

    location / {
        root /var/www/myapp.learndart.dev/html;
        index index.html;
    }

    location /api/ {
        proxy_pass http://localhost:8080/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

For requests coming in for myapp.learndart.dev, you'll serve web content from the html folder you created earlier. However, there's also a special location block for /api/ requests. That means any request coming in for myapp.learndart.dev/api will be proxied (forwarded) to an internal server (localhost) listening on port 8080. This is where your Dart server will be running in a Docker container.

Note: If you use location /api/, the /api part will be removed from the proxyed URL. For example, https://learndart.dev/api/hello will be proxied to localhost:8080/hello. You also need to have the / at the end of http://localhost:8080/. However, if you use location /api (without the trailing /), the /api part will remain. For example, https://learndart.dev/api/hello will be proxied to localhost:8080/api/hello.

Repeat the other steps for enabling this site:

sudo ln -s /etc/nginx/sites-available/myapp.learndart.dev /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx

Visit your app site in a browser:

MyApp site working

Looks good.

Next, you'll add a certificate to your site to enable HTTPS encryption.

Enabling HTTPS

TLS (Transport Layer Security) or SSL (Secure Sockets Layer) certificates are used to encrypt the traffic between your browser and the server. They're provided by a certificate authority (CA), which is a trusted third party that issues certificates. This is the requirement for using HTTPS rather than HTTP in your site address. That matters for your MyApp API especially because you don't want user data to appear in plain text to anyone who's sniffing the network traffic.

If you're using Cloudflare, then you already have a certificate for your domain. Kind of. Cloudflare provides the certificate for the traffic between the browser and Cloudflare's servers. But the traffic between Cloudflare and your server remains unencrypted. We're going to go the extra mile and use a certificate from Let's Encrypt to make sure the data is encrypted along the entire route.

Enabling Full (strict) encryption in Cloudflare

Note: If you're not using Cloudflare, you can skip this step.

You need to tell Cloudflare that you will be using another SSL/TLS certificate for your site. To do that, go to your site in the Cloudflare dashboard. Then go to SSL/TLS in the menu and choose Overview. Choose Full (strict) from the list of encryption mode options.

Choose Full (strict) mode in Cloudflare

Now, if you visit [your site](https://learndart.dev) in a browser, you'll see that your site is down. (You might have to refresh your browser cache.) It'll come back after you create the certificate in the next step.

Getting a certificate

Whether you are getting a certificate to encrypt traffic from your server to Cloudflare or using another registrar and encrypting your traffic from your server to the browser, the process of getting a certificate is the same. And thankfully it's pretty easy in Ubuntu 22.04 if you use the Certbot client for Let's Encrypt.

First, install Certbot on your server:

sudo snap install core
sudo snap refresh core
sudo apt remove certbot
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

Then tell Certbot which domains you want to get certificates for, swapping out your own domain names after the -d flags:

sudo certbot --nginx -d learndart.dev -d myapp.learndart.dev

You'll have to provide your email and agree to their terms of service.

Cerbot will request TLS certificates for your site from Let's Encrypt and then update your server configuration files.

Open the files to see the changes that Certbot made:

sudo nano /etc/nginx/sites-available/learndart.dev

Here's the modified content:

server {
    server_name learndart.dev www.learndart.dev;

    location / {
        root /var/www/learndart.dev/html;
        index index.html;
    }

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/learndart.dev/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/learndart.dev/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

server {
    if ($host = learndart.dev) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    listen 80;
    server_name learndart.dev www.learndart.dev;
    return 404; # managed by Certbot
}

Note the following points:

Also check out the changes to your app site:

sudo nano /etc/nginx/sites-available/myapp.learndart.dev

Here's the content:

server {
    server_name myapp.learndart.dev;

    location / {
        root /var/www/myapp.learndart.dev/html;
        index index.html;
    }

    location /api/ {
        proxy_pass http://localhost:8080/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/learndart.dev/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/learndart.dev/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

server {
    if ($host = myapp.learndart.dev) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    listen 80;
    server_name myapp.learndart.dev;
    return 404; # managed by Certbot
}

Mostly this is the same, but I had you open this file to note that the /api proxy is still using HTTP on port 8080, not HTTPS. That's because calls to localhost port 8080 are internal and not exposed to the public internet. Since presumably you are the only one with login access to the server, this is probably fine. For a larger company with multiple employees that have access to the server, you may need to consider additional security measures.

Visit your site (https://learndart.dev) in a browser and it should be working over HTTPS now. Congratulations!

HTTPS working

You can log out of your server for now:

exit

Next, you'll move on to Docker and Dart.

Step 3: Docker

Before you actually get to Docker, you need to prepare your MyApp API server. While anything that runs in Docker is fine, for this tutorial, you'll use the default Dart Shelf server that is auto-generated when you create a new Shelf project.

Creating a sample Dart server

Assuming you have Dart installed on your local machine, run the following command:

dart create -t server-shelf my_app_server

Test that it's working locally by starting the server:

cd my_app_server
dart run bin/server.dart

Open the following address in a browser:

You should see Hello, World!:

Dart server working locally

You can stop the server by pressing Ctrl+C.

Evaluating alternative deployment options

Now that your server is ready to deploy, there are several options to run a Dart server on a VPS:

There are advantages and disadvantages to each of these options. The Docker option is relatively painless and it's nice to have everything isolated in its own container, so that's the route I'll take in this tutorial.

Setting up Docker locally

Docker is a program that lets you to bundle all of the dependencies your project needs along with the project itself. This allows the application to run in a consistent environment regardless of the machine it's located on.

To start with, you need to install Docker on your local machine. I'll leave that for you to figure out.

Browsing a Dockerfile

Have a look at the Dockerfile in your my_app_server project:

# Use latest stable channel SDK.
FROM dart:stable AS build

# Resolve app dependencies.
WORKDIR /app
COPY pubspec.* ./
RUN dart pub get

# Copy app source code (except anything in .dockerignore) and AOT compile app.
COPY . .
RUN dart compile exe bin/server.dart -o bin/server

# Build minimal serving image from AOT-compiled `/server`
# and the pre-built AOT-runtime in the `/runtime/` directory of the base image.
FROM scratch
COPY --from=build /runtime/ /
COPY --from=build /app/bin/server /app/bin/

# Start server.
EXPOSE 8080
CMD ["/app/bin/server"]

The comments in the file tell you what each step is doing. The main thing to understand is that the Dockerfile tells how to build a Docker image.

Note: When learning Docker, you'll see the terms "image" and "container" a lot. Sometimes the difference can be confusing. You can think of an image like a Dart class and the container as an instance of that class. The image is a template for creating containers and the container is what actually runs on your system. This article is pretty good if you want a more in-depth explanation.

Building a Docker image

Now that you've installed Docker on your local machine, make sure Docker is running.

Then, from within your my_app_server project, run the following terminal command to build the Docker image:

docker build . -t myapp:v1.0.0

Here are a few notes:

List the existing Docker images:

docker images

You should see something similar to the following:

REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
myapp        v1.0.0    ac4498012b1b   5 minutes ago   10.1MB

Creating and starting a Docker container

Test your image to make sure it's working locally:

docker run -it -p 8080:8080 myapp:v1.0.0

Here are a few notes of what that does:

Note: My local machine is an Intel chip Mac and that's all I've tested this on. It runs the default image we created earlier. If you're running Windows or an Apple chip Mac, I'm not sure if you'll need to specify a different platform for the image build step. If you can let me know whether it's also working on these platforms, I'd appreciate it.

Open the following address in a browser:

You should see Hello, World! again as you did when you ran your program directly with Dart.

Stopping a Docker container

Pressing Ctrl+C doesn't work to stop a running Docker container. To stop it, open another terminal and run:

docker ps

This will tell you the container ID:

CONTAINER ID   IMAGE          COMMAND             CREATED         STATUS         PORTS                    NAMES
e3da89cdf8ef   myapp:v1.0.0   "/app/bin/server"   5 minutes ago   Up 5 minutes   0.0.0.0:8080->8080/tcp   serene_morse

Then run the following command, replacing the container ID with your own:

docker stop e3da89cdf8ef

Moving a Docker image to the server

Now it's time to copy your Docker image to the server.

First, save your image as a tar file:

docker save -o myapp-v1.0.0.tar myapp:v1.0.0

Copy the image to the home folder of your server:

scp myapp-v1.0.0.tar [email protected]:~

Log in to your server:

ssh [email protected]

Confirm that the image is there:

ls

Setting up Docker on the server

Next you need to install Docker on the server. The following directions come from the Docker docs. You can also check out the DigitalOcean guide for help.

First uninstall any old versions of Docker:

for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt remove $pkg; done

Then run each of the following commands to add Docker's official GPG key:

sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

Then add the Docker repository to the Apt sources:

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Update the packages again:

sudo apt update

Now you can finally install Docker:

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Check that Docker is installed by running the following command:

sudo docker run hello-world

You should see a Hello from Docker! message among other output.

Running your Docker image on the server

Now you can load the Docker image you saved earlier:

sudo docker load -i myapp-v1.0.0.tar

The -i flag refers to the input file.

Check to see if your image loaded:

sudo docker images

You should see output similar to the following:

REPOSITORY    TAG       IMAGE ID       CREATED          SIZE
myapp         v1.0.0    ac4498012b1b   51 minutes ago   10.1MB
hello-world   latest    d2c94e258dcb   12 months ago    13.3kB

Now start running your container:

sudo docker run -d --restart unless-stopped -p 8080:8080 myapp:v1.0.0

This is similar to the run command when you ran the Docker container on your local machine. Here are the new parts:

Check that the container is running:

sudo docker ps

You should see something like the following:

CONTAINER ID   IMAGE          COMMAND             CREATED         STATUS         PORTS                    NAMES
a1af7688de9a   myapp:v1.0.0   "/app/bin/server"   2 minutes ago   Up 2 minutes   0.0.0.0:8080->8080/tcp   relaxed_hermann

Testing it out

First make sure that Docker is working internally on your server:

curl localhost:8080

You should see Hello, World! on the command line.

Now for the big test. Go to your browser and navigate to

You should see the Hello, World! message.

Success!

Congratulations! You've deployed your first Dart server to a VPS.

Troubleshooting

It can be helpful to check the logs to see what is happening:

This is for Docker:

docker ps
sudo docker logs <container id>

And you can use this one for Nginx:

sudo tail -f /var/log/nginx/access.log

Conclusion

As you can see, it's an involved process to deploy a server to a VPS. However, when you do it like this you're in complete control. And after doing it a few times, it gets easier. Bookmark this page, and come back the next time you need to set up a server.

If you have any comments, discuss them on the Reddit post for this article, or you can reach me on X @Suragch1.

P.S. I'm using the following commands to upload my Stack Shock site (including this page) to the server:

shock build
rsync -avz --delete build/ [email protected]:/var/www/learndart.dev/html/

You can ask your favorite LLM about what that command does.