|
| 1 | +# Serverpod Deployment to a VPS using Docker |
| 2 | + |
| 3 | +> 💡 Deploying Serverpod to a Virtual Private Server (VPS) using Docker is a |
| 4 | +> cost-effective and scalable solution for startups and small- to medium-scale |
| 5 | +> projects. |
| 6 | +
|
| 7 | +This guide walks you through deploying a server built using the Serverpod |
| 8 | +framework to a Virtual Private Server (VPS) with Docker. |
| 9 | + |
| 10 | +Serverpod is a Flutter/Dart backend framework offering database integration and |
| 11 | +seamless client–server communication. VPS deployment provides a cost-effective |
| 12 | +solution for small to medium projects. |
| 13 | + |
| 14 | +The setup allows vertical scaling through VM upgrades and can be extended to |
| 15 | +horizontal scaling with load balancers. This guide helps you create a |
| 16 | +production-ready Docker Compose deployment. |
| 17 | + |
| 18 | +To reduce the workload on the machine, we do not use Redis in this deployment. |
| 19 | +(Redis becomes necessary when you want to scale your application horizontally.) |
| 20 | + |
| 21 | +> 💡 In many cases, scaling vertically is sufficient and saves you the hassle of |
| 22 | +> setting up a load balancer and additional infrastructure. Always start with |
| 23 | +> vertical scaling and only scale horizontally if you need to. |
| 24 | +
|
| 25 | +## Prerequisites |
| 26 | + |
| 27 | +- This guide assumes you have basic knowledge of Serverpod and command-line usage. |
| 28 | +- This guide contains terminal commands that are specific to Unix-based systems (macOS & Linux). |
| 29 | +- The `docker-compose.production.yaml` file is configured to run on ARM machines. |
| 30 | + |
| 31 | +## Table of Contents |
| 32 | + |
| 33 | +- [Prerequisites](#prerequisites) |
| 34 | +- [Table of Contents](#table-of-contents) |
| 35 | +- [Preparing the server](#preparing-the-server) |
| 36 | + - [Registering at Hetzner Cloud](#registering-at-hetzner-cloud) |
| 37 | + - [Setting up an SSH key to connect to the server](#setting-up-an-ssh-key-to-connect-to-the-server) |
| 38 | + - [Creating a new server](#creating-a-new-server) |
| 39 | + - [Setting up the server](#setting-up-the-server) |
| 40 | + - [Step 1: Create the new user](#step-1-create-the-new-user) |
| 41 | + - [Step 2: Grant Docker permissions](#step-2-grant-docker-permissions) |
| 42 | + - [Step 3: Enable SSH access](#step-3-enable-ssh-access) |
| 43 | + - [Step 4: Set up SSH key-based authentication](#step-4-set-up-ssh-key-based-authentication) |
| 44 | + - [Firewall configuration](#firewall-configuration) |
| 45 | +- [Preparing the domain](#preparing-the-domain) |
| 46 | +- [Preparing the repository](#preparing-the-repository) |
| 47 | + - [Getting a GitHub Personal Access Token](#getting-a-github-personal-access-token) |
| 48 | + - [Adding the secrets to the repository](#adding-the-secrets-to-the-repository) |
| 49 | +- [Creating the deployment files](#creating-the-deployment-files) |
| 50 | +- [Configuring SSL-certificates](#configuring-ssl-certificates) |
| 51 | +- [Configuring the GitHub-Action](#configuring-the-github-action) |
| 52 | +- [Running the GitHub-Action](#running-the-github-action) |
| 53 | +- [Using the Serverpod Insights app](#using-the-serverpod-insights-app) |
| 54 | +- [Connecting your Flutter client](#connecting-your-flutter-client) |
| 55 | +- [Connecting to the Database using DBeaver](#connecting-to-the-database-using-dbeaver) |
| 56 | + |
| 57 | +## Preparing the server |
| 58 | + |
| 59 | +This guide uses Hetzner Cloud. You can use any server provider, but Hetzner is a |
| 60 | +good and cost-effective option. If you want to use another architecture or |
| 61 | +provider, check the Docker Compose file and the deployment script for any |
| 62 | +necessary changes. Currently, the deployment is meant to run on ARM machines. |
| 63 | + |
| 64 | +### Registering at Hetzner Cloud |
| 65 | + |
| 66 | +Register an account at Hetzner Cloud and create a new project. |
| 67 | +[Use this referral link to get €20 credits for free at Hetzner Cloud](https://hetzner.cloud/?ref=BFdFFipLgfDs) |
| 68 | + |
| 69 | +Next, go to the [Cloud Console](https://console.hetzner.cloud/) and create a project. |
| 70 | + |
| 71 | +### Setting up an SSH key to connect to the server |
| 72 | + |
| 73 | +In order to configure your server, you need to access it through SSH. Create an |
| 74 | +SSH keypair if you don't have one yet. If you are not sure whether you already |
| 75 | +have one, you can check by running: |
| 76 | + |
| 77 | +```bash |
| 78 | +cat ~/.ssh/id_rsa.pub |
| 79 | +``` |
| 80 | + |
| 81 | +To create a new keypair, run the following command. Leave all options at their |
| 82 | +default values by pressing enter. |
| 83 | + |
| 84 | +```bash |
| 85 | +ssh-keygen -t rsa -b 4096 |
| 86 | +``` |
| 87 | + |
| 88 | +When asked for a password, don't enter anything—just press enter. This will |
| 89 | +create a keypair in `~/.ssh/id_rsa` and `~/.ssh/id_rsa.pub`. |
| 90 | + |
| 91 | +Copy the public key to your clipboard: |
| 92 | + |
| 93 | +```bash |
| 94 | +cat ~/.ssh/id_rsa.pub |
| 95 | +``` |
| 96 | + |
| 97 | +Select the output and copy it. |
| 98 | + |
| 99 | +In your Hetzner project, follow these steps: |
| 100 | + |
| 101 | +1. In the left-hand menu, click on **Security** > **SSH keys** > **Add SSH key**. |
| 102 | +2. Paste the public key you generated earlier. |
| 103 | + |
| 104 | +### Creating a new server |
| 105 | + |
| 106 | +Continuing in your Hetzner project, create a new server: |
| 107 | + |
| 108 | +1. In the left-hand menu, go to **Server** and click **Create server**. |
| 109 | +2. In the **Image** section, click on **Apps** and select **Docker CE**. |
| 110 | +3. **Type/Architecture:** Select **vCPU** and **Arm64 (Ampere)**. The smallest tier is sufficient for most projects—you can always upgrade the specs later. |
| 111 | +4. Ensure that the public IPv4 address is enabled. |
| 112 | +5. In the SSH-Keys section, make sure your SSH key is selected. |
| 113 | +6. Name your server and create it. |
| 114 | + |
| 115 | +### Setting up the server |
| 116 | + |
| 117 | +Once the server is created, you can connect to it using SSH. Find the server IP |
| 118 | +in the Hetzner Cloud Console and connect using the following command: |
| 119 | + |
| 120 | +```bash |
| 121 | +ssh root@<your-server-ip> |
| 122 | +``` |
| 123 | + |
| 124 | +When prompted with "Are you sure you want to continue connecting? [...]" type "yes" and press enter. |
| 125 | + |
| 126 | +> If you are asked for a password, it means the SSH key was not added correctly. |
| 127 | +> Delete the corresponding entry from `~/.ssh/known_hosts` and delete the |
| 128 | +> server. Then create a new server and ensure the SSH key is added correctly. |
| 129 | +
|
| 130 | +For security reasons, we will create a new user to manage the deployment. This user will not have root privileges. |
| 131 | + |
| 132 | +#### Step 1: Create the new user |
| 133 | + |
| 134 | +```bash |
| 135 | +sudo adduser github-actions |
| 136 | +``` |
| 137 | + |
| 138 | +Replace `github-actions` with your desired username. This command will prompt |
| 139 | +you to set a password and enter user information. |
| 140 | + |
| 141 | +#### Step 2: Grant Docker permissions |
| 142 | + |
| 143 | +Add the user to the `docker` group so they can run Docker commands: |
| 144 | + |
| 145 | +```bash |
| 146 | +sudo usermod -aG docker github-actions |
| 147 | +``` |
| 148 | + |
| 149 | +#### Step 3: Enable SSH access |
| 150 | + |
| 151 | +SSH access is available by default for any user on the server. However, to ensure proper access, check the `sshd_config` file: |
| 152 | + |
| 153 | +```bash |
| 154 | +sudo nano /etc/ssh/sshd_config |
| 155 | +``` |
| 156 | + |
| 157 | +Find or add the `AllowUsers` directive. This directive specifies which users are |
| 158 | +allowed to SSH into the server. If it doesn't exist, add it at the end of the |
| 159 | +file. If there are multiple users, separate them with spaces: |
| 160 | + |
| 161 | +```text |
| 162 | +AllowUsers root github-actions |
| 163 | +``` |
| 164 | + |
| 165 | +To save and exit the file, press `Ctrl + X`, then `Y`, and finally `Enter`. Restart the SSH service to apply the changes: |
| 166 | + |
| 167 | +```bash |
| 168 | +sudo systemctl restart ssh |
| 169 | +``` |
| 170 | + |
| 171 | +#### Step 4: Set up SSH key-based authentication |
| 172 | + |
| 173 | +1. Log in as the new user: |
| 174 | + |
| 175 | + ```bash |
| 176 | + su - github-actions |
| 177 | + ``` |
| 178 | + |
| 179 | +2. Create an SSH keypair: |
| 180 | + |
| 181 | + ```bash |
| 182 | + ssh-keygen -t rsa -b 4096 |
| 183 | + ``` |
| 184 | + |
| 185 | + Leave the options at their default values by pressing enter. |
| 186 | + |
| 187 | +3. Add the public SSH key to the `authorized_keys` file: |
| 188 | + |
| 189 | + ```bash |
| 190 | + cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys |
| 191 | + ``` |
| 192 | + |
| 193 | +4. Copy the private key to your clipboard—including the lines `-----BEGIN OPENSSH PRIVATE KEY-----` and `-----END OPENSSH PRIVATE KEY-----`. Save this key in a secure location, as you will need it later. To display the private key, run: |
| 194 | + |
| 195 | + ```bash |
| 196 | + cat ~/.ssh/id_rsa |
| 197 | + ``` |
| 198 | + |
| 199 | +5. Logout from the github-actions user: |
| 200 | + |
| 201 | + ```bash |
| 202 | + exit |
| 203 | + ``` |
| 204 | + |
| 205 | +6. Restart the SSH service to apply changes: |
| 206 | + |
| 207 | + ```bash |
| 208 | + sudo systemctl restart ssh |
| 209 | + ``` |
| 210 | + |
| 211 | +### Firewall configuration |
| 212 | + |
| 213 | +In the Hetzner Web Interface, enter your server configuration and click on **Firewalls**, then click **Create Firewall**. |
| 214 | + |
| 215 | +By default, there will be two inbound rules: one for SSH (port 22) and one for ICMP. We will add two more for HTTP and HTTPS. |
| 216 | + |
| 217 | +1. Click on **Add Rule**, name it HTTP, set the port to 80, and choose TCP as the protocol. |
| 218 | +2. Click on **Add Rule**, name it HTTPS, set the port to 443, and choose TCP as the protocol. |
| 219 | +3. In the "apply to" section, make sure your server is selected. |
| 220 | +4. Click **Create Firewall**. |
| 221 | + |
| 222 | +## Preparing the domain |
| 223 | + |
| 224 | +To access your server, you need to have a domain. You can purchase a domain from |
| 225 | +any provider (e.g., [Namecheap](https://www.namecheap.com/) or |
| 226 | +[GoDaddy](https://www.godaddy.com/)). |
| 227 | + |
| 228 | +Once you have a domain, set up the DNS records to point to your server. Create |
| 229 | +the following DNS records, replacing `Your server IP` with the IP address of |
| 230 | +your server: |
| 231 | + |
| 232 | +| Type | Name | Value | |
| 233 | +| ---- | -------- | -------------- | |
| 234 | +| A | api | Your server IP | |
| 235 | +| A | web | Your server IP | |
| 236 | +| A | insights | Your server IP | |
| 237 | + |
| 238 | +The full domains will be `api.your-domain.com`, `web.your-domain.com`, and `insights.your-domain.com`. |
| 239 | + |
| 240 | +## Preparing the repository |
| 241 | + |
| 242 | +### Getting a GitHub Personal Access Token |
| 243 | + |
| 244 | +1. Create a new Personal Access Token (PAT) on GitHub: |
| 245 | + - Click on your profile picture in the top-right corner. |
| 246 | + - Navigate to **Settings** > **Developer settings** > **Personal access tokens** > **Tokens (classic)**. |
| 247 | + - Click on **Generate new token**. |
| 248 | + |
| 249 | +2. Fill in the required fields: |
| 250 | + - In the **Note** field, set a name for the token, e.g., "Serverpod Deployment". |
| 251 | + - Set the expiration to **No expiration**. |
| 252 | + |
| 253 | +3. Select the necessary scopes: |
| 254 | + - **repo**: Required to read repositories, especially private ones. |
| 255 | + - **write:packages**: Required to push Docker images to the GitHub Package Registry. |
| 256 | + |
| 257 | +4. Generate and save the token: |
| 258 | + - Scroll to the bottom and click **Generate token**. |
| 259 | + - Copy the token and save it in a secure place. |
| 260 | + |
| 261 | +### Adding the secrets to the repository |
| 262 | + |
| 263 | +Go to your Serverpod project repository, then navigate to **Settings** > **Secrets and variables** > **Actions** and create the following secrets: |
| 264 | + |
| 265 | +| Secret Name | Value | |
| 266 | +| --------------- | --------------------------------------------------------------- | |
| 267 | +| PAT_USER_GITHUB | Your GitHub username | |
| 268 | +| PAT_GITHUB | Your GitHub PAT token | |
| 269 | +| SSH_HOST | The IP address of your server | |
| 270 | +| SSH_USER | The username you created on the server (e.g., "github-actions") | |
| 271 | +| SSH_PRIVATE_KEY | The private key you generated on the server | |
| 272 | + |
| 273 | +The following secrets configure Serverpod and the database: |
| 274 | + |
| 275 | +| Secret Name | Value | |
| 276 | +| ------------------------------------- | ----------------------------------------------------------------------------------------------------------- | |
| 277 | +| SERVERPOD_DATABASE_NAME | The name of the database (e.g., "serverpod") | |
| 278 | +| SERVERPOD_DATABASE_USER | The database user (e.g., "serverpod") | |
| 279 | +| SERVERPOD_DATABASE_PASSWORD | The database password | |
| 280 | +| SERVERPOD_API_SERVER_PUBLIC_HOST | The domain for the API server (e.g., api.my-domain.com) | |
| 281 | +| SERVERPOD_WEB_SERVER_PUBLIC_HOST | The domain for the Web server (e.g., web.my-domain.com) | |
| 282 | +| SERVERPOD_INSIGHTS_SERVER_PUBLIC_HOST | The domain for the Insights server (e.g., insights.my-domain.com) | |
| 283 | +| SERVERPOD_SERVICE_SECRET | The same value as in your local `passwords.yaml` file, required to connect using the Serverpod Insights app | |
| 284 | + |
| 285 | +## Creating the deployment files |
| 286 | + |
| 287 | +The CLI will generate all necessary deployment files for your project. |
| 288 | + |
| 289 | +1. Install the Serverpod VPS CLI: |
| 290 | + |
| 291 | + ```bash |
| 292 | + dart pub global activate serverpod_vps |
| 293 | + ``` |
| 294 | + |
| 295 | +2. Navigate to your Serverpod project directory: |
| 296 | + |
| 297 | + ```bash |
| 298 | + cd my_serverpod_project |
| 299 | + ``` |
| 300 | + |
| 301 | +3. Run the CLI to generate deployment files: |
| 302 | + |
| 303 | + ```bash |
| 304 | + serverpod_vps |
| 305 | + ``` |
| 306 | + |
| 307 | +4. When prompted, enter your email address for SSL certificate notifications. |
| 308 | + |
| 309 | +## Configuring SSL-certificates |
| 310 | + |
| 311 | +All external connections are secured by Traefik through HTTPS. Traefik uses |
| 312 | +[Let's Encrypt](https://letsencrypt.org/) to automatically generate SSL |
| 313 | +certificates for your domains. |
| 314 | + |
| 315 | +If you need to change the email address that Let's Encrypt uses for certificate |
| 316 | +notifications, edit the email address in the `docker-compose.production.yaml` |
| 317 | +file. Open the file and modify the value of the parameter |
| 318 | +`certificatesresolvers.myresolver.acme.email`. |
| 319 | + |
| 320 | +## Configuring the GitHub-Action |
| 321 | + |
| 322 | +From the root of your repository, open the `.github/workflows/deployment-docker.yml` file and adjust the following settings: |
| 323 | + |
| 324 | +- Update the `GHCR_ORG` variable by replacing `<ORGANIZATION>` with your GitHub username or organization name. |
| 325 | +- At the top of the file, you can change the branches that automatically trigger the deployment. By default, it is set to `main`. You can also trigger the action manually on a different branch. |
| 326 | + |
| 327 | +## Running the GitHub-Action |
| 328 | + |
| 329 | +Push your changes to the repository on the configured branch. |
| 330 | + |
| 331 | +To manually trigger the action, go to the **Actions** tab in your repository, |
| 332 | +click on the **Deploy to Docker** workflow, then click **Run workflow** and |
| 333 | +select the branch you want to deploy. |
| 334 | + |
| 335 | +## Using the Serverpod Insights app |
| 336 | + |
| 337 | +To enable the [Serverpod Insights app](https://docs.serverpod.dev/tools/insights), |
| 338 | +adjust the insights server host in `production.yaml` to the domain you set up in |
| 339 | +your DNS records. Ensure that the service secret specified in the repository |
| 340 | +secrets matches the one in your local `passwords.yaml` file for production. |
| 341 | + |
| 342 | +## Connecting your Flutter client |
| 343 | + |
| 344 | +To connect with your generated client, use the domain you set up in the DNS. |
| 345 | +Make sure to use HTTPS without any port numbers (e.g., |
| 346 | +`https://api.my-domain.com`). |
| 347 | + |
| 348 | +## Connecting to the Database using DBeaver |
| 349 | + |
| 350 | +To manage your database, you can use a tool like [DBeaver](https://dbeaver.io/). |
| 351 | +To connect to the database, set up an SSH tunnel to the server using the |
| 352 | +following example: |
| 353 | + |
| 354 | +1. Open DBeaver and click the **New Database Connection** button (usually in the top-left corner). |
| 355 | +2. Select **PostgreSQL** and click **Next**. |
| 356 | +3. Click the **SSH** tab. |
| 357 | +4. Check **Use SSH tunnel**. |
| 358 | +5. Set the following values: |
| 359 | + - **Host/IP:** Your server IP |
| 360 | + - **Port:** 22 |
| 361 | + - **Username:** root |
| 362 | + - **Authentication method:** Public key |
| 363 | + - **Private key:** The private key on your machine you [generated earlier](#setting-up-an-ssh-key-to-connect-to-the-server) |
| 364 | +6. Click **Test tunnel** to verify the connection. |
| 365 | +7. Return to the **Main** tab. |
| 366 | +8. Set the following values: |
| 367 | + - **Host:** localhost |
| 368 | + - **Port:** 5432 |
| 369 | + - **Database:** The database name you set in the [repository secrets](#adding-the-secrets-to-the-repository) |
| 370 | + - **User name:** The database user you set in the [repository secrets](#adding-the-secrets-to-the-repository) |
| 371 | + - **Password:** The database password you set in the [repository secrets](#adding-the-secrets-to-the-repository) |
| 372 | +9. Test the connection and save it. |
0 commit comments