Deploy to VPS with Docker

Published:

For me, deploying web applications is hard and complex. I always re-read stuff every time. This article assumes Ubuntu 20.10 as host server. You can use DigitalOcean to host your servers. The technologies are:

  • Docker and Docker Compose
  • Nginx

All CAPITALIZED texts are supposed to be replaced with proper values.

Summary

Configure the Application

  • Ensure that production credentials are not in the repository, create a separate .env.production.
  • Configure the filepath of encryption keys to use pattern ANY_ABSOLUTE_PATH/keys/.

For this step I recommend having separate Docker configurations for development environments and use the default for production, so you would end up with

  • Dockerfile (production)
  • Dockerfile.dev
  • docker-compose.yml (production)
  • docker-compose.dev.yml
  • .dockerignore (files to ignore in containers)
  • entrypoint.sh (application startup script)

Create Dockerfile

Find a proper image for you application at https://hub.docker.com/. Be as specific as possible.

FROM IMAGE_NAME:VERSION

# System install.
RUN apt install PACKAGE_NAME -y     # e.g. texlive

# Dependencies and Build Commands
WORKDIR /code
COPY DEPENDENCY_FILE /code/         # e.g. package.json
RUN INSTALL_COMMAND                 # e.g. yarn install

# Copy your code to container if needed.
COPY . /

Create docker-compose.yml

In production, you usually don't put your databases here because you should be able to run several containers of same application. I recommend using managed databases.

version: "VERSION_NUMBER"

services:
  # such as database, for development and prototype environments
  SERVICE_NICKNAME_1:
    image: IMAGE_NAME
    env_file:
      - .env
    restart: always # you can omit for development
  SERVICE_NICKNAME_2:
    image: IMAGE_NAME
    env_file:
      - .env
  web:
    build:
      context: .
      dockerfile: Dockerfile  # for development use Dockerfile.dev
    command: /code/entrypoint.sh
    depends_on:
      - SERVICE_NICKNAME_1
      - SERVICE_NICKNAME_2
    env_file:
      - .env
    ports:
      - "APPLICATION_PORT:CONTAINER_PORT"  # e.g. "8000:8000"
    restart: always
    volumes:
      - .:/code
      - BLOCK_STORAGE_PATH:/code/block-storage

Remember to start your application at 0.0.0.0, then it will be visible outside. For example:

# entrypoint.sh
gunicorn APP_STARTUP_FILE:app --host 0.0.0.0 --port CONTAINER_PORT -w 4 -k uvicorn.workers.UvicornWorker

Server Setup

Generate SSH Key

The keys are usually located at ~/.ssh.

cd ~/.ssh
mkdir APP_NAME
ssh-keygen -t ed25519 -f ./APP_NAME/KEY_NAME
# e.g. ./ecommerce/main_digital_ocean

Create the Server

  1. Go to you cloud platform and create a server there.
  2. Open the public SSH key ~/.ssh/APP_NAME/CLOUD_NAME.pub
  3. Add that as your server SSH key.

WARNING: Do not enable password access.

Firewall

Immediately after the server is created, add the firewall. This can usually be done in the cloud provider's dashboard. Add the following rules:

PortTypeSources
22SSHYOUR_IP_ADDRESS
443HTTPS0.0.0.0

The rules are to prevent SSH connections from unknown IP address. You may need to expose other ports depending on your application. There exists serverless setups that require database to be exposed, I recommend not to do that.

SSH Config File

  1. Obtain your server IP-address from cloud dashboard.
  2. Open ~/.ssh/config
  3. Add the following lines:
Host APP_NAME SERVER_IP_ADDRESS
  HostName SERVER_IP_ADDRESS
  User webapp
  IdentityFile ~/.ssh/APP_NAME/CLOUD_NAME.pem

https://linux.die.net/man/5/ssh_config

Create a non-root user

ssh root@APP_NAME
adduser webapp
usermod -aG sudo webapp
rsync --archive --chown=webapp:webapp ~/.ssh /home/webapp
exit

https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu-20-04

Update the System

ssh APP_NAME
sudo apt update
sudo apt upgrade

Install Docker and Docker Compose

Just follow the official instructions, commands are not added here because they are complex and may change:

https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository

Setup the Application

The application should be in Git repository host like GitLab or GitHub. Add a read-only SSH key to your repository. GitHub calls them deploy keys:

https://docs.github.com/en/developers/overview/managing-deploy-keys#deploy-keys

Clone your application somewhere, I suggest ~/projects.

Environment Variables

Create ~/projects/APP_NAME/.env file and put your credentials and configurations there.

Application Start

Then start your application:

docker-compose build
docker-compose up -d

https://docs.docker.com/compose/production/

Point your domain to the server

Go to DNS provider you use (could be same as domain registrar), add A record that point to your server IP. The IP may change if you shut down your server, in that case you need to update it or reserve an IP (usually one or two euros a month).

Install Nginx

sudo apt install nginx

Configure Nginx

Open file at /etc/nginx/sites-available/APPLICATION_DOMAIN/nginx.conf

http {
  server {
    listen 80;
    server_name APPLICATION_DOMAIN;

    location / {
      proxy_pass http://127.0.0.0:APPLICATION_PORT;
      proxy_set_header    Host                $host;
      proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;

      limit_except GET HEAD POST PUT PATCH DELETE OPTIONS { deny all; }
    }
  }
}

HTTPS Certificates

Follow the instructions: https://certbot.eff.org/lets-encrypt/ubuntufocal-nginx

Choose the option to redirect HTTP to HTTPS. After the certbot is done, your site should be up.